././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3501275 ceilometer-27.0.0.0rc1/0000775000175000017500000000000015250267567013335 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1353567 ceilometer-27.0.0.0rc1/.coveragerc0000664000175000017500000000020215250267452015441 0ustar00zuulzuul[run] branch = True source = ceilometer omit = ceilometer/tests/* dynamic_context = test_function [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1363568 ceilometer-27.0.0.0rc1/.mailmap0000664000175000017500000000370615250267452014755 0ustar00zuulzuul# Format is: # # Adam Gandelman Alan Pevec Alexei Kornienko ChangBo Guo(gcb) Chang Bo Guo Chinmaya Bharadwaj chinmay Clark Boylan Doug Hellmann Fei Long Wang Fengqian Gao Fengqian Fengqian Gao Fengqian.Gao Gordon Chung gordon chung Gordon Chung Gordon Chung Gordon Chung gordon chung Ildiko Vancsa Ildiko John H. Tran John Tran Julien Danjou LiuSheng liu-sheng Mehdi Abaakouk Nejc Saje Nejc Saje Nicolas Barcet (nijaba) Pádraig Brady Rich Bowen Sandy Walsh Sascha Peilicke Sean Dague Shengjie Min shengjie-min Shuangtai Tian shuangtai Swann Croiset ZhiQiang Fan ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1373568 ceilometer-27.0.0.0rc1/.pre-commit-config.yaml0000664000175000017500000000323715250267452017614 0ustar00zuulzuulrepos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: trailing-whitespace # Replaces or checks mixed line ending - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' # Forbid files which have a UTF-8 byte-order marker - id: fix-byte-order-marker # Checks that non-binary executables have a proper shebang - id: check-executables-have-shebangs # Check for files that contain merge conflict strings. - id: check-merge-conflict # Check for debugger imports and py37+ breakpoint() # calls in python source - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - repo: https://github.com/hhatto/autopep8 rev: v2.3.2 hooks: - id: autopep8 files: '^.*\.py$' - repo: https://opendev.org/openstack/hacking rev: 7.0.0 hooks: - id: hacking additional_dependencies: [] - repo: https://github.com/PyCQA/doc8 rev: v2.0.0 hooks: - id: doc8 - repo: https://github.com/openstack/bashate rev: 2.1.1 hooks: - id: bashate args: ['-v', '-iE006'] exclude: '.tox/.*' - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: - id: codespell args: ['--ignore-words=doc/dictionary.txt'] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.7 hooks: - id: ruff-check args: ['--fix', '--unsafe-fixes', '--show-fixes'] #--diff'] # Add this after submitting a commit for ruff check, then one that fixes linting, then one to move autopep to after this. #- id: ruff-format ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1383567 ceilometer-27.0.0.0rc1/.stestr.conf0000664000175000017500000000010515250267452015573 0ustar00zuulzuul[DEFAULT] test_path=${OS_TEST_PATH:-ceilometer/tests/unit} top_dir=./././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1383567 ceilometer-27.0.0.0rc1/.zuul.yaml0000664000175000017500000000473515250267452015300 0ustar00zuulzuul- job: name: grenade-ceilometer parent: grenade required-projects: - opendev.org/openstack/grenade - opendev.org/openstack/ceilometer - name: gnocchixyz/gnocchi override-checkout: stable/4.7 vars: configure_swap_size: 8192 grenade_devstack_localrc: shared: CEILOMETER_BACKEND: gnocchi devstack_plugins: ceilometer: https://opendev.org/openstack/ceilometer devstack_services: ceilometer-acompute: true ceilometer-acentral: true ceilometer-aipmi: true ceilometer-anotification: true tempest_test_regex: '\[.*\bsmoke\b.*\]|^(telemetry_tempest_plugin)' tox_envlist: all irrelevant-files: &ceilometer-irrelevant-files - ^\.gitignore$ - ^\.gitreview$ - ^\.pre-commit-config\.yaml$ - ^test-requirements\.txt$ - ^setup.cfg$ - ^doc/.*$ - ^.*\.rst$ - ^releasenotes/.*$ - ^ceilometer/hacking/.*$ - ^ceilometer/locale/.*$ - ^ceilometer/tests/.*$ - ^tools/.*$ - ^tox.ini$ - project: queue: telemetry templates: - openstack-cover-jobs - openstack-python3-jobs - publish-openstack-docs-pti - periodic-stable-jobs - release-notes-jobs-python3 - check-requirements check: jobs: - grenade-ceilometer - telemetry-dsvm-integration-centos-9s: irrelevant-files: *ceilometer-irrelevant-files voting: false - telemetry-dsvm-integration-centos-9s-fips: irrelevant-files: *ceilometer-irrelevant-files voting: false - telemetry-dsvm-integration: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-aetos: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-aetos-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files gate: jobs: - grenade-ceilometer - telemetry-dsvm-integration: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-aetos: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-aetos-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788964727.043128 ceilometer-27.0.0.0rc1/AUTHORS0000664000175000017500000005035515250267567014415 0ustar00zuulzuul1iuwei <1iu.wei81@zte.com.cn> Abhishek Chanda Abhishek Lekshmanan Abhishek Lekshmanan Adelina Tuvenie Ajaya Agrawal Akhil Hingane Ala Rezmerita Alessandro Pilotti Alex Holden Alexander Chadin Alexandra Settle Alexei Kornienko Alfredo Moralejo Amy Fong Ana Malagon Ananya Chatterjee Andrea Frittoli Andreas Jaeger Andreas Jaeger Andrew Hutchings Andrew Melton Angus Lees Angus Salkeld Ann Kamyshnikova Arnaud Morin Arnaud Morin Arnaud Morin Artem Vasilyev Artur Svechnikov Ashwin Agate Balazs Gibizer Bartosz Górski Ben Nemec Ben Nemec Boris Pavlovic Brad Pokorny Brant Knudson Brian Cline Brian Moss Brooklyn Chen Béla Vancsics Callum Dickinson Can ZHANG Cao Xuan Hoang Cedric Soulas Chad Lung Chandan Kumar Chandan Kumar ChangBo Guo(gcb) Chaozhe.Chen Charles Bitter Chen Hanxiao ChenZheng Chinmaya Bharadwaj Chmouel Boudjnah Chris Dent Chris Dent Chris Sibbitt Christian Berendt Christian Martinez Christian Schwede Christian Zunker Christophe Useinovic Chuck Short Clark Boylan Claudiu Belu Cyril Roelandt Cyril Roelandt Dai Dang Van Damian Van Vuuren Dan Florea Dan Prince Dan Travis Danek Duvall Daniel Pawlik Daniel Russell Daniel Vincze Dao Cong Tien Darren Birkett Darren Hague Davanum Srinivas David Peraza David Rabel Dazhao Debo~ Dutta Deepthi V V Dina Belova Dirk Mueller Divya Dmitriy Rabotjagov Dmitriy Rabotyagov Dong Ma Dou Yuan Doug Hellmann Drew Thorstensen Edward Hope-Morley Edwin Zhai Elod Illes Emilien Macchi Emma Foley Emma Foley Endre Karlson Eoghan Glynn Eoghan Glynn Eric Berglund Eric Brown Erno Kuvaja Eyal Fabio Giannetti Fei Long Wang Feilong Wang Felix Walter Feng Xi Yan Fengqian Gao Flavio Percoco François Charlier François Rossigneux Frederic FAURE Gangyi Luo Gauvain Pocentek Gerard Garcia Ghanshyam Mann Gordon Chung Graham Binns Graham Hayes Guangyu Suo Gyorgy Szombathelyi Ha Van Tu Han Guangyu Hang Liu Hangdong Zhang Hanxi Hanxi Liu Hanxi_Liu Haomeng, Wang Harri Hämäläinen Hervé Beraud Hisashi Osanai Hoang Trung Hieu Hongbin Lu Huachao Mao Huan Xie Huang Rui Ian Wienand Ianeta Hutchinson Igor Degtiarov Ihar Hrachyshka Ildar Svetlov Ildiko Vancsa Ilya Sviridov Ilya Tyaptin Ionuț Arțăriși Ivan Anfimov Jake Liu James E. Blair James E. Blair James Page Jaromir Wysoglad Jaromír Wysoglad Jason Myers Jason Zhang Jay Lau Jay Pipes Jeffrey Zhang Jens Rosenboom Jeremy Stanley Ji-Wei Jiang Qin Jianghua Wang Jie Li Jim Rollenhagen Jimmy McCrory Joanna H. Huang Joe Gordon Joe H. Rahme John H. Tran John Herndon Jon Schlueter Jonte Watford JordanP Jorge Niedbalski Joseph Davis Joseph Richard Joshua Harlow JuPing Juan Antonio Osorio Robles Juan Larriba Julien Danjou June.King Justin SB KIYOHIRO ADACHI Kamil Rykowski Keith Byrne Ken Pepple Ken'ichi Ohmichi Ken'ichi Ohmichi Kennan Kennan Kevin McDonald Kevin_Zheng Kirill Bespalov Kishore Juigil Kobi Samoray Koert van der Veer Komei Shimamura Ladislav Smola Lan Qi song Lance Albertson Lars Kellogg-Stedman Laszlo Hegedus Leehom Li (feli5) Lena Novokshonova Lianhao Lu Lingxian Kong LinuxJedi LiuSheng Luigi Toscano Luis A. Garcia Luis Pigueiras Luo Gangyi Luong Anh Tuan Mahmoud Abouzeid Maho Koshiya Manik Bindlish Manuel Rodriguez Marios Andreou Mariusz Karpiarz Mark Goddard Mark McClain Mark McLoughlin Martin Geisler Martin Kletzander Martin Mágr Masanori Ueno Mathew Odden Mathieu Gagne� Mathieu Gagné Matt Riedemann Matt Wisch Matthias Runge Maxime Guyot Mehdi Abaakouk Mehdi Abaakouk Michael Krotscheck Michael Still Michał Jastrzębski Michel Nederlof Miguel Alex Cantu Miguel Grinberg Mike Spreitzer Milan Potdar Ming Shuang Xian Monsyne Dragon Monty Taylor Morgan Fainberg Nadya Privalova Nadya Shakhat Nam Nguyen Hoai Nejc Saje Ngo Quoc Cuong Nguyen Phuong An Nguyen Van Trung Nick Barcet Nicolas Barcet (nijaba) Nishant Kumar Noorul Islam K M Octavian Ciuhandu OpenStack Release Bot Pablo Iranzo Gómez PanFengyun PanFengyun Patrick East Paul Belanger Paul Bourke Pavlo Shchelokovskyy Pedro Pedro Henrique Peter Nordquist Peter Portante Petr Kovar Petr Kuběna Phil Neal Pierre Riteau Pierre Riteau Piyush Masrani Pradeep Kilambi Pradeep Kilambi Pradeep Kumar Singh Pradyumna Sampath Prudhvi Rao Shedimbi Pádraig Brady Qiaowei Ren Rabi Mishra Rafael Folco Rafael Rivero Rafael Weingärtner Rafal Szmigiel Rich Bowen Richard Devers Rikimaru Honjo Rob Raymond Robert Collins Robert Mizielski Rohit Jaiswal Romain Soufflet Roman Bogorodskiy Roman Podoliaka Rosario Di Somma Ruslan Aliev Russell Bryant Ryan Petrello Ryota MIBU SU, HAO-CHEN Saba Ahmed Sam Morrison Samta Samuel Merritt Sandy Walsh Sanja Nosan Sascha Peilicke Sean Dague Sean McGinnis Sean Mooney Sergey Lukjanov Sergey Vilgelm Seyeong Kim Shane Wang Shengjie Min Shi Yan Shilla Saebi Shuangtai Tian Shubham Chitranshi Simon Zhou Simona Iuliana Toader Sofer Athlan-Guyot Srinivas Sakhamuri Stas Maksimov Stefano Zilli Stephen Balukoff Stephen Finucane Stephen Gran Steve Lewis Steve Martinelli Steven Berler Sumant Murke Sumit Jamgade SunAnChen Supreeth Shivanand Surya Prabhakar Svetlana Shturm Swami Reddy Swann Croiset Swapnil Kulkarni (coolsvap) Sylvain Afchain Szymon Wroblewski Takashi Kajinami Takashi Kajinami Takashi NATSUME Taketani Ryo Tatsuro Makita Terri Yu Theo Gindre Thierry Carrez Thomas Bechtold Thomas Goirand Thomas Graichen Thomas Herve Thomas Herve Thomas Maddox Tin Lam Tobias Urdin Tobias Urdin Tong Li Tony Breeds Trinh Nguyen Tuan Do Anh Ubuntu Victor Stinner Victor Stinner Victoria Martinez de la Cruz Vinay Kapalavai Vitalii Lebedynskyi Vitaly Gridnev Vladislav Kuzmin Vu Cong Tuan WenyanZhang Wenzhi Yu Wissem MIMOUNA Witek Bedyk Witold Bedyk Wu Wenxiang Xia Linjuan XiaBing Yao Xiang Li XieYingYun Yadnesh Kulkarni Yadnesh Kulkarni Yaguang Tang Yaguang Tang Yandong Xuan Yanos Angelopoulos Yanyan Hu Yarko Tymciurak Yassine Lamgarchal Yathiraj Udupi You Yamagata Yuanbin.Chen Yunhong, Jiang Yurii Prokulevych Yuriy Zveryanskyy Yushiro FURUKAWA ZTE-SuZhengwei ZhaoBo Zhengwei Gao Zhi Kun Liu Zhi Yan Liu ZhiQiang Fan ZhongShengping Zhongyue Luo Zi Lian Ji aggaatul akhiljain23 alextricity25 ananya23d annegentle ansaba blue55 caoyuan cbitte000 ccrouch celik.esra chen-xing chenaidong1 chenxing daz dongwenjuan dsxyy eNovance emilienm fengchaoyang florent fujioka yuuichi gaofei gengchc2 gengjh ghanshyam ghanshyam gong yong sheng gord chung gugug guillaume pernot hanxi.liu hgangwx inspurericzhang jiaxi jimmygc jing.liuqing jinxingfang jizilian jlarriba jonnary joyce jwysogla kairat_kushaev kairoaraujo khushbuparakh kiwik-chenrui kpdev kuangcx leizhang lianghuifei lijian lijunjie likui lipan liuqing liusheng liuwei liyi liyuenan lizheming ljhuang lqslan lrqrun ls1175 lvdongbing lvxianguo lzhijun melissaml mgirgisf minruigao mizeng nellysmitt nicodemus npraveen35 obutenko pangliye pedro pleimer prankul prankul mahajan qin.jiang replay rwe sanuptpm sh.huang shangxiaobj shengjie min sin songwenping srsakhamuri tanlin terriyu unknown vagrant venkatamahesh vivek.nandavanam vivek.nandavanam wangqi wangqiangbj wangzihao wbluo0907 wu.shiming xialinjuan xianbin xiangjun li xiangjun.li xiaozhuangqing xiexianbin xingzhou xqk xugang xuqiankun yanghuichan yanheven yuyafei zhang-jinnan zhang-shaoman zhang.lei zhangboye zhangdaolong zhangguoqing zhangshengping2012 zhangxuanyuan zhangyangyang zhangyanxian zhaolihui zhufl zhurong zjingbj ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1383567 ceilometer-27.0.0.0rc1/CONTRIBUTING.rst0000664000175000017500000000106515250267452015771 0ustar00zuulzuulIf you would like to contribute to the development of OpenStack, you must follow the steps documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/ceilometer ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964726.6571286 ceilometer-27.0.0.0rc1/ChangeLog0000664000175000017500000055644515250267567015132 0ustar00zuulzuulCHANGES ======= 27.0.0.0rc1 ----------- * Fix typo in warning message * Set provider metadata on pool capacity samples * Remove redundant C302 check * Remove deprecated os\_cacert options * Clean up deprecated cli options * Remove invalid endpoint type * Use openstack SDK Image types in test\_novaclient * Add FakeSDKImageClient and Image fixtures to test fakes * Imported Translations from Zanata * Move Cinder bad service test fixture into fakes * Replace cinderclient with openstacksdk * Log error on alarm and rgw pollster issues * Improve exception classes for better error handling * Do not return garbage during discover * ruff: Enable S (security/bandit) checks * devstack: Manage [telemetry\_services] metric\_backends option * Do not skip tempest tests for requirements update * Drop own check for LOG.warn * Skip Prometheus samples with counter\_volume=None * doc: Updated outdated test documentation * Remove unused testresources * Direct repository url to opendev.org * ruff: Enable G (flake8-logging-format) checks * Fix flavor\_id mapping for event-driven instance resources * Drop keystoneclient completely * Use built-in method to get built-in default log levels * Update FakeResponse class in test\_dynamic\_pollsters * ruff: Enable pyupgrade checks * ruff: Enable LOG checks * Do not install code to build release notes * Drop unnecessary ignores for flake8 * Report minimal metrics for stopped (SHUTOFF) VMs * Add comprehensive test coverage for cinder pollsters * Remove unnecessary patching in pollster tests * Remove FakeKeystoneClient from fakes * Add aetos jobs * devstack: Fix redis install when coordination URL uses redis * Remove deprecated aliases of [publisher] telemetry\_secret * Add ruff for linting and code quality checks * Add cinder\_client module to abstract python-cinderclient usage * Add unit tests for volume discovery classes * Remove deprecated [DEFAULT] http\_timeout * devstack: Use global cache backend * Expose rgw\_client min and max TLS version * Use common devstack helper to clone gnocchi * Use grouping for prometheus pushgateway publishing * Ubuntu: Replace the service to restart from cinder-api to apache2 * Replace tabs with spaces * Add from\_openstacksdk methods to Project and Domain * Ubuntu: Use systemctl to restart services * doc: Replace gnocchi-api service with apache2 and use systemd to restart * Update octavia tests to use Fake SDK Connection * Add FakeCinderClient and real-typed cinder test data to fakes * Fix interface config for neutronclient * Update manila tests to use Fake SDK Connection * Update designate tests to use Fake SDK Connection * Drop redundant variables * Use common utility to compose query string * tox: Use new constraints option * Assert public method * Drop Python 3.10 * Add volume.service.health pollster for cinder services * Drop indirect dependency * Update coverage to include dynamic context * Expand FakeConnection with identity resources and SDK test fixtures * Add Project and Domain classes to keystoneclient abstraction * Use keystone client convenience functions in gnocchi and discovery * Add ceilometer/exceptions * Remove dead plugin\_base.\_get\_keystone mock from six test setUp() methods * Add list\_domains() convenience method to keystone\_client.Client * Add find\_domains() convenience method to keystone\_client.Client * Add list\_projects() convenience method to keystone\_client.Client * Add find\_projects() convenience method to keystone\_client.Client * Add a Client class to ceilometer.keystoneclient * Fix polling tests to use keystoneclient v3 and FakeKeystoneClient * Skip functional tests for hacking plugin update * Add FakeKeystoneClient to fakes module * Add FakeConnectionFixture * Add TLS verification config for RGW Admin API * Remove unused trust\_id parameter in keystoneclient.get\_client() * Remove unneeded mock in test\_manager * Set fetch\_extra\_metadata to False by default * Update project ID in Gnocchi resources * Bump gnocchi version in grenade * [tests] Reduce noisy log output in tests * [tests] Use ceilometer BaseTestCase for all unit tests * Patch keystoneclient.v3.client.Client instead of keystone\_client.get\_client * Remove MAINTAINERS file * [keystone] Add convenience function to use openstacksdk * Update master for stable/2026.1 26.0.0 ------ * [objectstore] Refactor RGW endpoint lookup to consolidate logic * Replace manual call counters with mock.call\_count in test\_novaclient * Revert "Remove CentOS Stream 9 jobs" * [pre-commit] Configure autopep8 for pre-commit * Fix blake2b call when fips enabled * Update gitignore to ignore md files for AI agents * Imported Translations from Zanata * Imported Translations from Zanata * Remove unused sphinxcontrib-httpdomain * Imported Translations from Zanata * [test\_discovery] Split test\_get\_server into separate testcases * Drop unused sphinxcontrib-blockdiag * Remove leftover of removed environment variables * [pre-commit] Configure codespell for pre-commit * Add get\_url helper function to keystoneclient * Use API filtering to ignore disabled projects * Replace neutronclient with openstacksdk * Replace glanceclient with openstacksdk * Run ceilometer-upgrade only in the node with gnocchi * Add Manila share polling metrics via openstacksdk * Declare Python 3.13 support * Add Designate DNS zone metrics polling support * Add Octavia load balancer metrics polling via openstacksdk * Allow disabling filtering the Gnocchi project * Use stable hashing for cache in Gnocchi publisher * Imported Translations from Zanata * Do not drop firewalls with unknown states * Un-deprecate Neutron FWaaS support * Remove unused method to get all ports * Fix non-deterministic test\_export\_fails\_if\_incomplete\_tls test * Add unit tests for ceilometer.keystone\_client * Add project badge * Replace request-aws by awscurl * Fix polling manager exceptions * reno: Update master for unmaintained/2024.1 * Use wsgi module to run gnocchi api * Change size units from MB/GB to MiB/GiB * Skip functional tests for pre-commit-config.yaml update * Fix tox.ini styling * Remove libvirt dependency from test\_discovery * Bump pyupgrade target to 3.10+ * pre-commit: Bump versions * devstack: Use global uwsgi * Install coverage only when needed * Remove outdated options from local.conf sample * Fix removing stale metrics from Prometheus exporter * Update master for stable/2025.2 * Update outdated default envlist * Make image attributes in Gnocchi optional * Fix missing session options in [service\_credentials] 25.0.0 ------ * Drop explicit description about default values * Fix missing cotyledon options * Exclude .eggs dir from pep8 tests * Remove remaining log translation * Enable a few common hacking checks * Drop outdated description about oslo-incubator * Add memory.available metric * Use 'actual' memory statistic for memory pollster * Handle EPIPE Broken Pipe from Libvirt RPC * Add memory pollster * Revert "Disable power.state meter" * Disable power.state meter * Add vcpus pollster * Refactor rootwrap utilities * Publish image meta from notifications and pollsters (if available) * Get more flavor attributes from libvirt metadata (if available) * Run telemetry tempest tests in grenade job * Expose the flavor\_id and flavor\_name in Prometheus metrics * Fix outdated package/service name in RDO * doc: Drop edit of sudoers in CentOS/RHEL * Imported Translations from Zanata * devstack: Use common function to manage keystoneauth options * Threads to process pollsters * Deprecate pollster builder * Fix a typo in release note * Do not use eval for user input * Fix incomplete mock of libvirtError * Ensure libvirt python binding is installed for tests * Refactor volume pollsters * Fix volume provider pool capacity metrics * Add bashate * Catch exceptions for interfaceStats in libvirt inspector * Remove Python 3.9 support * devstack: Remove unused gnocchi-related variables * devstack: Drop definition of ceilometermiddleware directory * Replace deprecated datetime.datetime.utcnow * Deprecate unused [DEFAULT] http\_timeout * Drop description about ZeroMQ * Remove support for Intel Node Manager * Remove old code for suse in DevStack plugin * Poll for evaluation metrics from aodh * Remove unused test method * Remove CentOS Stream 9 jobs * Remove installation guide for openSUSE/SLES * Run pyupgrade to clean up Python 2 syntaxes * Use pre-commit to run pep8 checks * add pyproject.toml to support pip 23.1 * Enable prom-exporter with tls * Drop remaining lower-constraints target * Drop redundant injection of VIRTUAL\_ENV * Fix missing requests-aws * Add pool capacity pollsters * Make the instance 'host' attribute optional * Update master for stable/2025.1 * Skip functional tests for .gitreview 24.0.0 ------ * Rename [polling] tenant\_name\_discovery * Remove remaining logic for gnocchi devstack plugin * Drop implicit test dependency on iso8601 * Add a map event trait plugin * Revert "Fix vm\_instance label from exporter Prometheus metrics" * Do not drop floating IPs/VPNs with unknown status * Add the disk.ephemeral.size and disk.root.size pollsters * Imported Translations from Zanata * Add storage\_policy attribute for Swift containers * Add the volume\_type\_id attribute for volumes * Drop unused pyOpenSSL * Drop unused gabbi * Dynamic pollsters: Append relative url\_path to URLs * Update default envlist * Remove metrics dependent on Intel CMT perf events * Remove stale metrics from prometheus exporter * Replace oslo\_utils.encodeutils.exception\_to\_unicode * Use constants to validate sample type * Fix gnocchi filter project for non-default domain * Add maximum fan rpm to hardware.ipmi.fan metrics * Remove cpu\_util meter * nova: Remove unused instance\_get\_all * Imported Translations from Zanata * Drop unnecessary OS\_CLOUD override * Remove unused test utilities * Remove VMWare vSphere support * Imported Translations from Zanata * Fix vm\_instance label from exporter Prometheus metrics * Introduce power.state metric * Fix outdated supported database backends * Remove OpenContrail support * Remove remaining reference to OpenDaylight * Drop deprecated cinderv2 option * devstack: Drop unused local variable * reno: Update master for unmaintained/2023.1 * Make all metrics be gauge type * Make grenade job voting * grenade: Use systemd unit name to check service status * grenade: Fix detection of ceilometer-upgrade * Use default service group to launch ceilometer * Replace deprecated configure\_auth\_token\_middleware * Add parameter for disabled projects * Pass interface to keystone client * Fix the doc build * Remove 'x' bit from non-executable files * Remove Python 3.8 support * Prometheus: Refactor metric handling * Ensure supported metric type is given * Peometheus: Refactor label generation logic * Drop unused eventlet from test requirements * Revert "Disable GLOBAL\_VENV" * Fix Prom exporter resource\_name parsing * Add server\_group label to prometheus exporter * Skip functional jobs for locale files * Support file watcher to trigger GMR report * Adjust explanation of enable\_notifications * Get rid of pkg\_resources * Get rid of distutils * Replace deprecated constant\_time\_compare * Handle IPv6 address for prometheus\_listen\_addresses * Ceilometer to export Prometheus metrics * Imported Translations from Zanata * Add [DEFAULT] polling\_namespace to generated ceilometer.conf * Fix a missing space in parameter description * Drop unused import * Remove leftover for python 3.7 support * Update master for stable/2024.2 * Fix package name of gnocchiclient * Imported Translations from Zanata 23.0.0 ------ * Remove sahara support * Imported Translations from Zanata * Add heart beat report for polling agents * Enable instance metadata polling * Imported Translations from Zanata * Replace use of testtools.testcase.TestSkipped * Remove Windows OS support * Add more metadata to volume.size and volume.backup.size pollster * Fix exception when updating non-existing gnocchi resources * Remove metrics specific to OpenDaylight * Remove old excludes * reno: Update master for unmaintained/zed * Remove OpenDaylight support * Stop stack traces on Swift when receiving 403 * Imported Translations from Zanata * Imported Translations from Zanata * reno: Update master for unmaintained/xena * reno: Update master for unmaintained/wallaby * reno: Update master for unmaintained/victoria * Fix dynamic pollsters on compute nodes and coordination process * Update master for stable/2024.1 * Update regex to detect closed branch 22.0.0 ------ * Fix wrong stacklevel in deprecation warnings * Change default pipeline interval comment * Fix releasenotes build of yoga moved to unmaintained * Remove unused cache\_key\_mangler * Bump hacking * Document choices of instance\_discovery\_method by oslo.config interface * devstack: Use spaces instead of tabs * doc: Remove remaining reference to Xen hypervisor * Remove uml virt type support * libvirt: Fix unsupported 'parallels' virt type * Stop using deprecated zake coordination driver * Update python classifier in setup.cfg * Create new meter to poll power usage * Drop authtoken middleware options from ceilometer config file * Update supported python versions * doc: Drop DJANGO\_SETTINGS\_MODULE environment * Add opentelemetry publisher base on http * doc: Fix wrong unit of "current" type ipmi sensor * Deprecate OpenContrail support * Deprecate OpenDaylight support * Add 'user\_id' label in prometheus publisher * Add logs for the \`tenant\_name\_discovery=True\` flow * Add logs in the SampleEndpoint class * Fix \`TelemetryIpc\` when using \`tenant\_name\_discovery=False\` * Fix devstack plugin for CEILOMETER\_BACKEND=none * Remove monasca * Add sg-core backend support in devstack plugin * Fix python shebang * Fallback to oslo\_cache.dict caching backend * Fix ConnectionRefused in TCP publisher * notification: Dump loaded options during start up * Update master for stable/2023.2 21.0.0 ------ * Deprecate support for VMWare vSphere * Deprecate Windows OS support * Add file publisher entrypoint to publish events * Refactor TCP provider * Disable GLOBAL\_VENV * Imported Translations from Zanata * Make centos9s jobs non-voting * Include 'hardware.ipmi.fan' to resource type ipmi\_sensor * Imported Translations from Zanata * Imported Translations from Zanata * Make multiple attempts to obtain gnocchiclient * jsonpath-rw-ext uses operator.add for "+" and operator.sub for "-". operator.add can do string concatenation, but operator.sub cannot do string subtraction. so the original hack does not work, perhaps we should use sub to get an empty string * Add logs to help identify when a polling task finishes * Imported Translations from Zanata * Add 'check\_requirements' job to CI * Make TCP publisher log warning instead of failing * Add vanity names to notification samples * [coordination] backend\_url should be secret * Imported Translations from Zanata * Update master for stable/2023.1 * Add vanity names to telemetry polling notifications 20.0.0 ------ * Enable volume metrics by default * Remove SNMP metrics from default polling * Imported Translations from Zanata * Imported Translations from Zanata * Fix gnocchi install from git * Imported Translations from Zanata * Add TCP publisher * Make tox.ini tox 4.0 compatible * Imported Translations from Zanata * NoUniqueMatch: ClientException on Gnocchi publisher * Fix barbican credentials retrieval * Tests the sample name * Fix OutgoingBytesDeltaPollster sample name * Change oslo\_cache implementation * Improve logging for Gnocchi publisher * Imported Translations from Zanata * Post individual JSONs instead of the whole list * Update the installation guide for RHEL8/CentOS8 * Imported Translations from Zanata * Fix the handle of plain text outputs * Remove lingering queue declaration * Properly handle 'resource\_id' as None for Gnocchi publisher * Add support to namespaces on dynamic pollsters * Imported Translations from Zanata * Add extra metadata fields skip * Add support to host command dynamic pollster definitions * Add user/project names to polled samples * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed 19.0.0 ------ * Imported Translations from Zanata * Fix Non-OpenStack dynamic pollster credentials handling * Exclude metrics from "service" project * Imported Translations from Zanata * Remove unnecessary logic for missing oslo.cache * Add response handlers to support different response types * [zuul] Make telemetry-dsvm-integration-centos-9s-fips vote again * Imported Translations from Zanata * Remove unicode prefixes * Replace abc.abstractproperty with property and abc.abstractmethod * Add debug to tox environment * Remove GenericHardwareDeclarativePollster * Replace deprecated iso8601\_from\_timestamp * Imported Translations from Zanata * Remove [coordination] check\_watchers * Rename a release note file * zuul: Declare queue at top level * Update python testing as per zed cycle testing runtime * Remove support for neutron-lbaas * Replace CentOS Stream 8 by 9 * Fix swift pollsters that use get\_account * Imported Translations from Zanata * Drop lower-constraints.txt and its testing * Debug log libvirt metadata version fails * Add Python3 zed unit tests * Update master for stable/yoga * Remove reference to the tenant attribute of RequestContext 18.0.0 ------ * OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's data * Support two nova metadata versions in instance XML * Update python testing classifier * Remove the wrong release note file * Add missing oslo.cache parameters * Deprecate support for Neutron FWaaS * Fix ignored [notification] batch\_size * Deprecate support for Neutron LBaaS * Update requirements and lower\_constraints * Fix broken test\_discovery\_with\_libvirt\_error * Update compute.discovery to get nova domain meta * Deprecate GenericHardwareDeclarativePollster * Imported Translations from Zanata * Add the describe of uWSGI to run Gnocchi API * Imported Translations from Zanata * Add Python3 yoga unit tests * Update master for stable/xena 17.0.0 ------ * Add missing [oslo\_reports] options * Fix CA file for Swift pollster * Adding FIPS job * Do not install libvirt python bindings from pip * Moving Centos8s job to telemetry-tempest-plugin * Do not install libvirt-python on RHEL via pip * Replace deprecated oslo\_utils.timeutils.isotime * Moving IRC network reference to OFTC * Gnocchi: replace deprecated [storage] coordination\_url * Replace oslo\_utils.fnmatch with fnmatch * Notify and update snapshot metrics * Remove references to Ocata and Newton * Move bug tracking back to launchpad * Move IRC channel to OFTC * Changed minversion in tox to 3.18.0 * remove requires on monotonic * Deprecate unused [coordination] check\_watchers * Remove dependency to panko * vmware tests: Support different moref backend representations * Introduce \`timeout\` in dynamic pollsters sub-system * Remove Xen support * Ceilometer compute \`retry\_on\_disconnect\` using \`no-wait\` * Use tox constraints instead of upper constraints * setup.cfg: Replace dashes with underscores * Imported Translations from Zanata * Add Python3 xena unit tests * Update master for stable/wallaby * Cap tenacity < 7.0.0 * Imported Translations from Zanata 16.0.0 ------ * Deprecate support for Xen * Fix gnocchi create resource error when missing flavor * Using Iterable was deprecated in python 3.3 * Imported Translations from Zanata * Update requirements URLs in tox config * Replace six with python3 code style * Fix invalid argument formatting in the exception message * Imported Translations from Zanata * Fix lower-constraints job * Imported Translations from Zanata * Imported Translations from Zanata * Update bug location * Imported Translations from Zanata * Reuse the docs deps to benefit from constraints * Imported Translations from Zanata * Imported Translations from Zanata * Implement some new meters for vnic delta * Bump hacking min version to 3.0.1 * Imported Translations from Zanata * Revert "[goal] Migrate testing to ubuntu focal" * Drop py37 from tox * Imported Translations from Zanata * add py38 in tox.ini * Dep's should be restricted by upper-constraints * Imported Translations from Zanata * Add AZ resource metadata for volume size polling * Remove six.moves * Add Python3 wallaby unit tests * Update master for stable/victoria * Remove babel.cfg etc * [goal] Migrate testing to ubuntu focal * Remove install unnecessary packages * Only process 'resource\_id' if metric is configured for Gnocchi push 15.0.0 ------ * Fix handling of metadatas that are set to None or False * [goal] Migrate tox based testing to ubuntu focal * Statement about complex object handling in the Dynamic pollster documentation * Add logic for event\_update handling and set transfer event as event\_update * maybe use word is better * Run migration with NotFound exception as well * Run migration with NotFound exception as well * Fix failing unit tests for new msgpack version * Fix volume attachment event parsing * Dynamic pollster support paging systems that return only a path for the next page * Enable processing metadata with nested objects * Add support to the use of headers in the dynamic pollsters * Allow operations on Ceilometer dynamic pollster to reference the sample itself * Remove [ and ] around parameter values in http url * Imported Translations from Zanata * Adding exception handling when inspect\_disks * Remove glance-registry from docs * Fix dynamic pollster \`default\_discovery\` method call * Fix dynamic pollster samples indentation in the documentation * Trigger volume deletes only on volume deleted * Imported Translations from Zanata * Fix PDF build * Added missing Ceilometer configuration step for controller in docs * drop mock from lower-constraints * Refresh tox * Drop undefined doc config path * Stop to use the \_\_future\_\_ module * Port the grenade job to Zuul v3 * Enable OpenStack pollster to configure Ids(project, user, and resource) * Imported Translations from Zanata * Add volume.manage to metrics * Switch to newer openstackdocstheme and reno versions * Fix pygments style * Imported Translations from Zanata * Add py38 package metadata * Add Python3 victoria unit tests * Update master for stable/ussuri 14.0.0 ------ * Imported Translations from Zanata * Imported Translations from Zanata * Imported Translations from Zanata * Cleanup py27 support * Fix docs build error * Imported Translations from Zanata * Temporary failures should be treated as temporary * Update hacking for Python3 * Use unittest.mock instead of third party mock * Add support to linked samples responses * Multi metric dynamic pollsters (handling attribute values with list of objects) * Tell reno to ignore the kilo branch * Fix logging libvirt error on python 3 * Add note for loadbalancer resource type support * Update links to Gnocchi website * Remove mention of meter\_dispatchers * Fix mismatch print format in log message * Imported Translations from Zanata * Imported Translations from Zanata * Include project\_id in Prometheus publisher * Dynamic pollsters: enable operation on attributes * [ussuri][goal] Drop python 2.7 support and testing * Make grenade-dsvm-ceilometer run under python3 * Dynamic pollster system to support non-OpenStack APIs * Imported Translations from Zanata * Fix instantiation of manager.AgentManager on cmd.polling.create\_polling\_service * Add missing ws seperator between words * Add loadbalancer resource type * Add Monasca to supported measurement back ends * Trivial-change: Fix rst syntax * support events * Dynamic pollsters to support nested dictionary values * Dynamic pollsters to handle different response structures * Create dynamic pollster feature * publisher: Contribute the Monasca publisher * Imported Translations from Zanata * Fix samples with dots in sample name * Update master for stable/train 13.0.0.0rc1 ----------- * PDF documentation build * Set proper trait type * Type field missing in snmp data harware.cpu meters * Run 'telemetry-dsvm-integration-ipv6-only' job in gate * Only install monotonic on python2 * Fixing broken links * Get output for processutils.execute impi command * Unit field missing in snmp data * Windows: avoid passing conf objects to subprocesses * Cleanup option meter\_definitions\_cfg\_file * Cap sphinx for py2 to match global requirements * Add framework for ceilometer-status upgrade check * Imported Translations from Zanata * Update reno for stable/rocky * Change a URL to point to more accurate targets * Imported Translations from Zanata * Update Python 3 test runtimes for Train * metering data lost randomly * Fix error print format * Update the job's irrelevant files * Switch to oslo privsep * Add lower-constraints job * Update the .gitignore file * disk capacity is less than disk usage * Fix the wrong ipmitool command location file * Fix install\_command in tox.ini * Remove \_ceilometer\_check\_for\_storage check * Replace git.openstack.org URLs with opendev.org URLs * OpenDev Migration Patch * tempest: Allow to configure tempest config * Dropping the py35 testing * Remove the unused function \_ceilometer\_config\_apache\_wsgi * remove redundant line * Reduce the alarm\_threshold value to make autoscaling test stable * Remove telemetry-tox-py37 * making inspector object singleton again by assigning to the base pollster class * Handle volume.retype event from cinder * Replace openstack.org git:// URLs with https:// * Update master for stable/stein * Imported Translations from Zanata * Add availability\_zone attribute to gnocchi instance resources * Set instance\_type\_id in event traits to be a string * add python 3.7 unit test job 12.0.0 ------ * Add interfaceid and bridges parameters * [Trivial fix] Add missing ws seperator between words * Added metric backup.size to remove warning in ceilometer-agent-notification log * Only print polling.yaml file contents as DEBUG * Replace tripleo-scenario002-multinode with scenario002-standalone * modify the mail list address * Fix the misspelling of "configuration" * Use kafka extras for oslo.messaging * Replace tripleo-scenario001-multinode with scenario001-standalone * Pass gnocchi endpoint into adapter endpoint\_override param * Update mailinglist from dev to discuss * Added support for python3 * Update min tox version to 2.0 * Fixes KeyError on volume create/delete * Added snapshot delete event * Add release note link in README * Fix the section of notify\_on\_state\_change to [notifications] * Replace deprecated snmp metrics * Skip oslo.messaging 9.0.0 release * Don't quote {posargs} in tox.ini * Volume discovery minimum 3.43 api version * Imported Translations from Zanata * opts: remove unused import * gnocchi: Don't fail if archive policy exists * Deprecate event subsystem * event: remove deprecated method * gnocchi: handle domain for filtering swift * Fix logic when selecting configuration file * agent: Remove unused parameter in load\_config * agent: remove unused config change detection code * agent: use any to filter source objects * Imported Translations from Zanata * Imported Translations from Zanata * Fix the default filter\_project name * Use openstack-tox-cover template * pipeline: remove confusing log message * notification: remove unused constant * notification: declare logging method as static * notification: do not store temporary transport * notification: remove unused pipeline listener * pipeline: simplfy classes * compute: remove deprecated disk meters * pipeline: remove transformer support * Follow the new PTI for document build * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * notification: remove workload partitioning * gnocchi: add ceilometer-high{,-rate} archive policies * gnocchi: use ceilometer-low as default archive policy * polling: remove deprecated option batch\_polled\_samples * notifications: remove deprecated meter\_definitions\_cfg\_file option * publisher: mark messaging topic options deprecated * Fix ceilometer polling process not able to coordinate issue * Imported Translations from Zanata 11.0.0 ------ * Migrate to stestr for running tests * Add option to file publisher to write json * Remove unused tox target * Add gating on py37 * udp: remove wrong and useless tests * Add support for Python 3.7 * publisher: allow to not inherit from ConfigPublisherBase * Remove deprecated gnocchi\_dispatcher option group * gnocchi: don't create metrics with resource * polling: remove useless base class * Imported Translations from Zanata * Remove dead link in doc file * objectstore/rgw: Add config option to support RGW implicit tenants * Fix broken link to ceph radosgw usage logging docs * fix tox python3 overrides * Wrong block format in rst file * del unused module * Ability to define batch size off polled samples * Adjust the controller installation position * fix typos * inspector: memory: use usable of memoryStats if available * snmp: ignore rfc1905.NoSuchInstance result * snmp: make oid value retrieval more solid * Replace Chinese punctuation with English punctuation * Remove restiction on allowable namespaces in polling * Imported Translations from Zanata * Imported Translations from Zanata * Deprecating transformers and pipeline partitioning * Unlist non existent storage.api.request * Stop gnocchi during unstack * publisher: add a Prometheus Pushgateway publisher * Imported Translations from Zanata * Remove liusheng and llu from the maintainers * supplement "zakar" and "https" description * fix errors about words spelling * Fix doc title format error * Modify the empty list ensure method * Imported Translations from Zanata * Imported Translations from Zanata * Imported Translations from Zanata * fix a typo in documentation * Imported Translations from Zanata * Imported Translations from Zanata * Imported Translations from Zanata * Don't use gnocchiclient during publisher init * gnocchi: configure archive policies on Ceilo side * cinder: link volume to image and instance * Add new attributes to instance * Imported Translations from Zanata * Imported Translations from Zanata * hyper-v: Converts all os-win exceptions * remove integration playbooks * fix meter formating * the previous patch was missing a 'continue' * Gracefully handle missing metadata in libvirt xml * add disk.device.\*.latency meters * remove ceilometerclient references * avoid verbose tracebacks on known errors * update ipmi docs * add ipmi sensor data to gnocchi * Fix glossary.rst * Remove deprecated rgw meters * Use msgpack instead of msgpack-python * Update reno for stable/queens 10.0.0 ------ * Imported Translations from Zanata * add hardware.disk.read|write.\* stats * add volume.provider.\* meters to docs * add note explaining gnocchi coordination\_url * capture cell name * Deprecate aggregated disk.\* metrics on instance * ignore compute.instance.update for metrics * utils: move hash\_of\_set where it's solely used * utils: move kill\_listeners to ceilometer.notification * Imported Translations from Zanata * set cache with attributes only * Cached should not be used when creating gnocchi resources * Zuul: Remove project name * Remove run-tests.sh * utils: move publisher-only utils functions in publisher * remove repeated host * Imported Translations from Zanata * Replace curly quotes with straight quotes * Delete not applicable definition * add volume provider resource types to gnocchi * support cinder capacity statistics * Remove use of unsupported TEMPEST\_SERVICES variable * Imported Translations from Zanata * Remove state\_description for trove * polling: iter randomly over sources and pollsters when polling * polling: simplify manager method * Remove shuffle\_time\_before\_polling\_task option * utils: remove unused decimal/dt conversion functions * devstack: fix gnocchi database setup * gate: move tripleo job to experimental * Remove bundled intree ceileometer tempest plugin * tempest: reuse zuul conf of the plugin * tempest: use new plugin * Remove the redundant conf.py file * ignore compute.instance.create.start for metrics * tempest: remove ceilometer-api alarm * Remove extra space between method parameters * change doc ceilometer bug tracker url * remove gnocchi ignore option * simplify gnocchi batch push setup * compute sample as dictionary once * fix gnocchi stats logging * use hashmap to quickly find matching resource def * cleanup measurements page * Add README.rst record more project message * Imported Translations from Zanata * Check required Gnocchi version is installed * Replace ujson with json * Fix maximum recursion depth exceeded bug when property referencing itself * Run all telemetry tests in integration jobs * update best practices * cleanup data-pipelines admin guide * cleanup data collection admin guide * cleanup admin-guide architecture * reorder admin and contributor topics * remove configuration from contributor guide * remove install section from contributor guide * partial clean up of contributor install info * cleanup contributor plugins details * Do not check iterable objects before for loop * Do not check keystone domains length in TenantDiscovery * Add doc8 to pep8 check for ceilometer project * Imported Translations from Zanata * Remove filter\_service\_activity option in doc * Minor update comment of devstack settings * Add user\_id for sample volume.snapshot.size * Remove useless trailing newlines * Add missing name traits * devstack: redis on opensuse needs to have default config * ensure pipeline\_listener set up * remove sample sorting * Update http publisher options in doc * clean up non-install parts of contributor docs * Update NotifierPublisher class doc * Fix incorrect yaml code block in pipeline definition * [doc] frequency of polling should be controlled via the polling configuration * remove kafka publisher * Fix typo in utils.py * split partitioning polling tests * drop base polling test separation * static resources not picked up * Move delayed out of utils * doc: remove useless Ceilometer service creation * Move utils.dict\_to\_keyval to opendaylight * minor update to gnocchi contributor docs * ignore api docs * cleanup collector references * Imported Translations from Zanata * remove ceilometerclient * Imported Translations from Zanata * Imported Translations from Zanata * remove unused pollster exception resources * simplify test data generation * remove duplicate polling.yaml setup * remove test\_manager\_exception\_persistency * minor polling cleaning * Allow requeues in when there is a failure in EventsNotificationEndpoint * Add cpu\_l3\_cache to polling yaml * simplify cache generation * Put configurations to appropriate part * polling: run polling tasks immediately on start * Set shuffle\_time\_before\_polling\_task to float and set a minimum * fix ceilometer-compute invoke libvirt exception error * Imported Translations from Zanata * always declare partitioning variables * Remove setting of version/release from releasenotes * treat warnings as errors in release notes build * place release notes inline * zuul: run TripleO jobs with new zuulv3 layout * Remove unused ceilometer.utils.update\_nested * Remove unused ceilometer.utils.EPOCH\_TIME * Remove unused ceilometer.utils.sanitize\_timestamp * utils: remove unused create\_periodic function * queues per manager rather than per pipeline * pluggable notification agent * remove redundant filter check * move pipeline out of \_\_init\_\_ * separate polling code * nearly pluggable notification agent * separate base manager from pipeline * set models as part of pipeline manager class * remove agent setup\_\* helper functions * move sample/event specifc pipeline models to own module * Change oslo.messaging prefetch default * Imported Translations from Zanata * stop double refreshing on start * update install docs * move listener targets to notification agent * rename sample handler * common notification endpoint * refresh agent if group membership changes * Revert "devstack: Set an optimal processing queue" * libvirt: share disk device listing * revise doc of "workload\_partitioning" and term "Notification Agents" * Make Doc links point to the latest branch * Fix bug for ceilometer polling generates an exception * Imported Translations from Zanata * Clarify that there is only one polling agent * Remove direct publisher since it is deprecated * Fix Format in contributor/plugins.rst * set ceilometer\_backend * remove kafka and keystonemiddleware reqs * Zuul: add file extension to playbook path * Clean up direct publisher * Replace jsonutils by ujson * Remove deprecated storage drivers * remove branch specific references * Move binary and notification tests in unit * Remove Ceilometer API * Imported Translations from Zanata * Rename magnum events * fix radosgw meter name * Remove the wrap for skip inspect rbd disk info * tests: fix Redis key length debugging output format * snmp: warn if snmp call timeout * Move oslo.cache to gnocchi flavor * Use generic user for both zuul v2 and v3 * fix gnocchi publisher * zuul: remove ElasticSearch tempest experimental job * Migrate to Zuul v3 * handle new tempest swift interface * Remove unused and unmaintained doc Makefile * Update tests to do not use deprecated test.services() * kill collector * Imported Translations from Zanata * pipeline: remove polling fallback support * event: move models out of storage * Replace the Gnocchi dispatcher by a publisher * Add Hanxi Liu as a Ceilometer maintainer * Fix a typo in the Installation Guide * Use tempest.clients.Manager as base clients class * capture aodh events * Move object storage container\_client to match tempest * agent: log pollster skipping to debug * Cleanup test-requirements * messaging-publisher: fix threadsafe of flush() * devstack: install ceilometer extra * remove unused ExchangeTopics * remove ironic exchange * Replace the usage of some aliases in tempest * Remove deprecated pollster-list option * re-add cpu\_util metric to measurement list * Remove class KafkaBrokerPublisher * Set default ceilometer storage backend to gnocchi * Add dipatcher correlated renos * Remove deprecated nova\_http\_log\_debug option * Remove deprecated compute.workload\_partitioning * Initialize hashrings as AgentManager object * Removes use of timeutils.set\_time\_override * Remove class HttpDispatcher * Add missing command of adding admin role to gnocchi * Remove class FileDispatcher * Fix wrong links in ceilometer * Remove class PublisherBase * Retry to upgrade Gnocchi if connection fails * Use gnocchiclient 4.0 exception types * Update description 'resource\_update\_interval' option * support new gnocchiclient interface * fix disk total\_time metrics * Remove deprecated \*\_control\_exchange * Imported Translations from Zanata * Update reno for stable/pike 9.0.0 ----- * update measurements * vmware:The cpu\_util value should be a decimal * keystone: pass region name to keystone client * Add disk total duration of reads/writes metric * Modify memory swap metric type * Fix a typo in ceilometer * Remove install-guide env which is not effective * fix cpu\_util precision is too precise * ensure timezone is included in timestamps * snmp: Fix discovery when total memory is missing * Fix share.size meter * vmware: ignore -1 values * stop converting to old message format * deprecated pollster-list * use gnocchi devstack script * [Trivialfix]Fix typos in ceilometer * Update and replace http with https for doc links in ceilometer * Deprecate kafka publisher * wrong document location * Update the documentation link for doc migration * Fix neutron lbaas v2 when no listeners are set * add configuration folder * Fix inspect\_vnics error for libvirt * generate conf on doc build * turn on warning-is-error for doc builds * add missing sphinx extension * High precision rate of change timedelta * Specify region when looking for radosgw admin URL * doc: move install guide to main doc dir * remove templates * doc: move old dev docs to contributor section * doc: initial index cleanup * Project\_id for SDN controller meters * Gnocchi support for SDN controller meters * Ceilometer meter support to collect network statistics * Fix some initializations * Docs: update the path of "meters.yaml" and its new feature * Docs: switch to openstackdocstheme * Remove the explanation of nonexistent parameter * Update URL home-page in documents according to document migration * Add memory swap metric * Move "test\_versions.py" to the directory of functional test * Remove interval reference to pipeline * api: remove unused code * tempest: Use primary user token * Update Documentation link in README * Add some unit test cases * Limit the default numbers of metrics polled * Don't check libvirt exception if libvirt is absent * Revert "deprecate archive policy of Gnocchi dispatcher" * Fixed ceilometer arithmetic transformer bug * explicitly note sanitize\_timestamp return condition * Remove old maintainers * Add a description of the parameters to function * Migrate telemetry-events * Migrate telemetry-best-practices * Migrate telemetry-data-retrieval * Migrate telemetry-data-collection * Migrate telemetry-data-pipelines * Migrate telemetry troubleshooting guide * migrate telemetry measurements * Fix typo for additional device support in intel node manger * Add some unit test cases * Add event definition for manila * Migrate telemetry system architecture * Fix UnicodeEncodeError error when log messge in poll\_and\_notify * use tempest manager * Fix some reST field lists in docstrings * expect oslo.messaging messages * Fix can not find VMwareAPISession when import oslo\_vmware * post\_test\_hook: stop sourcing gnocchi/devstack/settings * cap cpu\_util * ensure resources are hashable * Doc: guide for operating resource type * Add share create/delete/expand/shrink meters * fix service path for fedora * modify the description for GnocchiDispatcher * add instruction to set auth\_mode * Update meters definition reference * Added Nova Compute CPU metrics to gnocchi\_resources.yaml * devstack: install Gnocchi using pip * This remove annoying message repetition in logs * Fix html\_last\_updated\_fmt for Python3 * update link away from google docs * Replace assertRaisesRegexp with assertRaisesRegex * Trivial fix typos * change to reference gnocchi.xyz * Change default polling interval * Fix typo in docstring * XenAPI: use os-xenapi for XenAPI driver * Support loading multiple meter definition files * tempest: fix some future flake8 issue * tempest: remove deprecation warning * Deprecate olsotest.mockpatch in favor of native fixtures * Fix a typo * tempest: Allow to configure granularity * integration: always run all commands * Remove upper constraint on sqlalchemy * Adds a Zaqar publisher * tempest: tell tempest our scenario are long * integration: Fix report generation * tests: fix messaging driver * minor doc updates * Replace Ceilometer coordination layer by tooz partition system * Fix publisher doc link * gnocchi/notification: allow to configure the maximum number of requests in parallel * tempest: Don't hardcode the network name * Remove powervm from inspector doc * Cleanup post\_test\_hook.sh * tempest: use the configured flavor * tests: remove oslo.config fixture usage * tempest: rework gabbi setup * Remove log translations * Change the compute polling local cache to be mutex * Optimize the link address * Format the abstraction layer of inspector methods to avoid TypeError * fix install-guide gnocchi+keystone instruction * Correct the configuration of pipeline\_processing\_queues in devstack * compute: remove default duration value * compute disks: use the generic compute pollster * compute vnics: use the generic compute pollster * Add tool for migrating metric data from ceilometer's storage to gnocchi * Use vcpu.x.time and vcpu.x.wait values in libvirt inspector * tests: fix conf object creation * Adds 'disabled' to the possible states for a member in LBaaSv2 * deprecated support of pipeline.yaml for polling * Correct bad use response\_strings in live.yaml * Use HostAddressOpt for opts that accept IP and hostnames * deprecate archive policy of Gnocchi dispatcher * tempest: Fix exception handling * [install-guide] Add more links * tempest: remove confusing stack check step * fix gnocchi unprocessed measures debug * compute: Remove dead code * compute: create one pollster to rule them all * Bump gnocchiclient min version * Remove second tenacity in requirements * tests: stop hammering CPU while waiting for sample to wait * coordination: remove started check * coordination: remove group\_id check * coordination: stop checking for \_coordinator to be None * coordination: create coordinator at init time * coordination: make group\_id to never be None * Add sem-ver flag so pbr generates correct version * tests: simplify broken test * libvirt: rewrite the error handling * Fix the incorrect gnocchi command * fix blacklisting NovaLike resources * tests: remove unused mocked method * remove resource if not created * fix gnocchi\_resources mapping * cleanup unused devstack code * start notification agent after restarting apache * Swallow & report 404s from Swift (missing tenant) * Remove Rohit Jaiswal from maintainers * devstack: Set an optimal processing queue * tempest: remove broken tests * remove ceilometer-collector condition when configuring storage * make gnocchi independent of ceilometer-api * Switch to use stable data\_utils * make gnocchi posting more resilient * remove tooz safety catch * Bugfix: use transport\_url from [oslo\_messaging\_notifications] if present * Bump kafka-python and oslo.messaging * Make sure to get all tenants by scanning each available domain * add jitter to notification agent * Modify variable's usage in Log Messages * agent: only create partition coordinator if backend url provided * agent: start coordinator at run() and never stops * coordination: use tooz builtin heartbeat manager * coordination: use join\_group\_create() * coordination: simplify retry condition by using tenacity.TryAgain * coordination: stop tracking joined groups * coordination: use a conf object with registered options in tests * Remove pipeline periodic refresh feature * Switch to use stable data\_utils * Enable Basic and https certificate authentication for http publisher * tempest: skip legacy telemetry-api tests * Use more specific asserts in tests * Load pipeline config files from /etc/ceilometer/ firstly * Don't run ceilometer-upgrade on unconfigured db * use tooz hashring * Remove unused variable * prepare future tempest breakage * include gnocchi+keystone instructions in install guide * add configuration instructions from admin guide and dev docs * upgrade gnocchi via ceilometer * switch dispatcher references to publisher * remove legacy db stuff * gnocchi: remove archive policy setting for identity * Use bytes for coordination member * Using Panko as publisher than dispatcher if enabled * remove keystone\_authtoken from polling agent * support gnocchi timeout * Remove smoke tag for TelemetryNotificationAPITest * add missing instance meta * Trivial: remove extra spaces * Support extended declaring exchanges * Remove unused override * remove collector instructions * chill out on the number of items in toc * Revert "verify gnocchi connection before processing" * Fix reno title format * doc: update pipeline link * Ship YAML file to /usr/share * Deprecate event\_dispatchers and meter\_dispatchers options * Remove useless metric name * set OS\_AUTH\_TYPE in gate * Support i18n for api app * Correct the use of marker function * match generic cirros name * nova: track flavor name * Trivial-fix: use domain\_id instead of domain\_name * fix [service\_credentials] section location * fix gnocchi url links * drop kwapi pollster * Correct the doc link * remove PaaS event format * cleanup devstack cache initialisation * add note about batching+gnocchi * Switch to use test\_utils.call\_until\_true * gabbi: use history * Deprecate collector * Remove support for py34 * Use https instead of http for git.openstack.org * stop hardcode timeout in tempest tests * Update reno for stable/ocata 8.0.0 ----- * add polling.yaml docs * Do not use non-UUID resource ID in Aodh+Gnocchi tests * Use Tempest stable library interfaces * polling definition file * remove endpoint\_override * gnocchi: do not use removed encode\_resource\_id * update multi-publisher image * make connection pool configurable * make http publisher equivalent to dispatcher * add ceilometer-upgrade step to install guide * update verify to use gnocchi * drop api and storage references from install-guide * Switch to decorators.idempotent\_id * modernise gabbi tests * drop notes re mod\_wsgi * move and trim legacy db docs * show panko events for debug * devstack: make sure it's possible to deploy panko only * set project\_id for cinder polling * install-doc: Disable ceilometer-api service * check panko during integration test * set panko dispatcher if enabled * tripleo: Fix logging of tripleo discovery * agent: always print the extension name on failure * Fix same type of simple message error * Don't load many times the same publisher * pipeline: remove tests helper from runtime code * Trivial: add white space of error message * Add support of refereshing the resource info in local cache * Simplify code of test\_complex\_query * Trivial: remove white space of exception message * switch instance\_discovery to libvirt\_metadata * publisher: fix pipeline confusing reference * remove test\_hbase\_table\_utils * Use parameter skip\_op\_id in compute.virt.vmware.test\_vsphere\_operations * gnocchi: replace / by \_ in resource\_id * gnocchi: don't rely on gnocchi uuid5 * gnocchi: prepare removal of gnocchi uuid5 * gnocchi: move volume\_type to correct section * tempest: use tempest img * fix the gnocchi resource type upgrade * remove residual instance pollster * use domainListGetStats to get cputime * drop instance meter * Fix oslo.vmware change that added new keyword argument * Don't poll nova with compute agent * tempest: Allow to not run deprecated API tests * Remove events storage and API * Update custom install doc * correct volumes name source * add volume.backup.size to gnocchi * upgrade: fix gnocchi resource update * stop assuming ceph/swift share same endpoint * devstack: check for ceilometer+panko service * tempest: allow usage of scenario-img for autoscaling * Fix the gate failure because of several issues * Trival-Fix: replace "nova" with "ceilometer" in api-ref * Fix error module usage * add support to parse user metadata * [doc] Note lack of constraints is a choice * always create ceilometer user * glossary: remove collector and add publisher * Test suite for Aodh's gnocchi-resource-threshold alarm * simplify fail gnochi+ceilometer check * Fix publisher comment * integration: run gabbi first * integration: deleting the stack sometimes timeout * tempest: support keystone v3 for autoscaling * tests: generate reports when tempest fail * Make sure gnocchi is enabled before ceilometer * Added new instance metrics to gnocchi definition * Revert "Add hypervisor inspector sanity check" * gnocchi: Allow to set a different creds section * Allow to configure the triplo network name * Fix oslo.vmware lazy loading * test: remove unused variable * utils: remove unused function stringify\_timestamps * gnocchi: Add volume\_type attribute to volume * devstack: Don't enable api by default * add libxml2 required for lxml * fix http publisher test * filtered out the phynical nics when query vm nics with VMware API * Correct ceilometer reraising of exception * Fix typo in plugin.sh * Add old vm state for compute.instance.update * capture keystone authentication as metric * tools: stop using global conf * gnocchi: use batch create\_metrics=True * Fix the install guide bug * Update Ceilometer architecture docs * Remove useless mock * drop \`counters\` support from pipeline * cleanup unit test location * update architecture docs * Remove api index file * Bump minimal version of cotyledon * Make rabbitmq configuration much simpler * Reenable the tempest test about nova notification * use hyphen-less option name sql\_expire\_sample\_only * Translate info-level log messages for LOG.error * drop disable\_non\_metric\_meters option * cleanup gnocchiclient * Change gnocchi\_ext.NotFound to gnocchi\_ext.ResourceTypeNotFound * integration tests: fix generation of tests\_results.html * Use one log level * Add volume and floatingip event\_delete for gnocchi event\_dispatcher * more gnocchi mapping cleanup * cleanup gnocchi mapping * cleanup manual install * fix postgresql functional gate * Add a release note for HTTPProxyToWSGI middleware * Modify unit of disk requests rate * Handling KeyError error, make gnocchi event dispatcher work * Corrected debug msg in CPUL3CachePollster * doc cleanup * use aodhclient in integration test * Print ceilometer configuration on startup * register ceilometer options at runtime * utils: stop using global conf * intel\_node\_manager: stop using global conf * Remove useless singleton check * register oslo\_db options at runtime * register keystoneauth options at runtime * storage: stop using global conf * cmd.storage: stop using global conf * test\_storage\_scenarios: stop using global conf * raise InstanceShutOffException if vm power off when get\_vm\_moid use map of vm manage obj to reduce vmware api calls * Remove duplicated code * Remove useless code * drop image pollster * gnocchi: remove useless keepalive adapter * comment: remove unused comment * XenAPI: Not support two key network meters * Replace retrying with tenacity * Clean the glance v1 code in tempest plugin * Convert file publisher to be equivalent to file dispatcher * Add aliases for direct publisher * doc: fix the wsgi configuration file name * deprecate ceilometer api * make blacklist log more specific * extract 'instance\_id' as 'resource\_id' in event definitions * disable signing in direct publisher * Add pyOpenSSL to test-requirements * Trivial fix: fix a wrong config option type usage * Add support of metering volume related resources * coordination: remove unused kwarg * Fix bug for ceilometer polling generates an error * fix perf when libvirt is >=2.0.0 and <2.3.0 * Remove ceilometer tool show\_data.py since it is not usable * Bump hacking to 0.12 * Replace oslo\_utils.timeutils.isotime * inspectors: stop using global conf * meter.notifications: stop using global conf * collector: stop using global conf * pollsters: stop using global conf * Replace SimpleProducer with KafkaProducer * document: remove configuration README description * devstack: allow to configure not backend * Modify variable's using method in Log Messages * compute.util: stop using global conf * event.converter: stop using global conf * discovery: stop using global conf * event: stop using global conf * sample: stop using global conf * keystone\_client: stop using global conf * publisher: stop using global conf * Use method constant\_time\_compare from oslo.utils * Add more verbosity for gnocchi\_resources.yaml * Add package "pifpaf" license description * Change redirect status from 307 to 308 * Use set\_defaults method in oslo\_middleware to check CORS\_OPTS * change gnocchi cache enable condition * Correct wrong description of method enforce * Fix gate problem related to \_error\_checker() * Remove deprecated ceilometer-dbsync * gnocchi: don't show backtrace on connection failure * fix gate * Redact password from opendaylight client logging * Broken Link: setuptools entry point * enable caching * Replace 'vmware' with 'vsphere' * gnocchi: stronger configuration file parsing * Don't create useless threads * pipeline: stop using global conf * declarative: stop using global conf * coordination: stop using global conf * Remove buggy usage of global config * pipeline services: stop using global conf * Fix collector single worker to handle UDP package with many worker config * Add http\_proxy\_to\_wsgi to config-generator * Fixed cotyledon version requirement * Remove pecan\_debug option * Fix the issue that missing the app file * neutron\_client: stop using global conf * nova\_client: stop using global conf * messaging: stop using global config * collector: stop using global config * api: Remove global conf * tests: fix broken udp tests * tests: fix tests for functional tests without dsvm * fix perf when libvirt is >=2.0.0 and <2.3.0 * Add http\_proxy\_to\_wsgi to api-paste * make gnocchi event dispatcher work * Enable release notes translation * collector: do not set any dispatcher by default * Remove the unnecessary space * Add autoscaling scenario in tempest tests * Compute agent can poll tx and rx errors and drops * Batching of event & meter dispatching over HTTP * Stop adding ServiceAvailable group option * specific the title * Trivial - Changes rst markup * Alters rst markup to comply with OpenStack rst guidelines * Add prefix "$" for command examples * Use 'code-block' for pieces of code * Docstrings should not start with a space * collector: fix graceful shutdown when udp enabled * Updates rst markup to better align with OpenStack rst guidelines * Modify startup parameters of ceilometer-api in devstack script * Fix the "Gnocchi" link pages of custom.rst doc * Bad Link: stevedore * Clean the deprecated non-metric related code * Clean imports in code * Using assertIsNone() instead of assertIs(None) * Using assertIsNone() instead of assertIs(None) * Fix typo in a docstring in agent/manager.py * XenAPI: polling meters are always 0 * Fix UnicodeEncodeError in Ceilometer polling * gabbi: set panko\_is\_disabled to avoid relying on Keystone autodiscovery * tox: refactor targets * Fix problem when using wsgi script to start * Remove import objects and use import modules * Refactor Ceilometer resource API * Move oslo.db to hard requirements list * Remove left over from old ceilometer-api binary * gabbi: import pipeline\_cfg\_file option before using it * Update reno for stable/newton 7.0.0.0rc1 ---------- * agentbase: remove flaky test * add note regarding pipeline\_processing\_queues option * Refactor Ceilometer event API * Refactor Ceilometer alarm API * standardize release note page ordering * gnocchi: enable event\_dispatcher in devstack and doc * Use pbr wsgi\_scripts feature to build ceilometer-api * Change fnmatch.match method to fnmatch.fnmatch * tox: Remove useless env * Use deps extra for optional requirements * Don't require gnocchiclient * Initialize correctly collector * update docs to show Telemetry projects * Remove store\_events options * Remove sql-expire-samples-only as a CLI option * Update the compute node service\_credentials parameters * Corrected file mode settings * Add oslo.config cli opt to skip the confirm check * add new meters about some perf events * Set a correct number of threads for polling tasks * improve notification processing * Fix string interpolation in log * correct input params in get\_samples * refactor service to be less pipeline dependent * [api-ref] Correct response code * [api-ref] Remove temporary block in conf.py * XenAPI: correct polling on memory\_usage * gnocchi: Create resource-types on upgrades * Allow to skip metering/event database upgrade * Deprecate Kwapi pollsters * Rename ceilometer-dbsync in ceilometer-upgrade * make reload test more resilient 7.0.0.0b3 --------- * generalise instable API redirect assertion * Nit: Aligning the content * Config logABug feature for Ceilometer api-ref * Fix checking IP version when using IPv6 * remove needless brackets * The debug that network.incoming/outgoing.packets print is not right * Update readme file * [install] Create endpoint in one command * [install] Add a missing stash * Replace urllib.quote() with six.moves.urllib.parse.quote() * correct the meaning of direct publish transport * correct the mistake in install-guide document * Add a publish transport in Ceilometer architecture document * XenAPI: failed to poll cpu\_util * Cleanup imports in code * add url in setup.cfg * conversions: remove no used local variable * Add api-ref/build to .gitignore * Fix a warning when running \`tox -e api-ref\` * Gnocchi dispatcher fails on skipped metric * Get ready for os-api-ref sphinx theme change * [dev-docs] Changed location of a class * Limit Happybase to < 1.0.0 * Fix tempest.conf generation * Configuration of certificate verification for HTTP dispatcher * api: redirect to Panko if enabled * fix the %{} when string formatting * ValueError exception when SNMP returns NoSuchObject * Format error message in http.py * HTTP Dispatcher: Post event data as JSON & improve logging * dispatcher: deprecate event database dispatcher for Panko * dispatcher/database: simplify connection retrieving * dispatcher: split the database dispatcher * Switch to use Glance v2 in image pollsters * Fix the not found link for notification in glossary.rst * Added a link to the architecture guide * Removed redundant phrase to increase readability * Fixed small grammar error in overview.rst * Set the correct for image bytes\_sent event * Change keystoneclient to keystoneauth * tests: rewrite batching test in a less racy way * Remove an unused method of network pollsters' base class * Fixed a small grammar error configuration help doc * add memory bandwidth meter * Do not limit elasticsearch to 2.0 * tests: remove dead code with self.source\_resources * Remove Nadya Privalova from core reviewers * Fix wrong parameter reference in periodic decorator * Add missing %s in print message * test: Fix wrong override value of config option interface * Modify py3.4 to py3.5 in the py35-functional of tox * Put py34 first in the env order of tox * This adds migrated API reference files * consumes error notif. when event are disabled * publisher: make direct publisher generic * Revert "[install] Create endpoint in one command" * Register the gnocchi event dispatcher * Reduce code duplication * Specify host info when using the notifier publisher * Add Python 3.4 and 3.5 classifiers and targets * Retrieval of RBD device information issue * Install configuration files in etc * Added full support of snmp v3 usm model 7.0.0.0b2 --------- * base.Resource not define \_\_ne\_\_() built-in function * Check lbaas version if call is v2 specific * move out oslo.service * Remove unused AdvEnum class definition * fix l3\_cache definition * throw PollsterPermenantError exception for memory usage meter * events: fix operator check in event filter * Raise PollsterPermanentError if inspector is not implemented * Remove unused LOG * Fix get\_gnocchiclient to work with python3.5 * collector: use an intermediate proxy class for event dispatcher * Replace raw\_input with input to make PY3 compatible * pollsters: Remove eventlet timers * Imported Translations from Zanata * Fix the name as it was a typo * devstack: prefix \_drop\_database * start partition coordinator only when there is a group to join * split discover into different namespaces * devstack: disable workload\_partition for compute polling * Add a tool to clean the legacy alarm tables * devstack: do not wait for service to start * Config: no need to set default=None * sqlalchemy: do not run upgrade on fresh install * sqlalchemy: fix JSONEncodedDict implementation type * Add install-guide for ceilometer * gnocchi: use events to end Gnocchi resource * Don't generate hostname of host running build to config file * Fixing a trivial typo * Add 'task\_state' attribute to meter metadata * add support of batch recording metering data for mongodb * Switch to use glanceclient to get image * Add install description for Neutron * add l3 cache usage meter * doc: remove left over of docbookrestapi * tests: do not override auth version to v2 6.1.0 ----- * dispacher/gnocchi: measures sent fix logging * Copy images\_client from tempest + Correct concurrency of gabbi 1.22.0 * Rename gabbits with \_ to have - instead * Correct concurrency of gabbi tests for gabbi 1.22.0 * Use "topics" instead of "topic" in Notifier initialization * Clean deprecated "rpc\_backend" in tests * Use trusts in Heat integration test * Remove configuration README in etc * Imported Translations from Zanata * Copy images\_client from tempest * Add Magnum events to definitions * [dispatcher/gnocchi] add unit for metric * Delete unused last\_dup variable * catch DriverLoadFailure for get\_transport optional * catch DriverLoadFailure for get\_transport optional * Bump to Nova v2.1 * Fix the py34 jenkins job * tempest: import ImagesClient as ImagesClientV2 * fix some typos in our doc, comments and releasenotes * enable swift pollsters poll data for specific region * enable swift pollsters poll data for specific region * Imported Translations from Zanata * tempest\_plugin: drop telemetry decorator * tempest\_plugin: drop telemetry decorator * Updated from global requirements * [Trivial] Update Neutron resource status list * doc: remove database alarm capability * replace deprecated heat command with OSC * Update to hacking 0.11.0 * Verify message's signature for every dispatcher * fix CI failure due to oslo.messaging 5.0.0 * remove log in tools/make\_test\_{event\_}data.py * fix CI failure due to oslo.messaging 5.0.0 * remove record\_metering\_data method from collector * tests: replace overtest by pifpaf * Add log hints for partition coordinator * fix opts.list\_opts for polling options * update help string for messaging\_urls * Drop timestamping in pollsters * Set the time point polling starts as timestamp of samples * Fix notification listeners usage * tox: only install hacking in pep8 target * Remove unused pylintrc * devstack: remove useless policy\_file setting * event: verify signature before recording events for all dispatchers * tests: stop services on tests teardown * Fix oslo\_service stop/start mechanism * remove floating\_ip\_get\_all in nova\_client * [Trivial] Refactor libvirt inspector connection & uri * Fix concurrency issue with snmp pollsters * Drop the executability of http.py * Updated from global requirements * remove deprecated auth type password-ceilometer-legacy * [Trivial] Update Neutron resource status list * [Trivial] Remove CEILOMETER\_API\_LOG\_DIR option for devstack * Update the default log levels * Clean some unused method in ceilometer/keystone\_client.py * remove invalid todo in storage functional test code * return 400 when invalid aggregation function is specified * Replace logging with oslo\_log * remove deprecated option database\_connection * move EventFilter to event storage namespace * remove MultipleResultsFound and NoResultFound exception * Remove useless file * remove todo for OS\_TEST\_PATH * add tempest to test-requirements.txt * Improve the docstring for Swift pollsters * add log decorator for neutron\_client public method * add debtcollector to requirements * Remove direct dependency on babel * Imported Translations from Zanata * Refactor floatingip pollster to use discovery * Fix notification listeners usage * notification: Remove eventlet timers * use static timestamps for api samples * refactor DefinitionException classes * collector: Don't use eventlet thread * fix openstack cli command in doc manual * Add release note link * switch to openstack cli instead of keystone cli * Updated from global requirements * libvirt: fix missing python-libvirt issue * Add status in Ceilometer VPN connection sample * document how to enable ceilometer stable branch in devstack * remove python-ceilometerclient from requirements * Imported Translations from Zanata * Updated from global requirements * Imported Translations from Zanata * Ignore the filter\_service\_activity option if gnocchi project not found * Fix Ceilometer tests config options * Updated from global requirements * Fix doc build if git is absent * Replace tempest-lib by os-testr * Add notes on moving to Gnocchi * delete verbose/redundant/deprecated text * replace fnmatch with oslo.utils.fnmatch * add ceilometer to gnocchi configuration notes * Updated from global requirements * Imported Translations from Zanata * remove complex capabilities for meter, resource and statistics * gnocchi: batch measurements * change keystone to openstack cli * re-org existing manually install notes * messaging: remove RequestContextSerializer * Remove unused context object in vpnaas test * Remove unused object from lbaas\_v2 test * Remove unused context object lbaas test * test: remove unused context object in FWaaS tests * Remove unused context objects in Glance tests * Remove unused context object in test * Remove a useless usage of oslo.context in meters API * Remove the deprecated DB2 driver * Update the Administrator Guide links * mongo: remove unused function * Updated from global requirements * Imported Translations from Zanata * drop magnetodb support * Simplify chained comparison * Enhancing Retry logic to Coordination when joining partitioning grp * publisher: clean out context usage * Disable ceilometer-aipmi by default for devstack * Remove useless context object usage * Imported Translations from Zanata * Imported Translations from Zanata 6.0.0 ----- * collector: never allow to lose data * 'ceilometer-polling' should fail with no valid pollsters * Imported Translations from Zanata * Fix typos in comments and config strings * Updated from global requirements 6.0.0.0rc2 ---------- * abort alarms URLs when Aodh is unavailable * abort alarms URLs when Aodh is unavailable * fix minor typo in test\_generic.py * Imported Translations from Zanata * Add the functional tests for getting events * collector: never allow to lose data * devstack Fix unprocess measure path * Imported Translations from Zanata * devstack: allow ceilometer-api and keystone to run on different hosts * Devstack: install coordination backend for compute agent * remove dns and trove from entry\_points * correct docstring in storage module * Imported Translations from Zanata * Remove gabbi tests that check content-location * Add http publisher * remove dns and trove from entry\_points * Imported Translations from Zanata * Imported Translations from Zanata * Update reno for stable/mitaka * Update .gitreview for stable/mitaka * Remove gabbi tests that check content-location 6.0.0.0rc1 ---------- * Imported Translations from Zanata * remove unused field 'triggers' defined in sample event\_pipeline.yaml * remove SERVICE\_TENANT\_NAME from devstack plugin * clean devstack plugin * add rc1 release notes * Use assertIn and assertNotIn * core status cleanup * tests: remove ceilometer-api bin test cases * gate: add missing sudo * change dns and trove notifications to declarative * Remove en\_GB translations * register the config generator default hook with the right name * Imported Translations from Zanata * Updated from global requirements * tempest: migrate api and scnario tests from tempest * mitaka-3 release notes * Adjust log levels for InstanceShutOffException * Fix event\_type creationg failure due to race condition * Imported Translations from Zanata * Ignoring cpu measurement when instance's state is SHUTOFF * Add validation for polling\_namespaces option * xenapi: support the session when xenserver is slave * Imported Translations from Zanata * gnocchi dispatch: Added new resource type support * remove wrong "#!/usr/bin/env python" header * Fixed corner cases of incorrect use of oslo.config * Updated from global requirements * timedelta plugin for meter definition process * Cast Int64 values to int, float in statistics * Cache getters for the decalarative definitions 6.0.0.0b3 --------- * [sahara] add events definitions regarding new notifications * Moved CORS middleware configuration into oslo-config-generator * Add the meter example file 'lbaas-v2-meter-definitions.yaml' * Change default policy to allow create\_samples * Enable the Load Balancer v2 events * Remove unused pngmath Sphinx extension * Updated from global requirements * Fix a minor missing parameter issue * close services in test * Add an update interval to compute discovery * Docs: Configure meters/events dispatch separately * Fix the typo in the gnocchiclient exception * Updated from global requirements * Add gnocchi dispatcher opts to config * Change the SERVICE\_TENANT\_NAME to SERVICE\_PROJECT\_NAME * Hyper-V: replaces in-tree hyper-v utils usage with os\_win * Initial seed of hacking * Add /usr/local/{sbin,bin} to rootwrap exec\_dirs * Fix SDR file parsing for Intel Node Manager * Gnocchi: fix ResourcesDefinitionException for py3 * Change LOG.warn to LOG.warning * tests: fix unworking debug output * Adds timestamp option to Aggregation transformer * remove default=None for config options * Replace assertEqual(None, \*) with assertIsNone in tests * Trivial: Cleanup unused conf variables * Enable the Load Balancer v2 for the Ceilometer(Part Two) * Remove unused variable * Enable the Load Balancer v2 for the Ceilometer(Part One) * Fix footnote reference to Aodh in docs * Updated from global requirements * Set None explicitly to filter options * KEYSTONE\_CATALOG\_BACKEND is deprecated * Use overtest to setup functional backends * devstack: Fix Keystone v3 configuration typo * Imported Translations from Zanata * Handle malformed resource definitions gracefully * Update the home page * Skip duplicate meter definitions * set higher batching requirement * use retrying to attempt to rejoin group * network: remove deprecated option name * sample: remove deprecated option name * Fix wrong capitalization * rewriting history * Remove unused pytz requirement * devstack: use password with version discovery * fix tempest path * Updated from global requirements * raise coordination error if not registered * do not configure worker specific items in init * integration-gate: fix publicURL retrieval * rolling upgrades * fix locking in ceilometer * enable notification agent partitioning * better support notification coordination * remove useless notification listener helper * Lookup meter definition fields correctly * Enhances get\_meters to return unique meters * Imported Translations from Zanata * Updated from global requirements * Fix ceilometer floatingip pollster * Updated from global requirements * tempest: migrate base class for tests * tempest: add ceilometer tempest plugin * tempest: add telemetry client manager * tempest: migrate conf.py from tempest tree * tempest: copy telemetry client from tempest tree * Fix events rbac 6.0.0.0b2 --------- * Don't store events with Gnocchi * add additional mitaka-2 release notes * Corrects typo "a other" -> "another" * Updated from global requirements * add release notes for mitaka-2 * devstack: add support for Gnocchi backend * notification: Use oslo.messaging batch listener * Cleanup of Translations * Added CORS support to Ceilometer * Don't set keystonemiddleware cache * Set None explicitly to filter options * Add OSprofiler-specific events definitions * collector: Use oslo.messaging batch listener * Updated from global requirements * Changes aggregator transformer to allow retention\_time w/o size * Replace LOG.warn with LOG.warning * Updated from global requirements * wrong accumulative value of "network.services.lb.incoming.bytes" * Trivial: Remove vim header from source files * Trival: Remove unused logging import * Fix the typos in the source code * gnocchi: fix stack resource type * Misspelling in message * Clean pagination related methods of impl\_mongodb * Fix some typos in the snmp.py * remove local hacking check * [MongoDB] add indexes in event collection * Remove unused code in gnocchi dispatcher * remove unnecessary code * recheck cache after acquired gnocchi\_resource\_lock * collector: remove deprecated RPC code * fix case in function name * Catch the EndpointNotFound in keystoneauth1 than in keystoneclient * Log exception if stevedore fails to load module * Updated from global requirements * Revert "Revert "devstack config for dogpile cache"" * add per resource lock * verify gnocchi connection before processing * [refactor] remove redundant import of options * Added unit test cases for pysnmp 4.3 * Add keystoneauth1 in requirements * gnocchi: fix cache hash logic * gnocchi: use gnocchiclient instead of requests * show queue status on integration test * Updated from global requirements * using a consistent uuid as cache namespace * Duplicate information link for writing agent plugins * Use keystoneauth1 instead of manual setup * Do not mock the memcache interface for auth\_token * oslo.messaging option group/name change for notification topics * Correct the host field of instance metadata * fix the bug that gnocchi dispatcher can't process single sample * Replace stackforge with openstack * MAINTAINERS: remove outdated data 6.0.0.0b1 --------- * Remove version from setup.cfg * add initial release notes * fix functional gate * messaging: stop using RequestContextSerializer * Fix ceilometer-test-event.py script * Deduplicate the code about snmp meter loading * Updated from global requirements * Revert "devstack config for dogpile cache" * Revert "Workaround requests/urllib connection leaks" * add cpu.delta to gnocchi resources * simplify collector cache * Consistent publisher\_id for polling agent * build metric list on init * re-implement thread safe fnmatch * clean up integration test urls * tools: fix default resource metadata for instance * don't pass ceilometer options to oslo.db engine facade * Use str(name) instead of name.prettyPrint() * Reduce code duplication * remove config files when run clean.sh * fix some test case wrongly skipped for mysql backend * Add WebTest to test-requirements.txt * tests: remove testscenario usage for storage drivers * Remove eventlet usage * Remove alarming code * Clarify the doc about multiple notification\_topics usage * Reduced source code by extracting duplicated code * devstack config for dogpile cache * Updated from global requirements * Updated from global requirements * Fix an indent nit of enforce\_limit method * Move the content of ReleaseNotes to README.rst * use common cache * A dogpile cache of gnocchi resources * Updated from global requirements * install database when collector is enabled * Updated from global requirements * Updated from global requirements * add reno for release notes management * Updated from global requirements * Support to get hardware's cpu\_util from snmp * add rohit\_ to MAINTAINERS * gnocchi: set the default archive policy to None * Mv gabbi\_pipeline.yaml into test directories * Factorize yaml loading of declarative stuffs * Factorize field definition of declarative code * Wrong result is returned when call events getting API * tox: use pretty\_tox in most places * Updated from global requirements * avoid unnecessary inner join in get\_resources() for SQL backend * Add sql-expire-samples-only to option list * Updated from global requirements * configure Apache only when ceilometer-api is enabled * Imported Translations from Zanata * avoid using isolation level * unquote resource id to support slash in it * specify runtime environment for scripts * Using oslo-config-generator to instead of generate-config-file.sh * Use gnocchiclient for integration script * Enable signature verification for events * Correct the timestamp type when make test samples data * Updated from global requirements * avoid generate temporary table when query samples * Reject posting sample with direct=true if Gnocchi is enabled * make script under tools directory executable * Updated from global requirements * Added the README.rst in devstack folder * fix tools/make\_test\_event\_data.py * fix image\_ref attr in gnocchi resource * support mysql+pymysql in functional test * Updated from global requirements * Fix snmp pollster to not ignore valid meters * Block oslo.messaging 2.6.1 release * reset policy per test * Remove dependency on sphinxcontrib-docbookrestapi * gnocchi: remove possible ending / in URL * api: simplify root controller * api: simplify Pecan config * remove instance:FLAVOR related code and docs * Do collector setup and storage cleanup for all backends * change collector\_workers to [collector]workers * Enable POST samples API when gnocchi enabled * devstack: fix debug info for Gnocchi * Imported Translations from Zanata * Add Liberty release note link * Fix make\_test\_data.sh * Imported Translations from Zanata * Be explicit when copying files to /etc/ceilometer * Deprecate event trait plugin 'split' * Updated from global requirements * Clean some log messages when polling neutron resources * Simplify the validation of required fields of pipeline source * doc: service enablement not necessary when using Devstack plugin * Skip bad meter definitions instead of erroring out * Remove the unused network\_get\_all method * mark logging.info translation accordingly * logging cleanup * Updated from global requirements * Remove last vestiges of devstack from grenade plugin * Add missing ceilometerclient repo location 5.0.0 ----- * Imported Translations from Zanata * Fix for resource polling warnings * SQL: Fix event-list with multiple trait query filters * Fix the bug of "Error spelling of a word" * Imported Translations from Zanata * SQL: Fix event-list with multiple trait query filters * Fix a mistake in a test * Configure collector to only record meter or event * Rename list\_events tests to list\_samples tests * fix elasticsearch script reference * Fix the deprecation note in meter.yaml * Fix the deprecation note in meter.yaml * Remove deprecated archive policy map for Gnocchi * Remove enable\_notification.sh * Parametrize table\_prefix\_separator in hbase * Imported Translations from Zanata * fix typo in storage/impl\_sqlalchemy * devstack: install all configuration files from etc/ * dispatcher: remove deprecated CADF code in HTTP * mongodb: remove deprecated replica\_set support * Ensure the test data sample has correct signature * Open Mitaka development 5.0.0.0rc1 ---------- * gnocchi: Don't raise NotImplementedError * Add missing meter and exchange opts * Imported Translations from Zanata * Add test to cover history rule change * Workaround requests/urllib connection leaks * integration tests: additional debugging infos * Coordinator handles ToozError when joining group * Don't create neutron client at loadtime * Delete its corresponding history data when deleting an alarm * update event filter test to validate multiple trait args * Fix variable typos * Updated from global requirements * Change ignore-errors to ignore\_errors * Fix reconnecting to libvirt * remove batch processing requirement from arithmetic transformer * Cleanup empty dirs from tests * retain existing listeners on refresh * Override dispatcher option for test\_alarm\_redirect\_keystone * [ceilometer] Update links to Cloud Admin Guide * Adds support for dynamic event pipeline * Updated from global requirements * Imported Translations from Zanata * pollster/api now publish to sample queue * tox: generate config file on test run * tox: Allow to pass some OS\_\* variables * Refactor keystone handling in discovery manager * Use make\_sample\_from\_instance for net-pollster * apply limit constraint on storage base interface * gnocchi: add two new resources * Fixed tox -egenconfig Error * Add declarative meters to developer docs * add delta transfomer support * do not recreate main queue listeners on partitioning * Validate required fields in meter definition * deprecate cadf\_only http dispatcher * Fix the heavy time cost of event-list * Update API Doc to deprecate the alarming part * Deprecate config options of the old alarming functionality * update architecture documentation * Add attribute 'state' to meter metadata when source is polling * doc: update devstack usage * Remove useless base class * Split out image non-meters * Make the gabbi tox target work with modern tox * Avoid 500 errors when duplicating limit queries * Correct test\_list\_meters\_meter\_id to work with py3 * Updated from global requirements * Update event\_definitions for Cinder Image Cache * Update install docs * Use b64encode to replace of encodestring * Prevent ceilometer expirer from causing deadlocks * remove duplicate log exception message * Spelling mistake of comment in api/controllers/v2/query.py * Fix typos in gnocchi.py and converter.py * Updated from global requirements * Updated from global requirements * Add a py34-functional tox target * doc: update notification\_driver * polling: remove deprecated agents * Fix string in limit warning * Typo fixing * missed entrypoint for nova\_notifier removal * Imported Translations from Transifex * Fix links in README.rst * integration: Add debugging information * deprecate db2 nosql driver * devstack: add new option to support event-alarm * Sync devstack plugin with devstack:lib/ceilometer * Updated from global requirements * remove old nova\_notifier processing code 5.0.0.0b3 --------- * restrict admin event access * Migrate the old snmp pollsters to new declarative pollster * Support to load pollsters extensions at runtime * Added snmp declarative hardware pollster * Requeuing event with workload\_partitioning on publish failure * Event filtering for non-admin users * integration: fix typo * gnocchi: cleanup instance resource definition * Updated from global requirements * Adding pradk to MAINTAINERS * Adding liusheng to MAINTAINERS * Add index to metadata\_hash column of resource table * Incorrect Links are updated * Removing unused dependency: discover * Use new location of subunit2html * Change tox default targets for quick use * Fixed identity trust event types * gnocchi: quote the resource\_id in url * fix metadata for compute cpu notifications * support custom metadata * Move profiler meters to yaml * Control Events RBAC from policy.json * Events RBAC needs scoped token * make telemetry sample payloads dictionaries * Fix requeue process on event handling error * allow configurable pipeline partitioning * Keep the instance\_type meta from polling and notification consistent * Add user\_id,project\_id traits to audit events * Change json path's to start with $. for consistency * Add validation tests for arithmetic, string and prefix expressions * Fix description for "Inapt spelling of 'MongoDB'" * Create conf directory during devstack install phase * support custom timestamp * Add cpu meters to yaml * Fix description for "Incorrect spelling of a word" * integration: add some new tests * Fix disable\_non\_metric\_meters referencing * Update tests to reflect WSME 0.8 fixes * remove jsonpath-rw requirement * Do not use system config file for test * gnocchi: move to jsonpath\_rw\_ext * Updated from global requirements * Allow to run debug tox job for functional tests * Use jsonpath\_rw\_ext for meter/event definitions * preload jsonpath\_rw parsers * integration test: adjusts timeout * integration test: failfast * Updated from global requirements * Avoid recording whole instance info in log * Fix dependency for doc build * Mark record\_type in PaaS Event Format doc as optional * full multi-meter support * add flexible grouping key * Corrected test\_fallback\_meter\_path test case * Add hypervisor inspector sanity check * handle list payloads in notifications * xenapi: support the session to "unix://local" * Introduce Guru Meditation Reports into Ceilometer * Use start status of coodinator in tooz * Fixed event requeuing/ack on publisher failure * Implement consuming metrics from Magnum * Avoid from storing samples with empty or not numerical volumes * use union all when building trait query * Fixed spelling error, retreive -> retrieve * Use min and max on IntOpt option types * Update install docs with gnocchi dispatcher info * Make it possible to run postgresql functional job * Revert "Remove version from os\_auth\_url in service\_credentials" * Updated from global requirements * Use oslo\_config PortOpt support * integration: chown ceilometer directory properly * add mandatory limit value to complex query list * add test to validate jsonpath * Remove version from os\_auth\_url in service\_credentials * do not translate debug logs * Updated from global requirements * Grenade plugin using devstack plugin for ceilometer * remove alembic requirement * Convert instance, bandwidth and SwiftMiddleware meters * Change and move the workers options to corresponding service section * Drop the downgrade function of migration scripts * start rpc deprecation * support multiple-meter payloads * add poll history to avoid duplicate samples * Add Kilo release note reference * initialise opencontrail client in tests * Make ConnectionRetryTest more reliable * Correct thread handling in TranslationHook * Updated from global requirements * Correctly intialized olso config fixture for TestClientHTTPBasicAuth * Don't start up mongodb for unit test coverage * disable non-metric meter definitions * Cast Int64 values to float * Convert identity, sahara and volume to meters yaml * Enable entry points for new declarative meters * Fix for rgw still throwing errors * group pollsters by interval * Revert "Revert "remove instance: meter"" * api: fix alarm deletion and update * Fixes the kafka publisher * Sync devstack plugin with devstack:lib/ceilometer * integration: use the right user in gate * Imported Translations from Transifex * Initial separating unit and functional tests * Stop using openstack.common from keystoneclient * minimise scope of hmac mocking * Updated from global requirements * gnocchi: retry with a new token on 401 * Fix some gabbi tests * Improve comments in notification.py * mongo: fix last python3 bugs * postgres isolation level produces inconsistent reads * Masks messaging\_urls in logs during debug mode * Corrected unit of snmp based harware disk and memory meters * Provide base method for inspect\_memory\_resident * Fix Python 3 issue in opendaylight client * Fix more tests on Python 3 * Remove the compute inspector choice restriction * [MongoDB] Refactor indexes for meter and resources * tests: add an integration test * Fix WSGI replacement\_start\_response() on Python 3 * gnocchi: reduce the number of patch to gnocchi API * Make the partition coordinator log more readable * Drop out-of-time-sequence rate of change samples 5.0.0.0b2 --------- * [MongoDB] Use a aggregate pipeline in statistics * Instance Cache in Node Discovery Pollster * Instance Caching * Imported Translations from Transifex * fix gnocchi resources yaml * Import the api opt group in gabbi fixture * Add a batch\_polled\_samples configuration item * Remove redundant comma * storage: deprecates mongodb\_replica\_set option * Improves send\_test\_data tools * Replace isotime() with utcnow() and isoformat() * distributed coordinated notifications * Imported Translations from Transifex * Close and dispose test database setup connections * Updated from global requirements * api: Redirect request to aodh if available * api: return 410 if only Gnocchi is enabled * Fix broken IPMI agent * add mandatory limit value to meter list * add mandatory limit value to resource list * add mandatory limit value to event list * Move gnocchi resources definition in yaml file * Send a notification per sample, do not batch * Handles dns.domain.exists event in Ceilometer * Pollsters now send notifications without doing transforms * Imported Translations from Transifex * Switch to the oslo\_utils.fileutils * Updated from global requirements * Use choices for hypervisor\_inspector option * The product name Vsphere should be vSphere * Add necessary executable permission * Store and restore the xtrace option in devstack plugin * gnocchi: Remove useless resources patching * add Trove(DBaaS) events * Set conf.gnocchi\_dispatcher.url explicitly in tests * Declarative meters support * Stop the tests if backend hasn't started * Delay the start of the collector until after apache restart * Clean the re-implemented serializers in Ceilometer * monkey\_patch thread in tests * make notifier default event publisher * Fix gnocchi DispatcherTest tests * Sort metric data before grouping and processing * Namespace functions in devstack plugin * Added valid values of operator to response body * gnocchi: fixes the instance flavor type * gnocchi dispatcher: fix typo in stevedore endpoint * Imported Translations from Transifex * Tolerate alarm actions set to None * Make ceilometer work correctly when hosted with a SCRIPT\_NAME * Implementation of dynamically reloadable pipeline * fix log msg typo in api utils * Updated from global requirements * Add documentation about the usage of api-no-pipline * drop deprecated pipeline * Improve doc strings after changing method for index creation * set default limit to meter/sample queries * collector: fix test raising error * Remove test-requirements-py3.txt * remove unused event query * Create a devstack plugin for ceilometer * Add support for posting samples to notification-agent via API * restore long uuid data type * Revert "Add support for posting samples to notification-agent via API" * Update alarm history only if change in alarm property * test error log - catch dummy error * fix kafka tests from flooding logs * catch warnings from error tests * remove unused notifier * Add support for posting samples to notification-agent via API * Stop dropping deprecated tables while upgrade in mongodb and db2 * Add handler of sample creation notification * Remove the unused get\_targets method of plugin base * Replaces methods deprecated in pymongo3.0 * add oslo.service options * Restricts pipeline to have unique source names * drop use of oslo.db private attribute * Fix oslo.service configuration options building * Add fileutils to openstack-common.conf * disable non-metric meters 5.0.0.0b1 --------- * Remove unnecessary executable permission * Imported Translations from Transifex * Switch to oslo.service * Remove unnecessary wrapping of transformer ExtentionManager * Port test\_complex\_query to Python 3 * Fix expected error message on Python 3 * Fix usage of iterator/list on Python 3 * Replaces ensure\_index for create\_index * pip has its own download cache by default * For sake of future python3 encode FakeMemcache hashes * Make acl\_scenarios tests' keystonemiddleware cache work flexibly * Update version for Liberty * Gnocchi Dispatcher support in Ceilometer 5.0.0a0 ------- * Updated from global requirements * Fix alarm rest notifier logging to include severity * Remove useless execute bit on rst file * Fix unicode/bytes issues in API v2 tests * Fix script name in tox.ini for Elasticsearch * Fix the meter unit types to be consistent * tests: use policy\_file in group oslo\_policy * Fix publisher test\_udp on Python 3 * Fix Ceph object store tests on Python 3 * Port IPMI to Python 3 * Port middleware to Python 3 * [elasticsearch] default trait type to string * Updated from global requirements * Lower down the range for columns which are being used as uuid * Sync with latest oslo-incubator * Fix testing of agent manager with tooz * Remove deprecated Swift middleware * add DNS events * Handle database failures on api startup * Fix more tests on Python 3 * Switch to using pbr's autodoc capability * Remove old oslo.messaging aliases * Remove useless versioninfo and clean ceilometer.conf git exclusion * Register oslo\_log options before using them * Add running functional scripts for defined backend * Remove snapshot.update events as they are not sent * WSME version >=0.7 correctly returns a 405 * TraitText value restricted to max length 255 * Cause gabbi to skip on no storage sooner * Updated from global requirements * Move eventlet using commands into own directory * adjust alarm post ut code to adapt to upstream wsme * Disable rgw pollster when aws module not found * Fixes DiskInfoPollster AttributeError exception * remove useless log message * use oslo.log instead of oslo-incubator code * Port test\_inspector to Python 3 * Fix usage of dictionary methods on Python 3 * Imported Translations from Transifex * Add oslo.vmware to Python 3 test dependencies * Optionally create trust for alarm actions * Remove iso8601 dependency * Enable test\_swift\_middleware on Python 3 * Enable more tests on Python 3 * Skip hbase tests on Python 3 * Clear useless exclude from flake8 ignore in tox * Remove pagination code * Stop importing print\_function * Remove useless release script in tools * Remove useless dependency on posix\_ipc * Remove exceute bit on HTTP dispatcher * Remove oslo.messaging compat from Havana * Fixing event types pattern for Role Noti. handler * Mask database.event\_connection details in logs * Switch from MySQL-python to PyMySQL * Python 3: replace long with int * Python 3: Replace unicode with six.text\_type * Python 3: generalize the usage of the six module * Update Python 3 requirements * Python 3: set \_\_bool\_\_() method on Namespace * Python 3: encode to UTF-8 when needed * Python 3: sort tables by their full name * Python 3: replace sys.maxint with sys.maxsize * Initial commit for functional tests * Update a test to properly anticipate HTTP 405 for RestController * proposal to add Chris Dent to Ceilometer core * rebuild event model only for database writes * cleanup problem events logic in event db storage * fix incorrect docstring for dispatcher * Imported Translations from Transifex * api: record severity change in alarm history * VMware: verify vCenter server certificate * Add hardware memory buffer and cache metrics * Make interval optional in pipeline * Improve ceilometer-api install documentation * empty non-string values are returned as string traits * Trait\_\* models have incorrect type for key * small change to development.rst file * Drop use of 'oslo' namespace package * [unittests] Increase agent module unittests coverage * stop mocking os.path in test\_setup\_events\_default\_config * Remove py33 tox target * made change to mod\_wsgi.rst file * ensure collections created on upgrade * Fix raise error when run "tox -egenconfig" * Updated from global requirements * Fix None TypeError in neutron process notifications 2015.1.0 -------- * Have eventlet monkeypatch the time module * Have eventlet monkeypatch the time module * Add the function of deleting alarm history * Updated from global requirements * Fix valueerror when ceilometer-api start * Override gnocchi\_url configuration in test * Move ceilometer/cli.py to ceilometer/cmd/sample.py * Fix valueerror when ceilometer-api start * remove deprecated partitioned alarm service * use message id to generate hbase unique key * gnocchi: fix typo in the aggregation endpoint * Release Import of Translations from Transifex * Fix Copyright date in docs * Replace 'metrics' with 'meters' in option and doc * use message id to generate hbase unique key * update .gitreview for stable/kilo * gnocchi: fix typo in the aggregation endpoint * broadcast data to relevant queues only * Imported Translations from Transifex * fix combination alarm with operator == 'or' * Updated from global requirements 2015.1.0rc1 ----------- * proposal to add ZhiQiang Fan to Ceilometer core * Open Liberty development * Fix a samples xfail test that now succeeds * Cosmetic changes for system architecture docs * Fix a issue for kafka-publisher and refactor the test code * pymongo 3.0 breaks ci gate * use oslo.messaging dispatch filter * Further mock adjustments to deal with intermittent failure * Adds support for default rule in ceilometer policy.json * Updated from global requirements * limit alarm actions * Use oslo\_vmware instead of deprecated oslo.vmware * Remove 'samples:groupby' from the Capabilities list * Use old name of 'hardware.ipmi.node.temperature' * Revert "remove instance: meter" * Tweak authenticate event definition * Add project and domain ID to event definition for identity CRUD * Fix the event type for trusts * reset croniter to avoid cur time shift * Imported Translations from Transifex * Avoid a error when py27 and py-mysql tests run in sequence * Stop using PYTHONHASHSEED=0 in ceilometer tests * remove instance: meter * Added ipv6 support for udp publisher * Remove the unnecessary dependency to netaddr * Optimize the flow of getting pollster resources * support ability to skip message signing * Avoid conflict with existing gnocchi\_url conf value * Using oslo.db retry decorator for sample create * alarm: Use new gnocchi aggregation API * collector: enable the service to listen on IPv6 * minimise the use of hmac * Typo in pylintrc * Ceilometer retrieve all images by 'all-tenants' * fix incorrect key check in swift notifications * support disabling profiler and http meters * ensure collections created on upgrade * Fix common misspellings * Updated from global requirements * refuse to post sample which is not supported * Enable collector to requeue samples when enabled * drop deprecated novaclient.v1\_1 * exclude precise metaquery in query field 2015.1.0b3 ---------- * Imported Translations from Transifex * remove log message when process notification * Add gabbi tests for resources * Fix typos and format in docstrings in http dispatcher * add ability to dispatch events to http target * doc: fix class name * add ability to publish to multiple topics * make field and value attributes mandatory in API Query * Fix db2 upgrade in multi-thread run issue * Add memory.resident libvirt meter for Ceilometer * Update reference * Check the namespaces duplication for ceilometer-polling * Add gabbi tests to explore the Meter and MetersControllers * Imported Translations from Transifex * mysql doesn't understand intersect * order traits returned within events * add network, kv-store, and http events * Add support for additional identity events * Add a Kafka publisher as a Ceilometer publisher * Fix response POST /v2/meters/(meter\_name) to 201 status * Attempt to set user\_id for identity events * Switch to oslo.policy 0.3.0 * normalise timestamp in query * Add more power and thermal data * Updated from global requirements * Fix formatting error in licence * Added option to allow sample expiration more frequently * add option to store raw notification * use mongodb distinct * remove event\_types ordering assumption * Add gabbi tests to cover the SamplesController * api: fix alarm creation if time\_constraint is null * fix log message format in event.storage.impl\_sqlalchemy * Remove duplications from docco * Tidy up clean-samples.yaml * Fix a few typos in the docs * use default trait type in event list query * fix wrong string format in libvirt inspector * create a developer section and refactor * Do not default pecan\_debug to CONF.debug * Adding Gabbi Tests to Events API * fix config opts in objectstore.rgw * Updated from global requirements * support time to live on event database for sql backend * add an option to disable non-metric meters * add missing objectstore entry points * Initial gabbi testing for alarms * reorganise architecture page * Add ceph object storage meters * Use oslo\_config choices support * fix inline multiple assignment * alarming: add gnocchi alarm rules * Protect agent startup from import errors in plugins * Revert "Add ceph object storage meters" * api: move alarm rules into they directory * compress events notes * Destroy fixture database after each gabbi TestSuite * Fix unittests for supporting py-pgsql env * Adding links API and CLI query examples * correct column types in events * Be explicit about using /tmp for temporary datafiles * Patch for fixing hardware.memory.used metric * Add ceph object storage meters * [PostgreSQL] Fix regexp operator * Add clean\_exit for py-pgsql unit tests * modify events sql schema to reduce empty columns * Remove duplicated resource when pollster polling * check metering\_connection attribute by default * unicode error in event converter * cleanup measurements page * api: add missing combination\_rule field in sample * Fix test case of self-disabled pollster * update event architecture diagram * use configured max\_retries and retry\_interval for database connection * Updated from global requirements * Making utilization the default spelling * Add Disk Meters for ceilometer * correctly leave group when process is stopped * Updated from global requirements * enable oslo namespace check for ceilometer project * Add doc for version list API * Enabling self-disabled pollster * Use werkzeug to run the developement API server * Imported Translations from Transifex * switch to oslo\_serialization * move non-essential libs to test-requirements * Validate default values in config * fix the value of query\_spec.maxSample to advoid to be zero * clean up to use common service code * Add more sql test scenarios * [SQLalchemy] Add regex to complex queries * Fix duplication in sinks names * metering data ttl sql backend breaks resource metadata * Refactor unit test code for disk pollsters * start recording error notifications * Remove no\_resource hack for IPMI pollster * Add local node resource for IPMI pollsters * Use stevedore to load alarm rules api * [MongoDB] Add regex to complex queries * Imported Translations from Transifex * support time to live on event database for MongoDB 2015.1.0b2 ---------- * split api.controllers.v2 * add elasticsearch events db * use debug value for pecan\_debug default * Shuffle agents to send request * Updated from global requirements * Adds disk iops metrics implementation in Hyper-V Inspector * discovery: allow to discover all endpoints * Declarative HTTP testing for the Ceilometer API * add listener to pick up notification from ceilometermiddleware * Drop deprecated namespace for oslo.rootwrap * remove empty module tests.collector * Add disk latency metrics implementation in Hyper-V Inspector * add event listener to collector * add notifier publisher for events * enable event pipeline * Imported Translations from Transifex * deprecate swift middleware * sync oslo and bring in versionutils * Expose alarm severity in Alarm Model * Hyper-V: Adds memory metrics implementation * Remove mox from requirements * Fix IPMI unit test to cover different platforms * adjust import group order in db2 ut code * add event pipeline * remove unexistent module from doc/source/conf.py * Upgrade to hacking 0.10 * Remove the Nova notifier * Remove argparse from requirements * [MongoDB] Improves get\_meter\_statistics method * Fix docs repeating measuring units * [DB2 nosql] Create TIMESTAMP type index for 'timestamp' field * remove pytidylib and netifaces from tox.ini external dependency * Avoid unnecessary API dependency on tooz & ceilometerclient * Correct name of "ipmi" options group * Fix Opencontrail pollster according the API changes * enable tests.storage.test\_impl\_mongodb * Remove lockfile from requirements * Disable eventlet monkey-patching of DNS * Expose vm's metadata to metrics * Adding build folders & sorting gitignore * Disable proxy in unit test case of test\_bin * Add Event and Trait API to document * Refactor ipmi agent manager * Use alarm's evaluation periods in sufficient test * Use oslo\_config instead of deprecated oslo.config * Avoid executing ipmitool in IPMI unit test * Updated from global requirements * Add a direct to database publisher * Fixed MagnetoDB metrics title * Imported Translations from Transifex * Fix incorrect test case name in test\_net.py * Updated from global requirements * notification agent missing CONF option * switch to oslo\_i18n * Use right function to create extension list for agent test * Imported Translations from Transifex * Add an exchange for Zaqar in profiler notification plugin * Remove unused pecan configuration options * Updated from global requirements * Use oslo\_utils instead of deprecated oslo.utils * Match the meter names for network services * stop using private timeutils attribute * Update measurement docs for network services * Catch exception when evaluate single alarm * Return a meaningful value or raise an excpetion for libvirt * Imported Translations from Transifex * make transformers optional in pipeline * Added metering for magnetodb * Add release notes URL for Juno * Fix release notes URL for Icehouse * remove unnecessary str method when log messages * Revert "Remove Sphinx from py33 requirements" * untie pipeline manager from samples * reset listeners on agent refresh * Remove inspect\_instances method from virt * Optimize resource list query * Synchronize Python 3 requirements * Remove unnecessary import\_opt|group * Add test data generator via oslo messaging * Check to skip to poll and publish when no resource * Add oslo.concurrency module to tox --env genconfig * add glance events * add cinder events * Manual update from global requirements * Add cmd.polling.CLI\_OPTS to option list * Ignore ceilometer.conf * Switch to oslo.context library 2015.1.0b1 ---------- * Revert "Skip to poll and publish when no resources found" * Added missing measurements and corrected errors in doc * Remove Sphinx from py33 requirements * Clean up bin directory * Improve tools/make\_test\_data.sh correctness * ensure unique pipeline names * implement notification coordination * Make methods static where possible (except openstack.common) * Fix docs to suit merged compute/central agents concept * Drop anyjson * Move central agent code to the polling agent module * RBAC Support for Ceilometer API Implementation * [SQLalchemy] Add groupby ability resource\_metadata * Improve links in config docs * Make LBaaS total\_connections cumulative * remove useless looping in pipeline * Encompassing one source pollsters with common context * Modify tests to support ordering of wsme types * Make compute discovery pollster-based, not agent-level * Add docs about volume/snapshot measurements * Port to graduated library oslo.i18n * Retry to connect database when DB2 or mongodb is restarted * Updated from global requirements * Standardize timestamp fields of ceilometer API * Workflow documentation is now in infra-manual * Add alarm\_name field to alarm notification * Updated from global requirements * Rely on VM UUID to fetch metrics in libvirt * Imported Translations from Transifex * Initializing a longer resource id in DB2 nosql backend * Sync oslo-incubator code to latest * ensure unique list of consumers created * fix import oslo.concurrency issue * Add some rally scenarios * Do not print snmpd password in logs * Miniscule typo in metering\_connection help string * add http dispatcher * [MongoDB] Add groupby ability on resource\_metadata * [MongoDB] Fix bug with 'bad' chars in metadatas keys * Override retry\_interval in MongoAutoReconnectTest * Exclude tools/lintstack.head.py for pep8 check * Add encoding of rows and qualifiers in impl\_hbase * Database.max\_retries only override on sqlalchemy side * Support to capture network services notifications * Internal error with period overflow * Remove Python 2.6 classifier * Enable pep8 on ./tools directory * Imported Translations from Transifex * Fixes Hyper-V Inspector disk metrics cache issue * fix swift middleware parsing * Fix order of arguments in assertEqual * Updated from global requirements * Adapting pylint runner to the new message format * Validate AdvEnum & return an InvalidInput on error * add sahara and heat events * add keystone events to definitions * Add timeout to all http requests * [MongoDB] Refactor time to live feature * transform samples only when transformers exist * Updated from global requirements * Remove module not really used by Ceilometer * Switch to oslo.concurrency * Skip to poll and publish when no resources found * Change event type for identity trust notifications * Add mysql and postgresql in tox for debug env * Add new notifications types for volumes/snapshots * Add encoding to keys in compute\_signature * Tests for system and network aggregate pollsters * Add bandwidth to measurements * Fix wrong example of capabilities * Correct the mongodb\_replica\_set option's description * Alarms listing based on "timestamp" * Use 'pg\_ctl' utility to start and stop database * Correct alarm timestamp field in unittest code * Refactor kwapi unit test * Remove duplicated config doc * VMware: Enable VMware inspector to support any port * Clean event method difinition in meter storage base * Fix some nits or typos found by chance * Add Sample ReST API path in webapi document * Enable filter alarms by their type * Fix storage.hbase.util.prepare\_key() for 32-bits system * Add event storage for test\_hbase\_table\_utils * Add per device rate metrics for instances * Fix hacking rule H305 imports not grouped correctly * Add \_\_repr\_\_ method for sample.Sample * remove ordereddict requirement * Improve manual.rst file * Imported Translations from Transifex * Fix columns migrating for PostgreSQL * Updated from global requirements * Updated from global requirements * [MongoDB] Fix bug with reconnection to new master node * Updated from global requirements * support request-id * Update coverage job to references correct file * remove reference to model in migration * Use oslo\_debug\_helper and remove our own version * Allow collector service database connection retry * refresh ceilometer architecture documentation * Edits assert methods * Adds memory stats meter to libvirt inspector * Edits assert methods * Edits assert methods * Edits assert methods * Edits assert method * Imported Translations from Transifex * Imported Translations from Transifex * Updated from global requirements * add script to generate test event data * Handle poorly formed individual sensor readings * refactor hbase storage code * Avoid clobbering existing class definition * Hoist duplicated AlarmService initialization to super * Clarify deprecation comment to be accurate * Work toward Python 3.4 support and testing 2014.2 ------ * Fix recording failure for system pollster * sync and clean up oslo * Add missing notification options to the documentation * Add missing alarm options to the documentation * Add oslo.db to config generator * Add missed control exchange options to the documentation * Add coordination related options to the documentation * Add missing collector options to the documentation * switch to oslo-config-generator * Edit docs for docs.opentack.org/developer/ * Add oslo.db to config generator * Fix signature validation failure when using qpid message queue * clean capabilities * move db2 and mongo driver to event tree * move sql event driver to event tree * move hbase event driver to event tree * Sets default encoding for PostgreSQL testing * update database dispatcher to use events db * Add role assignment notifications for identity * add mailmap to avoid dup of authors * Add user\_metadata to network samples * Fix recording failure for system pollster 2014.2.rc2 ---------- * Manually updated translations * Updated from global requirements * Creates one database per sql test * Adds pylint check for critical error in new patches * Fix neutron client to catch 404 exceptions * Fix OrderedDict usage for Python 2.6 * Include a 'node' key and value in ipmi metadata * clean path in swift middleware * Implement redesigned separator in names of columns in HBase * [HBase] Add migration script for new row separate design * Imported Translations from Transifex * Include a 'node' key and value in ipmi metadata * Updated from global requirements * Run unit tests against PostgreSQL * create skeleton files for event storage backends * Imported Translations from Transifex * isolate event storage models * Fix neutron client to catch 404 exceptions * Run unit tests against MySQL * Updated from global requirements * Correct JSON-based query examples in documentation * Open Kilo development * Add cfg.CONF.import\_group for service\_credentials * Fix OrderedDict usage for Python 2.6 * clean path in swift middleware 2014.2.rc1 ---------- * Partition static resources defined in pipeline.yaml * Per-source separation of static resources & discovery * dbsync: Acknowledge 'metering\_connection' option * Fix bug in the documentation * Use oslo.msg retry API in rpc publisher * Describe API versions * Change compute agent recurring logs from INFO to DEBUG * Fix bug with wrong bool opt value interpolation * [HBase] Improves speed of unit tests on real HBase backend * Imported Translations from Transifex * Removed unused abc meta class * update references to auth\_token middleware * clean up swift middleware to avoid unicode errors * [HBase] Catch AlreadyExists error in Connection upgrade * Use None instead of mutables in method params default values * Updated from global requirements * Enable to get service types from configuration file * test db2 driver code * Docs: Add description of pipeline discovery section * Typo "possibilites" should be "possibilities" * Modified docs to update DevStack's config filename * Add an API configuration section to docs * Tune up mod\_wsgi settings in example configuration * Allow pecan debug middleware to be turned off * Provide \_\_repr\_\_ for SampleFilter * Eliminate unnecessary search for test cases * Switch to a custom NotImplementedError * minimise ceilometer memory usage * Partition swift pollster resources by tenant * Add IPMI pollster * Add IPMI support * Stop using intersphinx * Use central agent manager's keystone token in discoveries * Handle invalid JSON filters from the input gracefully * Sync jsonutils for namedtuple\_as\_object fix * ceilometer spamming syslog * Timestamp bounds need not be tight (per ceilometer 1288372) * Allow to pass dict from resource discovery * fix network discovery meters * switch to sqlalchemy core * Imported Translations from Transifex * Improve the timestamp validation of ceilometer API * Update docs with Sahara notifications configuration * Migrate the rest of the central agent pollsters to use discoveries * Add documentation for implemented identity meters * Fix tests with testtools>=0.9.39 * Document the standard for PaaS service notifications * Returns 401 when unauthorized project access occurs * Adding another set of hardware metrics * normalise resource data 2014.2.b3 --------- * warn against sorting requirements * Add validate alarm\_actions schema in alarm API * Fix help strings * Imported Translations from Transifex * Switch partitioned alarm evaluation to a hash-based approach * Central agent work-load partitioning * collector: Allows to requeue a sample * Typo fixed * Switch to oslo.serialization * Document pipeline publishers configuration * Alarm: Use stevedore to load the service class * Enhance compute diskio tests to handle multi instance * Adding comparison operators in query for event traits * XenAPI support: Update measurements documentation * update requirements * add documentation for setting up api pipeline * Permit usage of notifications for metering * XenAPI support: Disk rates * XenAPI support: Changes for networking metrics * XenAPI support: Memory Usage * XenAPI support: Changes for cpu\_util * XenAPI support: List the instances * Rebase hardware pollsters to use new inspector interface * Switch to use oslo.db * Remove oslo middleware * Adding quotas on alarms * Add an exchange for Trove in profiler notification plugin * Simplify chained comparisons * In-code comments should start with \`#\`, not with \`"""\` * Remove redundant parentheses * skip polls if service is not registered * re-add hashseed to avoid gate error * Switch to oslo.utils * Switch to oslotest * Handle sqlalchemy connection strings with drivers * Rewrite list creation as a list literal * Rewrite dictionary creation as a dictionary literal * Triple double-quoted strings should be used for docstrings * Add upgrading alarm storage in dbsync * Improving of configuration.rst * Fix typos in transformer docstrings * Update tox.ini pep8 config to ignore i18n functions * Added new hardware inspector interface * compute: fix wrong test assertion * sync olso-incubator code * VMware: Support secret host\_password option * refactor filter code in sql backend * Support for per disk volume measurements * Use a FakeRequest object to test middleware * Imported Translations from Transifex * Improve api\_paste\_config file searching * [Hbase] Add column for source filter in \_get\_meter\_samples * Issue one SQL statement per execute() call * Allow tests to run outside tox * [HBase] Refactor hbase.utils * Set page size when Glance API request is called * Adding init into tools folder * Enhancing the make\_test\_data script * correct DB2 installation supported features documentation * Avoid duplication of discovery for multi-sink sources * Improve performance of libvirt inspector requests * Documented Stevedore usage and source details * Add notifications for identity authenticate events * Add message translate module in vmware inspector * Handle Cinder attach and detach notifications * [HBase] Improve uniqueness for row in meter table * Doc enhancement for API service deployment with mod\_wsgi * Update documentation for new transformer * Add the arithmetic transformer endpoint to setup.cfg * Imported Translations from Transifex * Fix unit for vpn connection metric * Debug env for tox * Change spelling mistakes * Use auth\_token from keystonemiddleware * Fix dict and set order related issues in tests * Fix listener for update.start notifications * Sahara integration with Ceilometer * Add notifications for identity CRUD events * Extracting make\_resource\_metadata method * Fix make\_test\_data tools script * Add cumulative and gauge to aggregator transformer * Enable some tests against py33 * Remove --tmpdir from mktemp * Replace dict.iteritems() with six.iteritems(dict) * Replace iterator.next() with next(iterator) * Fix aggregator flush method * Automatic discovery of TripleO Overcloud hardware * Set python hash seed to 0 in tox.ini * Don't override the original notification message * Remove ConnectionProxy temporary class * Move sqlalchemy alarms driver code to alarm tree * basestring replaced with six.string\_types * Correct misspelled words 2014.2.b2 --------- * Add retry function for alarm REST notifier * Move hbase alarms driver code to alarm tree * Update measurement docs for FWaaS * Update measurement docs for VPNaaS * Follow up fixes to network services pollsters * Updated from global requirements * Implement consuming ipmi notifications from Ironic * Support for metering FWaaS * Adds Content-Type to alarm REST notifier * Multi meter arithmetic transformer * Remove redudent space in doc string * Use None instead of mutables in test method params defaults * Add support for metering VPNaaS * Use resource discovery for Network Services * Change of get\_events and get\_traits method in MongoDB and Hbase * Fix two out-dated links in doc * Move log alarms driver code to alarm tree * Separate the console scripts * clean up event model * improve expirer performance for sql backend * Move mongodb/db2 alarms driver code to alarm tree * Allow to have different DB for alarm and metering * Replace datetime of time\_constraints by aware object * Sync oslo log module and its dependencies * Use hmac.compare\_digest to compare signature * Add testcase for multiple discovery-driven sources * Fixes aggregator transformer timestamp and user input handling * Improves pipeline transformer documentation * Fix incorrect use of timestamp in test * Add keystone control exchange * Fix call to meter-list in measurements doc * Remove redundant parentheses * [Mongodb] Implement events on Mongodb and DB2 * Fix typos in code comments & docstrings * Make the error message of alarm-not-found clear * Fix SQL exception getting statitics with metaquery * Remove docutils pin * update default\_log\_levels set by ceilometer * Fix annoying typo in partition coordinator test * Transform sample\_cnt type to int * Remove useless sources.json * Fix H405 violations and re-enable gating * Fix H904 violations and re-enable gating * Fix H307 violations and re-enable gating * Fix the section name in CONTRIBUTING.rst * Added osprofiler notifications plugin * Improve a bit performance of Ceilometer * Revert "Align to openstack python package index mirror" * Fix aggregator \_get\_unique\_key method * Remove meter hardware.network.bandwidth.bytes * Fix F402 violations and re-enable gating * Fix E265 violations and re-enable gating * Fix E251 violations and re-enable gating * Fix E128 violations and re-enable gating * Fix E126,H104 violations and re-enable gating * Bump hacking to 0.9.x * Fixed various import issues exposed by unittest * use urlparse from six * clean up sample index * Fix HBase available capabilities list * Updated from global requirements * VMware:Update the ceilometer doc with VMware opts * Handle non-ascii character in meter name * Add log output of "x-openstack-request-id" from nova * Imported Translations from Transifex * fix StringIO errors in unit test * Fix hacking rule 302 and enable it * Imported Translations from Transifex * sync oslo code * Fixes ceilometer-compute service start failure * Reenables the testr per test timeout * Avoid reading real config files in unit test * Clean up oslo.middleware.{audit,notifier} * Use hacking from test-requirements * Splits hbase storage code base * Splits mongo storage code base * Separate alarm storage models from other models * Iterates swift response earlier to get the correct status * Fix messaging.get\_transport caching * Fix method mocked in a test * Don't keep a single global TRANSPORT object * Clean up .gitignore * Fix Sphinx directive name in session.py * Fix list of modules not included in auto-gen docs * Downgrade publisher logging to debug level again 2014.2.b1 --------- * remove default=None for config options * [HBase] get\_resource optimization * Fix incorrect trait initialization * Remove unused logging in tests * Revert "Fix the floatingip pollster" * Remove low-value logging from publication codepath * Fix LBaaS connection meter docs * Fix the meter type for LB Bytes * Adding alarm list filtering by state and meter * Adds caches for image and flavor in compute agent * [HBase] Implement events on HBase * Skipping central agent pollster when keystone not available * Respect $TMPDIR environment variable to run tests * Fixed unit test TestRealNotification * Update Measurement Docs for LBaaS * Metering LoadBalancer as a Service * Removes per test testr timeout * Change pipeline\_manager to instance attribute in hooks * Change using of limit argument in get\_sample * Refactor tests to remove direct access to test DBManagers * Fix notification for NotImplemented record\_events * Add missing explicit cfg option import * Fix ceilometer.alarm.notifier.trust import * Use TYPE\_GAUGE rather than TYPE\_CUMULATIVE * Update doc for sample config file issue * Corrects a flaw in the treatment of swift endpoints * use LOG instead of logger as name for the Logger object * Fix doc gate job false success * Improve performance of api requests with hbase scan * Add new 'storage': {'production\_ready': True} capability * Clean tox.ini * Remove (c) and remove unnecessary encoding lines * Fix testing gate due to new keystoneclient release * Ignore the generated file ceilometer.conf.sample * Update the copyright date in doc * Updated from global requirements * reconnect to mongodb on connection failure * refactor sql backend to improve write speed * Don't rely on oslomsg configuration options * replaced unicode() with six.text\_type() * Synced jsonutils from oslo-incubator * Fix the floatingip pollster * Fix project authorization check * Update testrepository configuration * Implemented metering for Cinder's snapshots * Use joins instead of subqueries for metadata filtering * Use None instead of mutables in method params defaults * Remove all mostly untranslated PO files * switch SplitResult to use six * Remove unused db code due to api v1 drop * Updated from global requirements * oslo.messaging context must be a dict * Drop deprecated api v1 * Fix network notifications of neutron bulk creation * mongo: remove \_id in inserted alarm changes * Clean up openstack-common.conf * Revert "oslo.messaging context must be a dict" * Correct class when stopping partitioned alarm eval svc * oslo.messaging context must be a dict * Corrections of spelling, rephrasing for clarity * Adapt failing tests for latest wsme version * Removed StorageEngine class and it's hierarchy * Correcting formatting and adding period in measurement doc * Initialize dispatcher manager in event endpoint * Replaced CONF object with url in storage engine creation * Synced jsonutils from oslo-incubator * Remove gettextutils.\_ imports where they are not used * Remove "# noqa" leftovers for gettextutils.\_ * transformer: Add aggregator transformer * Remove conversion debug message * Fix the return of statistic with getting no sample * Remove eventlet.sleep(0) in collector tests * Don't allow queries with 'IN' predicate with an empty sequence * Check if samples returned by get\_sample\_data are not None * Opencontrail network statistics driver * Add a alarm notification using trusts * Replace hard coded WSGI application creation * Describe storage backends in the collector installation guide * Made get\_capabilities a classmethod instead of object method * Disable reverse dns lookup * Consume notif. from multiple message bus * Use NotificationPlugin as an oslo.msg endpoint * Improve combination rule validation * Remove ceilometer.conf.sample * Use known protocol scheme in keystone tests * cleanup virt pollster code * Add encoding argument to deserialising udp packets in collector * Made get\_engine method module-private * Make entities (Resource, User, Project) able to store lists * Remove duplicate alarm from alarm\_ids * More accurate meter name and unit for host load averages * Replace oslo.rpc by oslo.messaging * Fix a response header bug in the error middleware * Remove unnecessary escape character in string format * Optimize checks to set image properties in metadata * fix statistics query in postgres * Removed useless code from \_\_init\_\_ method * Refactored fake connection URL classes * Replace assert statements with assert methods * Removes direct access of timeutils.override\_time * Disable specifying alarm itself in combination rule * Include instance state in metadata * Allowed nested resource metadata in POST'd samples * Sync oslo-incubator code * Updated from global requirements * Refactor the DB implementation of Capabilities API * Fix Jenkins translation jobs * Align to openstack python package index mirror * User a more accurate max\_delay for reconnects * Open Juno development 2014.1.rc1 ---------- * Imported Translations from Transifex * Add note on aggregate duplication to API docco * Use ConectionPool instead of one Connection in HBase * remove dump tables from previous migrations * De-dupe selectable aggregate list in statistics API * ensure dispatcher service is configured before rpc * improve performance of resource-list in sql * SSL errors thrown with Postgres on multi workers * Remove escape character in string format * Verify user/project ID for alarm created by non-admin user * enable a single worker by default * Fix ceilometer.conf.sample mismatch * Metadata in compute.instance.exists fix * Fix order of arguments in assertEquals * Documenting hypervisor support for nova meters * Ensure idempotency of cardinality reduction in mongo * VMware vSphere: Improve the accuracy of queried samples * Use swob instead of webob in swift unit tests * Disable oslo.messaging debug logs * Fix validation error for invalid field name in simple query * fix create\_or\_update logic to avoid rollbacks * Avoid swallowing AssertionError in test skipping logic * Fix hardware pollster to inspect multiple resources * spawn multiple workers in services * Install global lazy \_() * Fixes Hyper-V metrics units * Ensure intended indices on project\_id are created for mongo * Fix the type of the disk IO rate measurements * Change the sample\_type from tuple to string * Fix order of arguments in assertEquals * Ensure alarm rule conform to alarm type * insecure flag added to novaclient * Fixes duplicated names in alarm time constraints * Use the list when get information from libvirt * Eventlet monkeypatch must be done before anything * 028 migration script incorrectly skips over section * Fix bug in get\_capabilities behavior in DB drivers * Added documentation for selectable aggregates * Make sure use IPv6 sockets for ceilometer in IPv6 environment * VMware vSphere: Bug fixes * Ensure insecure config option propagated by alarm evaluator * Fix order of arguments in assertEquals * Fix order of arguments in assertEquals * Fix order of arguments in assertEquals * Rationalize get\_resources for mongodb * Ensure insecure config option propagated by alarm service * add host meters to doc * Add field translation to complex query from OldSample to Sample * Extend test case to cover old alarm style conversion * Updated doc with debug instructions * Refactored the way how testscenarios tests are run * Corrected the sample names in hardware pollsters * Prevent alarm\_id in query field of getting history * Make ceilometer work with sqla 0.9.x * Implements monitoring-network-from-opendaylight * Add user-supplied arguments in log\_handler * VMware vSphere support: Disk rates * Fix updating alarm can specify existing alarm name * Changes for networking metrics support for vSphere * VMware vSphere: Changes for cpu\_util * VMware vSphere support: Memory Usage * Fix broken statistics in sqlalchemy * Fixes Hyper-V Inspector network metrics values * Set storage engine for the trait\_type table * Enable monkeypatch for select module * Rename id to alarm\_id of Alarm in SqlAlchemy * Fix some spelling mistakes and a incorrect url * Skip central agent interval\_task when keystone fails 2014.1.b3 --------- * Ensure user metadata mapped for instance notifications * Per pipeline pluggable resource discovery * Wider selection of aggregates for sqlalchemy * Wider selection of aggregates for mongodb * Adds time constraints to alarms * Remove code duplication Part 3 * Decouple source and sink configuration for pipelines * Selectable aggregate support in mongodb * Selectable aggregation functions for statistics * Add simple capabilities API * Removed global state modification by api test * VMware vSphere support: Performance Mgr APIs * Fix typo * move databases to test requirements * Make recording and scanning data more determined * Implements "not" operator for complex query * Implements metadata query for complex query feature * Alarms support in HBase Part 2 * Alarm support in HBase Part 1 * Remove unused variable * Added hardware pollsters for the central agent * Added hardware agent's inspector and snmp implementation * Updated from global requirements * Pluggable resource discovery for agents * Remove code duplication Part 2 * Imported Translations from Transifex * remove audit logging on flush * Tolerate absent recorded\_at on older mongo/db2 samples * api: export recorded\_at in returned samples * Fix the way how metadata is stored in HBase * Set default log level of iso8601 to WARN * Sync latest config file generator from oslo-incubator * Fix typo on testing doc page * Remove code duplication * sample table contains redundant/duplicate data * rename meter table to sample * storage: store recording timestamp * Fixed spelling error in Ceilometer * Adds doc string to query validate functions in V2 API * Updated from global requirements * Remove code that works around a (now-resolved) bug in pecan * Fix missing source field content on /v2/samples API * Refactor timestamp existence validation in V2 API * Use the module units to refer bytes type * sync units.py from oslo to ceilometer * Add comments for \_build\_paginate\_query * Implements monitoring-network * Handle Heat notifications for stack CRUD * Alembic migrations not tested * Modify the discription of combination alarm * check domain state before inspecting nics/disks * Adds gettextutils module in converter * Keep py3.X compatibility for urllib.urlencode * Added missing import * Removed useless prints that pollute tests log * Implements in operator for complex query functionality * Implements field validation for complex query functionality * allow hacking to set dependencies * Implements complex query functionality for alarm history * Implements complex query functionality for alarms * Remove None for dict.get() * Replace assertEqual(None, \*) with assertIsNone in tests * Update notification\_driver * Switch over to oslosphinx * Fix some flaws in ceilometer docstrings * Rename Openstack to OpenStack * Remove start index 0 in range() * Updated from global requirements * Remove blank line in docstring * Use six.moves.urllib.parse instead of urlparse * Propogate cacert and insecure flags to glanceclient * Test case for creating an alarm without auth headers * Refactored run-tests script * Implements complex query functionality for samples * fix column name and alignment * Remove tox locale overrides * Updated from global requirements * Adds flavor\_id in the nova\_notifier * Improve help strings * service: re-enable eventlet just for sockets * Fixes invalid key in Neutron notifications * Replace BoundedInt with WSME's IntegerType * Replace 'Ceilometer' by 'Telemetry' in the generated doc * Doc: Add OldSample to v2.rst * Fixing some simple documentation typos * Updated from global requirements * Fix for a simple typo * Replace 'a alarm' by 'an alarm' * Move ceilometer-send-counter to a console script * sync oslo common code * Handle engine creation inside of Connection object * Adds additional details to alarm notifications * Fix formating of compute-nova measurements table * Fix string-to-boolean casting in queries * nova notifier: disable tests + update sample conf * Update oslo * Refactored session access * Fix the py27 failure because of "ephemeral\_key\_uuid" error * Correct a misuse of RestController in the Event API * Fix docs on what an instance meter represents * Fix measurement docs to correctly represent Existance meters * samples: fix test case status code check * Replace non-ascii symbols in docs * Use swift master * Add table prefix for unit tests with hbase * Add documentation for pipeline configuration * Remove unnecessary code from alarm test * Updated from global requirements * Use stevedore's make\_test\_instance * use common code for migrations * Use explicit http error code for api v2 * Clean .gitignore * Remove unused db engine variable in api * Revert "Ensure we are not exhausting the sqlalchemy pool" * eventlet: stop monkey patching * Update dev docs to include notification-agent * Change meter\_id to meter\_name in generated docs * Correct spelling of logger for dispatcher.file * Fix some typos in architecture doc * Drop foreign key contraints of alarm in sqlalchemy * Re-enable lazy translation * Sync gettextutils from Oslo * Fix wrong doc string for meter type * Fix recursive\_keypairs output * Added abc.ABCMeta metaclass for abstract classes * Removes use of timeutils.set\_time\_override 2014.1.b2 --------- * tests: kill all started processes on exit * Exclude weak datapoints from alarm threshold evaluation * Move enable\_acl and debug config to ceilometer.conf * Fix the Alarm documentation of Web API V2 * StringIO compatibility for python3 * Set the SQL Float precision * Convert alarm timestamp to PrecisionTimestamp * use six.move.xrange replace xrange * Exit expirer earlier if db-ttl is disabled * Added resources support in pollster's interface * Improve consistency of help strings * assertTrue(isinstance) replace by assertIsInstance * Return trait type from Event api * Add new rate-based disk and network pipelines * Name and unit mapping for rate\_of\_change transformer * Update oslo * Remove dependencies on pep8, pyflakes and flake8 * Implement the /v2/samples/ API * Fix to handle null threshold\_rule values * Use DEFAULT section for dispatcher in doc * Insertion in HBase should be fixed * Trivial typo * Update ceilometer.conf.sample * Fix use the fact that empty sequences are false * Remove unused imports * Replace mongo aggregation with plain ol' map-reduce * Remove redundant meter (name,type,unit) tuples from Resource model * Fix work of udp publisher * tests: pass /dev/null as config for mongod * requirements: drop netaddr * tests: allow to skip if no database URL * Fix to tackle instances without an image assigned * Check for pep8 E226 and E24 * Fixed spelling mistake * AlarmChange definition added to doc/source/webapi/v2.rst * 1st & last sample timestamps in Resource representation * Avoid false negatives on message signature comparison * cacert is not picked up correctly by alarm services * Change endpoint\_type parameter * Utilizes assertIsNone and assertIsNotNone * Add missing gettextutils import to ceilometer.storage.base * Remove redundant code in nova\_client.Client * Allow customized reseller\_prefix in Ceilometer middleware for Swift * Fix broken i18n support * Empty files should no longer contain copyright * Add Event API * Ensure we are not exhausting the sqlalchemy pool * Add new meters for swift * Sync config generator workaround from oslo * storage: factorize not implemented methods * Don't assume alarms are returned in insert order * Correct env variable in file oslo.config.generator.rc * Handle the metrics sent by nova notifier * Add a wadl target to the documentation * Sync config generator from oslo-incubator * Convert event timestamp to PrecisionTimestamp * Add metadata query validation limitation * Ensure the correct error message is displayed * Imported Translations from Transifex * Move sphinxcontrib-httpdomain to test-requirements * Ensure that the user/project exist on alarm update * api: raise ClientSideError rather than ValueError * Implement the /v2/sample API * service: fix service alive checking * Oslo sync to recover from db2 server disconnects * Event Storage Layer * config: specify a template for mktemp * test code should be excluded from test coverage summary * doc: remove note about Nova plugin framework * doc: fix formatting of alarm action types * Updated from global requirements * Add configuration-driven conversion to Events * add newly added constraints to expire clear\_expired\_metering\_data * fix unit * Add import for publisher\_rpc option * add more test cases to improve the test code coverage #5 * Create a shared queue for QPID topic consumers * Properly reconnect subscribing clients when QPID broker restarts * Don't need session.flush in context managed by session * sql migration error in 020\_add\_metadata\_tables 2014.1.b1 --------- * Remove rpc service from agent manager * Imported Translations from Transifex * organise requirements files * Add a Trait Type model and db table * No module named MySQLdb bug * Add a note about permissions to ceilometer logging directory * sync with oslo-incubator * Rename OpenStack Metering to OpenStack Telemetry * update docs to adjust for naming change * Add i18n warpping for all LOG messages * Imported Translations from Transifex * Removed unused method in compute agent manger * connection is not close in migration script * Fixed a bug in sql migration script 020 * Fixed nova notifier test * Added resources definition in the pipeline * Change metadata\_int's value field to type bigint * Avoid intermittent integrity error on alarm creation * Simplify the dispatcher method prototype * Use map\_method from stevedore 0.12 * Remove the collector submodule * Move dispatcher a level up * Split collector * Add a specialized Event Type model and db table * Remove old sqlalchemy-migrate workaround * Revert "Support building wheels (PEP-427)" * full pep8 compliance (part 2) * Selectively import RPC backend retry config * Fixes Hyper-V Inspector disk metrics bug * Imported Translations from Transifex * full pep8 compliance (part1) * Replace mox with mock in alarm,central,image tests * Stop ignoring H506 errors * Update hacking for real * Replace mox with mock in tests.collector * Replace mox with mock in publisher and pipeline * Replace mox with mock in novaclient and compute * Remove useless defined Exception in tests * Support building wheels (PEP-427) * Fixes Hyper-V Inspector cpu metrics bug * Replace mox with mock in tests.storage * Document user-defined metadata for swift samples * Replace mox with mock in energy and objectstore * Updated from global requirements * Replace mox with mock in tests.api.v2 * Refactor API error handling * make record\_metering\_data concurrency safe * Move tests into ceilometer module * Replace mox with mock in tests.api.v1 * Replace mox with mock in tests.api.v2.test\_compute * Corrected import order * Use better predicates from testtools instead of plain assert * Stop using openstack.common.exception * Replace mox with mock in tests.network * Replace mox with mocks in test\_inspector * Fix failing nova\_tests tests * Replace mox with mocks in tests.compute.pollsters * Add an insecure option for Keystone client * Sync log from oslo * Cleanup tests.publisher tests * mongodb, db2: do not print full URL in logs * Use wsme ClientSideError to handle unicode string * Use consistant cache key for swift pollster * Fix the developer documentation of the alarm API * Fix the default rpc policy value * Allow Events without traits to be returned * Replace tests.base part8 * Replace tests.base part7 * Replace tests.base part6 * Imported Translations from Transifex * Imported Translations from Transifex * Sync log\_handler from Oslo * Don't use sqlachemy Metadata as global var * enable sql metadata query * Replace tests.base part5 * Replace tests.base part4 * Imported Translations from Transifex * Updated from global requirements * Fix doc typo in volume meter description * Updated from global requirements * Add source to Resource API object * compute: virt: Fix Instance creation * Fix for get\_resources with postgresql * Updated from global requirements * Add tests when admin set alarm owner to its own * Replace tests.base part3 * Replace tests.base part2 * Replace tests.base part1 * Fix wrong using of Metadata in 15,16 migrations * api: update for WSME 0.5b6 compliance * Changes FakeMemcache to set token to expire on utcnow + 5 mins * Change test case get\_alarm\_history\_on\_create * Change alarm\_history.detail to text type * Add support for keystoneclient 0.4.0 * Ceilometer has no such project-list subcommand * Avoid leaking admin-ness into combination alarms * Updated from global requirements * Avoid leaking admin-ness into threshold-oriented alarms * Update Oslo * Set python-six minimum version * Ensure combination alarms can be evaluated * Ensure combination alarm evaluator can be loaded * Apply six for metaclass * add more test cases to improve the test code coverage #6 * Update python-ceilometerclient lower bound to 1.0.6 * Imported Translations from Transifex * add more test cases to improve the test code coverage #4 2013.2.rc1 ---------- * db2 does not allow None as a key for user\_id in user collection * Start Icehouse development * Imported Translations from Transifex * Disable lazy translation * Add notifications for alarm changes * Updated from global requirements * api: allow alarm creation for others project by admins * assertEquals is deprecated, use assertEqual * Imported Translations from Transifex * update alarm service setup in dev doc * Add bug number of some wsme issue * api: remove useless comments * issue an error log when cannot import libvirt * add coverage config file to control module coverage report * tests: fix rounding issue in timestamp comparison * api: return 404 if a alarm is not found * remove locals() for stringformat * add more test cases to improve the test code coverage #3 * Remove extraneous vim configuration comments * Return 401 when action is not authorized * api: return 404 if a resource is not found * keystone client changes in AuthProtocol made our test cases failing * Don't load into alarms evaluators disabled alarms * Remove MANIFEST.in * Allow to get a disabled alarm * Add example with return values in API v2 docs * Avoid imposing alembic 6.0 requirement on all distros * tests: fix places check for timestamp equality * Don't publish samples if resource\_id in missing * Require oslo.config 1.2.0 final * Don't send unuseful rpc alarm notification * service: check that timestamps are almost equals * Test the response body when deleting a alarm * Change resource.resource\_metadata to text type * Adding region name to service credentials * Fail tests early if mongod is not found * add more test cases to improve the test code coverage #2 * add more test cases to improve the test code coverage #1 * Imported Translations from Transifex * Replace OpenStack LLC with OpenStack Foundation * Use built-in print() instead of print statement * Simple alarm partitioning protocol based on AMQP fanout RPC * Handle manually mandatory field * Provide new API endpoint for alarm state * Implement the combination evaluator * Add alarm combination API * Notify with string representation of alarm reason * Convert BoundedInt value from json into int * Fix for timestamp precision in SQLAlchemy * Add source field to Meter model * Refactor threshold evaluator * Alarm API update * Update requirements * WSME 0.5b5 breaking unit tests * Fix failed downgrade in migrations * refactor db2 get\_meter\_statistics method to support mongodb and db2 * tests: import pipeline config * Fix a tiny mistake in api doc * collector-udp: use dispatcher rather than storage * Imported Translations from Transifex * Drop sitepackages=False from tox.ini * Update sphinxcontrib-pecanwsme to 0.3 * Architecture enhancements * Force MySQL to use InnoDB/utf8 * Update alembic requirement to 0.6.0 version * Correctly output the sample content in the file publisher * Pecan assuming meter names are extensions * Handle inst not found exceptions in pollsters * Catch exceptions from nova client in poll\_and\_publish * doc: fix storage backend features status * Add timestamp filtering cases in storage tests * Imported Translations from Transifex * Use global openstack requirements * Add group by statistics examples in API v2 docs * Add docstrings to some methods * add tests for \_query\_to\_kwargs func * validate counter\_type when posting samples * Include auth\_token middleware in sample config * Update config generator * run-tests: fix MongoDB start wait * Imported Translations from Transifex * Fix handling of bad paths in Swift middleware * Drop the \*.create.start notification for Neutron * Make the Swift-related doc more explicit * Fix to return latest resource metadata * Update the high level architecture * Alarm history storage implementation for sqlalchemy * Improve libvirt vnic parsing with missing mac! * Handle missing libvirt vnic targets! * Make type guessing for query args more robust * add MAINTAINERS file * nova\_notifier: fix tests * Update openstack.common.policy from oslo-incubator * Clean-ups related to alarm history patches * Improved MongoClient pooling to avoid out of connections error * Disable the pymongo pooling feature for tests * Fix wrong migrations * Fixed nova notifier unit test * Add group by statistics in API v2 * Update to tox 1.6 and setup.py develop * Add query support to alarm history API * Reject duplicate events * Fixes a bug in Kwapi pollster * alarm api: rename counter\_name to meter\_name * Fixes service startup issue on Windows * Handle volume.resize.\* notifications * Network: process metering reports from Neutron * Alarm history storage implementation for mongodb * Fix migration with fkeys * Fixes two typos in this measurements.rst * Add a fake UUID to Meter on API level * Append /usr/sbin:/sbin to the path for searching mongodb * Plug alarm history logic into the API * Added upper version boundry for six * db2 distinct call results are different from mongodb call * Sync rpc from oslo-incubator * Imported Translations from Transifex * Add pagination parameter to the database backends of storage * Base Alarm history persistence model * Fix empty metadata issue of instance * alarm: generate alarm\_id in API * Import middleware from Oslo * Imported Translations from Transifex * Adds group by statistics for MongoDB driver * Fix wrong UniqueConstraint name * Adds else and TODO in statistics storage tests * Imported Translations from Transifex * Extra indexes cleanup * API FunctionalTest class lacks doc strings * install manual last few sections format needs to be fixed * api: update v1 for Flask >= 0.10 * Use system locale when Accept-Language header is not provided * Adds Hyper-V compute inspector * missing resource in middleware notification * Support for wildcard in pipeline * Refactored storage tests to use testscenarios * doc: replace GitHub by git.openstack.org * api: allow usage of resource\_metadata in query * Remove useless doc/requirements * Fixes non-string metadata query issue * rpc: reduce sleep time * Move sqlachemy tests only in test\_impl\_sqlachemy * Raise Error when pagination/groupby is missing * Raise Error when pagination support is missing * Use timeutils.utcnow in alarm threshold evaluation * db2 support * plugin: remove is\_enabled * Doc: improve doc about Nova measurements * Storing events via dispatchers * Imported Translations from Transifex * ceilometer-agent-compute did not catch exception for disk error * Change counter to sample in network tests * Change counter to sample in objectstore tests * Remove no more used code in test\_notifier * Change counter to sample vocable in cm.transformer * Change counter to sample vocable in cm.publisher * Change counter to sample vocable in cm.image * Change counter to sample vocable in cm.compute * Change counter to sample vocable in cm.energy * Use samples vocable in cm.publisher.test * Change counter to sample vocable in volume tests * Change counter to sample vocable in api tests * Add the source=None to from\_notification * Make RPCPublisher flush method threadsafe * Enhance delayed message translation when \_ is imported * Remove use\_greenlets argument to MongoClient * Enable concurrency on nova notifier tests * Imported Translations from Transifex * Close database connection for alembic env * Fix typo in 17738166b91 migration * Don't call publisher without sample * message\_id is not allowed to be submitted via api * Api V2 post sample refactoring * Add SQLAlchemy implementation of groupby * Fixes failed notification when deleting instance * Reinitialize pipeline manager for service restart * Sync gettextutils from oslo-incubator * Doc: clearly state that one can filter on metadata * Add HTTP request/reply samples * Use new olso fixture in CM tests * Imported Translations from Transifex * Bump hacking to 0.7.0 * Fix the dict type metadata missing issue * Raise error when period with negative value * Imported Translations from Transifex * Import missing gettext \_ * Remove 'counter' occurences in pipeline * Remove the mongo auth warning during tests * Change the error message of resource listing in mongodb * Change test\_post\_alarm case in test\_alarm\_scenarios * Skeletal alarm history API * Reorg alarms controller to facilitate history API * Fix Jenkins failed due to missing \_ * Fix nova test\_notifier wrt new notifier API * Remove counter occurences from documentation * Updated from global requirements * Fixes dict metadata query issue of HBase * s/alarm/alarm\_id/ in alarm notification * Remove unused abstract class definitions * Removed unused self.counters in storage test class * Initial alarming documentation * Include previous state in alarm notification * Consume notification from the default queue * Change meter.resource\_metadata column type * Remove MongoDB TTL support for MongoDB < 2.2 * Add first and last sample timestamp * Use MongoDB aggregate to get resources list * Fix resources/meters pagination test * Handle more Nova and Neutron events * Add support for API message localization * Add the alarm id to the rest notifier body * fix alarm notifier tests * Sync gettextutils from oslo * Fix generating coverage on MacOSX * Use the new nova Instance class * Return message\_id in POSTed samples * rpc: remove source argument from message conversion * Remove source as a publisher argument * Add repeat\_actions to alarm * Rename get\_counters to get\_samples * Add pagination support for MongoDB * Doc: measurements: add doc on Cinder/Swift config * Update nova\_client.py * objectstore: trivial cleanup in \_Base * Add support for CA authentication in Keystone * add unit attribute to statistics * Fix notify method signature on LogAlarmNotifier * Fix transformer's LOG TypeError * Update openstack.common * Fixes Hbase metadata query return wrong result * Fix Hacking 0.6 warnings * Make middleware.py Python 2.6 compatible * Call alembic migrations after sqlalchemy-migrate * Rename ceilometer.counter to ceilometer.sample * Added separate MongoDB database for each test * Relax OpenStack upper capping of client versions * Refactored MongoDB connection pool to use weakrefs * Centralized backends tests scenarios in one place * Added tests to verify that local time is correctly handled * Refactored impl\_mongodb to use full connection url * calling distinct on \_id field against a collection is slow * Use configured endpoint\_type everywhere * Allow use of local conductor * Update nova configuration doc to use notify\_on\_state\_change * doc: how to inject user-defined data * Add documentation on nova user defined metadata * Refactored API V2 tests to use testscenarios * Refactored API V1 tests to use testscenarios * alarm: Per user setting to disable ssl verify * alarm: Global setting to disable ssl verification * Imported Translations from Transifex * Implementation of the alarm RPCAlarmNotifier * Always init cfg.CONF before running a test * Sets storage\_conn in CollectorService * Remove replace/preserve logic from rate of change transformer * storage: remove per-driver options * hbase: do not register table\_prefix as a global option * mongodb: do not set replica\_set as a global option * Change nose to testr in the documentation * Fixed timestamp creation in MongoDB mapreduce * Ensure url is a string for requests.post * Implement a https:// in REST alarm notification * Implement dot in matching\_metadata key for mongodb * trailing slash in url causes 404 error * Fix missing foreign keys * Add cleanup migration for indexes * Sync models with migrations * Avoid dropping cpu\_util for multiple instances * doc: /statistics fields are not queryable (you cannot filter on them) * fix resource\_metadata failure missing image data * Standardize on X-Project-Id over X-Tenant-Id * Default to ctx user/project ID in sample POST API * Multiple dispatcher enablement * storage: fix clear/upgrade order * Lose weight for Ceilometer log in verbose mode * publisher.rpc: queing policies * Remove useless mongodb connection pool comment * Add index for db.meter by descending timestamp * doc: add a bunch of functional examples for the API * api: build the storage connection once and for all * Fix the argument of UnknownArgument exception * make publisher procedure call configurable * Disable mongod prealloc, wait for it to start * Added alembic migrations * Allow to enable time to live on metering sample * Implement a basic REST alarm notification * Imported Translations from Transifex * Ensure correct return code of run-tests.sh * File based publisher * Unset OS\_xx variable before generate configuration * Use run-tests.sh for tox coverage tests * Emit cpu\_util from transformer instead of pollster * Allow simpler scale exprs in transformer.conversions * Use a real MongoDB instance to run unit tests * Allow to specify the endpoint type to use * Rename README.md to README.rst * Use correct hostname to get instances * Provide CPU number as additional metadata * Remove get\_counter\_names from the pollster plugins * Sync SQLAlchemy models with migrations * Transformer to measure rate of change * Make sure plugins are named after their meters * Break up the swift pollsters * Split up the glance pollsters * Make visual coding style consistent * Separate power and energy pollsters * Break up compute pollsters * Implement a basic alarm notification service * Optionally store Events in Collector * Fix issue with pip installing oslo.config-1.2.0 * Transformer to convert between units * publisher.rpc: make per counter topic optional * ceilometer tests need to be enabled/cleaned * Also accept timeout parameter in FakeMemCache * Fix MongoDB backward compat wrt units * Use oslo.sphinx and remove local copy of doc theme * Reference setuptools and not distribute * enable v2 api hbase tests * Register all interesting events * Unify Counter generation from notifications * doc: enhance v2 examples * Update glossary * Imported Translations from Transifex * Imported Translations from Transifex * Filter query op:gt does not work as expected * sqlalchemy: fix performance issue on get\_meters() * enable v2 api sqlalchemy tests * Update compute vnic pollster to use cache * Update compute CPU pollster to use cache * Update compute disk I/O pollster to use cache * update Quantum references to Neutron * Update swift pollster to use cache * Update kwapi pollster to use cache * Update floating-ip pollster to use cache * Update glance pollster to use cache * Add pollster data cache * Fix flake8 errors * Update Oslo * Enable Ceilometer to support mongodb replication set * Fix return error when resource can't be found * Simple service for singleton threshold eval * Basic alarm threshold evaluation logic * add metadata to nova\_client results * Bring in oslo-common rpc ack() changes * Pin the keystone client version * Fix auth logic for PUT /v2/alarms * Imported Translations from Transifex * Change period type in alarms API to int * mongodb: fix limit value not being an integer * Check that the config file sample is always up to date * api: enable v2 tests on SQLAlchemy & HBase * Remove useless periodic\_interval option * doc: be more explicit about network counters * Capture instance metadata in reserved namespace * Imported Translations from Transifex * pep8: enable E125 checks * pep8: enable F403 checks * pep8: enable H302 checks * pep8: enable H304 checks * pep8: enable H401 * pep8: enable H402 checks * Rename the MeterPublisher to RPCPublisher * Replace publisher name by URL * Enable pep8 H403 checks * Activate H404 checks * Ceilometer may generate wrong format swift url in some situations * Code cleanup * Update Oslo * Use Flake8 gating for bin/ceilometer-\* * Update requirements to fix devstack installation * Update to the latest stevedore * Start gating on H703 * Remove disabled\_notification\_listeners option * Remove disabled\_compute\_pollsters option * Remove disabled\_central\_pollsters option * Longer string columns for Trait and UniqueNames * Fix nova notifier tests * pipeline: switch publisher loading model to driver * Enforce reverse time-order for sample return * Remove explicit distribute depend * Use Python 3.x compatible octal literals * Improve Python 3.x compatibility * Fix requirements * Corrected path for test requirements in docs * Fix some typo in documentation * Add instance\_scheduled in entry points * fix session connection * Remove useless imports, reenable F401 checks * service: run common initialization stuff * Use console scripts for ceilometer-api * Use console scripts for ceilometer-dbsync * Use console scripts for ceilometer-agent-compute * Use console scripts for ceilometer-agent-central * agent-central: use CONF.import\_opt rather than import * Move os\_\* options into a group * Use console scripts for ceilometer-collector * sqlalchemy: migration error when running db-sync * session flushing error * api: add limit parameters to meters * python3: Introduce py33 to tox.ini * Start to use Hacking * Session does not use ceilometer.conf's database\_connection * Add support for limiting the number of samples returned * Imported Translations from Transifex * Add support policy to installation instructions * sql: fix 003 downgrade * service: remove useless PeriodicService class * Fix nova notifier tests * Explicitly set downloadcache in tox.ini * Imported Translations from Transifex 2013.2.b1 --------- * Switch to sphinxcontrib-pecanwsme for API docs * Update oslo, use new configuration generator * doc: fix hyphens instead of underscores for 'os\*' conf options * Allow specifying a listen IP * Log configuration values on API startup * Don't use pecan to configure logging * Mark sensitive config options as secret * Imported Translations from Transifex * ImagePollster record duplicate counter during one poll * Rename requires files to standard names * Add an UDP publisher and receiver * hbase metaquery support * Imported Translations from Transifex * Fix and update extract\_opts group extraction * Fix the sample name of 'resource\_metadata' * Added missing source variable in storage drivers * Add Event methods to db api * vnics: don't presume existence of filterref/filter * force the test path to a str (sometimes is unicode) * Make sure that v2 api tests have the policy file configured * Imported Translations from Transifex * setup.cfg misses swift filter * Add a counter for instance scheduling * Move recursive\_keypairs into utils * Replace nose with testr * Use fixtures in the tests * fix compute units in measurement doc * Allow suppression of v1 API * Restore default interval * Change from unittest to testtools * remove unused tests/skip module * Imported Translations from Transifex * Get all tests to use tests.base.TestCase * Allow just a bit longer to wait for the server to startup * Document keystone\_authtoken section * Restore test dependency on Ming * Set the default pipline config file for tests * Imported Translations from Transifex * Fix cross-document references * Fix config setting references in API tests * Restrict pep8 & co to pep8 target * Fix meter\_publisher in setup.cfg * Use flake8 instead of pep8 * Imported Translations from Transifex * Use sqlalchemy session code from oslo * Switch to pbr * fix the broken ceilometer.conf.sample link * Add a direct Ceilometer notifier * Do the same auth checks in the v2 API as in the v1 API * Add the sqlalchemy implementation of the alarms collection * Allow posting samples via the rest API (v2) * Updated the ceilometer.conf.sample * Don't use trivial alarm\_id's like "1" in the test cases * Fix the nova notifier tests after a nova rename * Document HBase configuration * alarm: fix MongoDB alarm id * Use jsonutils instead of json in test/api.py * Connect the Alarm API to the db * Add the mongo implementation of alarms collection * Move meter signature computing into meter\_publish * Update WSME dependency * Imported Translations from Transifex * Add Alarm DB API and models * Imported Translations from Transifex * Remove "extras" again * add links to return values from API methods * Modify limitation on request version * Doc improvements * Rename EventFilter to SampleFilter * Fixes AttributeError of FloatingIPPollster * Add just the most minimal alarm API * Update oslo before bringing in exceptions * Enumerate the meter type in the API Meter class * Remove "extras" as it is not used * Adds examples of CLI and API queries to the V2 documentation * Measurements documentation update * update the ceilometer.conf.sample * Set hbase table\_prefix default to None * glance/cinder/quantum counter units are not accurate/consistent * Add some recommendations about database * Pin SQLAlchemy to 0.7.x * Ceilometer configuration.rst file not using right param names for logging * Fix require\_map\_reduce mim import * Extend swift middleware to collect number of requests * instances: fix counter unit * Remove Folsom support * transformer, publisher: move down base plugin classes * pipeline, publisher, transformer: reorganize code * Fix tests after nova changes * Update to the lastest loopingcall from oslo * Imported Translations from Transifex * update devstack instructions for cinder * Update openstack.common * Reformat openstack-common.conf * storage: move nose out of global imports * storage: get rid of get\_event\_interval * Remove gettext.install from ceilometer/\_\_init\_\_.py * Prepare for future i18n use of \_() in nova notifier * Update part of openstack.common * Convert storage drivers to return models * Adpated to nova's gettext changes * add v2 query examples * storage: remove get\_volume\_sum and get\_volume\_max * api: run tests against HBase too * api: run sum unit tests against SQL backend too * Split and fix live db tests * Remove impl\_test * api: run max\_resource\_volume test on SQL backend * Refactor DB tests * fix volume tests to utilize VOLUME\_DELETE notification * Open havana development, bump to 2013.2 2013.1 ------ * Change the column counter\_volume to Float * tests: disable Ming test if Ming unavailable * Imported Translations from Transifex * enable arguments in tox * api: run max\_volume tests on SQL backend too * api: run list\_sources tests on SQL and Mongo backend * api: run list\_resources test against SQL * api: handle case where metadata is None * Fix statistics period computing with start/end time * Allow publishing arbitrary headers via the "storage.objects.\*.bytes" counter * Updated the description of get\_counters routine * enable xml error message response * Swift pollster silently return no counter if keystone endpoint is not present * Try to get rid of the "events" & "raw events" naming in the code * Switch to python-keystoneclient 0.2.3 * include a copy of the ASL 2.0 * add keystone configuration instructions to manual install docs * Update openstack.common * remove unused dependencies * Set the default\_log\_levels to include keystoneclient * Switch to final 1.1.0 oslo.config release * Add deprecation warnings for V1 API * Raise stevedore requirement to 0.7 * Fixed the blocking unittest issues * Fix a pep/hacking error in a swift import * Add sample configuration files for mod\_wsgi * Add a tox target for building documentation * Use a non-standard port for the test server * Ensure the statistics are sorted * Start both v1 and v2 api from one daemon * Handle missing units values in mongodb data * Imported Translations from Transifex * Make HACKING compliant * Update manual installation instructions * Fix oslo.config and unittest * Return something sane from the log impl * Fix an invalid test in the storage test suite * Add the etc directory to the sdist manifest * api: run compute duration by resource on SQL backend * api: run list\_projects tests against SQL backend too * api: run list users test against SQL backend too * api: run list meters tests against SQL backend too * Kwapi pollster silently return no probre if keystone endpoint is not present * HBase storage driver, initial version * Exclude tests directory from installation * Ensure missing period is treated consistently * Exclude tests when installing ceilometer * Run some APIv1 tests on different backends * Remove old configuration metering\_storage\_engine * Set where=tests * Decouple the nova notifier from ceilometer code * send-counter: fix & test * Remove nose wrapper script * Fix count type in MongoDB * Make sure that the period is returned as an int as the api expects an int * Imported Translations from Transifex * Remove compat cfg wrapper * compute: fix unknown flavor handling * Allow empty dict as metaquery param for sqlalchemy * Add glossary definitions for additional terms * Support different publisher interval * Fix message envelope keys * Revert recent rpc wire format changes * Document the rules for units * Fix a bug in compute manager test case * plugin: don't use @staticmethod with abc * Support list/tuple as meter message value * Imported Translations from Transifex * Update common to get new kombu serialization code * Disable notifier tests * pipeline: manager publish multiple counters * Imported Translations from Transifex * Use oslo-config-2013.1b3 * mongodb: make count an integer explicitely * tests: allow to run API tests on live db * Update to latest oslo-version * Imported Translations from Transifex * Add directive to MANIFEST.in to include all the html files * Use join\_consumer\_pool() for notifications * Update openstack.common * Add period support in storage drivers and API * Update openstack/common tree * storage: fix mongo live tests * swift: configure RPC service correctly * Fix tox python version for Folsom * api: use delta\_seconds() * transformer: add acculumator transformer * Import service when cfg.CONF.os\_\* is used * pipeline: flush after publishing call * plugin: format docstring as rst * Use Mongo finalize to compute avg and duration * Code cleanup, remove useless import * api: fix a test * compute: fix notifications test * Move counter\_source definition * Allow to publish several counters in a row * Fixed resource api in v2-api * Update meter publish with pipeline framework * Use the same Keystone client instance for pollster * pipeline: fix format error in logging * More robust mocking of nova conductor * Mock more conductor API methods to unblock tests * Update pollsters to return counter list * Update V2 API documentation * Added hacking.py support to pep8 portion of tox * setup: fix typo in package data * Fix formatting issue with v1 API parameters * Multiple publisher pipeline framework * Remove setuptools\_git from setup\_requires * Removed unused param for get\_counters() * Use WSME 0.5b1 * Factorize agent code * Fixed the TemplateNotFound error in v1 api * Ceilometer-api is crashing due to pecan module missing * Clean class variable in compute manager test case * Update nova notifier test after nova change * Fix documentation formatting issues * Simplify ceilometer-api and checks Keystone middleware parsing * Fix nova conf compute\_manager unavailable * Rename run\_tests.sh to wrap\_nosetests.sh * Update openstack.common * Corrected get\_raw\_event() in sqlalchemy * Higher level test for db backends * Remove useless imports * Flatten the v2 API * Update v2 API for WSME code reorg * Update WebOb version specification * Remove the ImageSizePollster * Add Kwapi pollster (energy monitoring) * Fixes a minor documentation typo * Peg the version of Ming used in tests * Update pep8 to 1.3.3 * Remove leftover useless import * Enhance policy test for init() * Provide the meters unit's in /meters * Fix keystoneclient auth\_token middleware changes * policy: fix policy\_file finding * Remove the \_initialize\_config\_options * Add pyflakes * Make the v2 API date query parameters consistent * Fix test blocking issue and pin docutils version * Apply the official OpenStack stylesheets and templates to the Doc build * Fixed erroneous source filter in SQLAlchemy * Fix warnings in the documentation build * Handle finish and revert resize notifications * Add support for Folsom version of Swift * Implement user-api * Add support for Swift incoming/outgoing trafic metering * Pass a dict configuration file to auth\_keystone * Import only once in nova\_notifier * Fix MySQL charset error * Use default configuration file to make test data * Fix Glance control exchange * Move back api-v1 to the main api * Fix WSME arguments handling change * Remove useless gettext call in sql engine * Ground work for transifex-ify ceilometer * Add instance\_type information to NetPollster * Fix dbsync API change * Fix image\_id in instance resource metadata * Instantiate inspector in compute manager * remove direct nova db access from ceilometer * Make debugging the wsme app a bit easier * Implements database upgrade as storage engine independent * Fix the v1 api importing of acl * Add the ability to filter on metadata * Virt inspector directly layered over hypervisor API * Move meter.py into collector directory * Change mysql schema from latin1 to utf8 * Change default os-username to 'ceilometer' * Restore some metadata to the events and resources * Update documentation URL * Add sql db option to devstack for ceilometer * Remove debug print in V2 API * Start updating documentation for V2 API * Implement V2 API with Pecan and WSME * Move v1 API files into a subdirectory * Add test storage driver * Implement /meters to make discovery "nicer" from the client * Fix sqlalchemy for show\_data and v1 web api * Implement object store metering * Make Impl of mongodb and sqlalchemy consistent * add migration migrate.cfg file to the python package * Fixes to enable the jenkins doc job to work * Lower the minimum required version of anyjson * Fix blocking test for nova notifier * network: remove left-over useless nova import * tools: set novaclient minimum version * libvirt: fix Folsom compatibility * Lower pymongo dependency * Remove rickshaw subproject * Remove unused rpc import * Adapted to nova's compute\_driver moving * doc: fix cpu counter unit * tools: use tarballs rather than git for Folsom tests * Used auth\_token middleware from keystoneclient * Remove cinderclient dependency * Fix latest nova changes * api: replace minified files by complete version * Add Folsom tests to tox * Handle nova.flags removal * Provide default configuration file * Fix mysql\_engine option type * Remove nova.flags usage * api: add support for timestamp in \_list\_resources() * api: add timestamp interval support in \_list\_events() * tests: simplify api list\_resources * Update openstack.common(except policy) * Adopted the oslo's rpc.Service change * Use libvirt num\_cpu for CPU utilization calculation * Remove obsolete reference to instance.vcpus * Change references of /etc/ceilometer-{agent,collector}.conf to /etc/ceilometer/ceilometer.conf * Determine instance cores from public flavors API * Determine flavor type from the public nova API * Add comment about folsom compatibility change * Add keystone requirement for doc build * Avoid TypeError when loading libvirt.LibvirtDriver * Don't re-import flags and do parse\_args instead of flags.FLAGS() * doc: rename stackforge to openstack * Fix pymongo requirements * Update .gitreview for openstack * Update use of nova config to work with folsom * compute: remove get\_disks work-around * Use openstack versioning * Fix documentation build * document utc naive timestamp * Remove database access from agent pollsters * Fix merge error in central/manager.py * Fix nova config parsing * pollster trap error due to zero floating ip * Use the service.py in openstack-common * Allow no configured sources, provide a default file * Add service.py from openstack-common * Update common (except policy) * nova fake libvirt library breaking tests * Move db access out into a seperate file * Remove invalid fixme comments * Add new cpu\_util meter recording CPU utilization % * Fix TypeError from old-style publish\_counter calls * Fix auth middleware configuration * pin sqlalchemy to 0.7.x but not specifically 0.7.8 * add mongo index names * set tox to ignore global packages * Provide a way to disable some plugins * Use stevedore to load all plugins * implement get\_volume\_max for sqlalchemy * Add basic text/html renderer * network: floating IP account in Quantum * add unit test for CPUPollster * Clean up context usage * Add dependencies on clients used by pollsters * add ceilometer-send-counter * Update openstack.common.cfg * Fix tests broken by API change with Counter class * api: add source detail retrieval * Set source at publish time * Instance pollster emits instance. meter * timestamp columns in sqlalchemy not timezone aware * Remove obsolete/incorrect install instructions * network: emit router meter * Fix sqlalchemy performance problem * Added a working release-bugs.py script to tools/ * Change default API port * sqlalchemy record\_meter merge objs not string * Use glance public API as opposed to registry API * Add OpenStack trove classifier for PyPI * bump version number to 0.2 0.1 --- * Nova libvirt release note * Update metadata for PyPI registration * tox: add missing venv * Fixes a couple typos * Counter renaming * Set correct timestamp on floatingip counter * Fix API change in make\_test\_data.py * Fix Nova URL in doc * Some more doc fixes * Ignore instances in the ERROR state * Use the right version number in documentation * doc: fix network.\*.\* resource id * image: handle glance delete notifications * image: handle glance upload notifications * image: add update event, fix ImageServe owner * network: fix create/update counter type & doc * Assorted doc fixes * add max/sum project volume and fix tests * Add general options * compute.libvirt: split read/write counters * API: add Keystone ACL and policy support * Add documentation for configuration options * network: do not emit counter on exists event, fix resource id * Move net function in class method and fix instance id * Prime counter table * Fix the configuration for the nova notifier * Initialize the control\_exchange setting * Set version 0.1 * Make the instance counters use the same type * Restore manual install documentation * add quantum release note * Add release notes to docs * Update readme and create release notes * Remove duration field in Counter * Add counter for number of packets per vif * Move instance counter into its own pollster * Add a request counter for instance I/O * Rename instance disk I/O counter * Rename instances network counters * Use constant rather than string from counter type * Update the architecture diagram * Increase default polling interval * Fix compute agent publishing call * network: listen for Quantum exists event * Correct requirements filename * Fix notification subscription logic * Fix quantum notification subscriptions * Split meter publishing from the global config obj * network: add counter for actions * network: listen for Quantum notifications * Rename absolute to gauge * Fix typo in control exchanges help texts * Rework RPC notification mechanism * Update packaging files * Update URL list * Update openstack.common * Add volume/sum API endpoint for resource meters * Add resource volume/max api call * Fix dependency on anyjson * Listen for volume.delete.start instead of end * implement sqlalchemy dbengine backend * Add a notification handler for image downloads * Allow glance pollster tests to run * Create tox env definition for using a live db * Picking up dependencies from pip-requires file * Specify a new queue in manager * Rework RPC connection * Stop using nova's rpc module * Add configuration script to turn on notifications * Pep8 fixes, implement pep8 check on tests subdir * Use standard CLI options & env vars for creds * compute: remove get\_metadata\_from\_event() * Listen for volume notifications * Add pollster for Glance * Fix Nova notifier test case * Fix nova flag parsing * Add nova\_notifier notification driver for nova * Split instance polling code * Use stevedore to load storage engine drivers * Implement duration calculation API * Create tool for generating test meter data * Update openstack-common code to latest * Add bin/ceilometer-api for convenience * Add local copy of architecture diagram * Add timestamp parameters to the API docs * Check for doc build dependency before building * Pollster for network internal traffic (n1,n2) * Fix PEP8 issues * Add archicture diagram to documentation * added mongodb auth * Change timestamp management for resources * Log the instance causing the error when a pollster fails * Document how to install with devstack * Remove test skipping logic * Remove dependency on nova test modules * Add date range parameters to resource API * Add setuptools-git support * Add separate notification handler for instance flavor * Change instance meter type * Split the existing notification handlers up * Remove redundancy in the API * Separate the tox coverage test setup from py27 * Do not require user or project argument for event query * Add pymongo dependency for readthedocs.org build * Update openstack.common * Add API documentation * Be explicit about test dir * Add list projects API * Sort list of users and projects returned from queries * Add project arg to event and resource queries * Fix "meter" literal in event list API * collector exception on record\_metering\_data * Add API endpoint for listing raw event data * Change compute pollster API to work on one instance at a time * Create "central" agent * Skeleton for API server * fix use of source value in mongdb driver * Add {root,ephemeral}\_disk\_size counters * Implements vcpus counter * Fix nova configuration loading * Implements memory counter * Fix and document counter types * Check compute driver using new flag * Add openstack.common.{context,notifier,log} and update .rpc * Update review server link * Add link to roadmap * Add indexes to MongoDB driver * extend developer documentation * Reset the correct nova dependency URL * Switch .gitreview to use OpenStack gerrit * Add MongoDB engine * Convert timestamps to datetime objects before storing * Reduce complexity of storage engine API * Remove usage of nova.log * Documentation edits: * fix typo in instance properties list * Add Sphinx wrapper around existing docs * Configure nova.flags as well as openstack.common.cfg * First draft of plugin/agent documentation. Fixes bug 1018311 * Essex: update Nova to 2012.1.1, add python-novaclient * Split service preparation, periodic interval configurable * Use the same instance metadata everywhere * Emit meter event for instance "exists" * Start defining DB engine API * Fallback on nova.rpc for Essex * Add instance metadata from notification events * Combined fix to get past broken state of repo * Add more metadata to instance counter * Register storage options on import * Add Essex tests * log more than ceilometer * Remove event\_type field from meter messages * fix message signatures for nested dicts * Remove nova.flags usage * Copy openstack.common.cfg * check message signatures in the collector * Sketch out a plugin system for saving metering data * refactor meter event publishing code * Add and use ceilometer own log module * add counter type field * Use timestamp instead of datetime when creating Counter * Use new flag API * Fix a PEP8 error * Make the stand-alone test script mimic tox * Remove unneeded eventlet test requirement * Add listeners for other instance-related events * Add tox configuration * Use openstack.common.cfg for ceilometer options * Publish and receive metering messages * Add floating IP pollster * Fix tests based on DB by importing nova.tests * make the pollsters in the agent plugins * Build ceilometer-agent and ceilometer-collector * Add plugin support to the notification portion of the collector daemon * Add CPU time fetching * Add an example function for converting a nova notification to a counter * add a tool for recording notifications and replaying them * Add an exception handler to deal with errors that occur when the info in nova is out of sync with reality (as on my currently broken system). Also adds a nova prefix to the logger for now so messages from this module make it into the log file * Periodically fetch for disk io stats * Use nova.service, add a manager class * Change license to Apache 2.0 * Add setup.py * Import ceilometer-nova-compute * Ignore pyc files * Add link to blueprint * Add .gitreview file * initial commit ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1383567 ceilometer-27.0.0.0rc1/HACKING.rst0000664000175000017500000000367415250267452015136 0ustar00zuulzuulCeilometer Style Commandments ============================= - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Ceilometer Specific Commandments -------------------------------- N/A Running Unit Tests ------------------ Tests are run using ``tox``:: tox -e py3 By default, log output from tests flows to stderr at ``INFO`` level, matching the behaviour of the master branch. Two environment variables control this: ``OS_LOG_CAPTURE`` When set to ``True`` (or ``true``, ``1``, ``yes``), the ``StandardLogging`` fixture is activated. Log records are captured in an in-memory buffer rather than printed to the terminal during the run. On test failure the captured output is surfaced automatically, making it easier to diagnose the failure without noise from passing tests. ``OS_DEBUG`` When set to ``True`` (or ``true``, ``1``, ``yes``), the log level is raised to ``DEBUG``. Typical workflows:: # Quiet run — failures show captured INFO logs OS_LOG_CAPTURE=1 tox -e py3 # Quiet run — failures show captured DEBUG logs OS_LOG_CAPTURE=1 OS_DEBUG=True tox -e py3 # Live log output to stderr (default) tox -e py3 Creating Unit Tests ------------------- For every new feature, unit tests should be created that both test and (implicitly) document the usage of said feature. If submitting a patch for a bug that had no unit test, a new passing unit test should be added. If a submitted bug fix does have a unit test, be sure to add a new one that fails without the patch and passes with the patch. All unittest classes must ultimately inherit from oslotest.base.BaseTestCase. All setUp and tearDown methods must upcall using the super() method. tearDown methods should be avoided and addCleanup calls should be preferred. Never manually create tempfiles. Always use the tempfile fixtures from the fixture library to ensure that they are cleaned up. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1393569 ceilometer-27.0.0.0rc1/LICENSE0000664000175000017500000002363715250267452014346 0ustar00zuulzuul Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3501275 ceilometer-27.0.0.0rc1/PKG-INFO0000644000175000017500000000717015250267567014435 0ustar00zuulzuulMetadata-Version: 2.4 Name: ceilometer Version: 27.0.0.0rc1 Summary: OpenStack Telemetry Home-page: https://docs.openstack.org/ceilometer/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Topic :: System :: Monitoring Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.11 License-File: LICENSE Requires-Dist: xmltodict>=0.13.0 Requires-Dist: cachetools>=2.1.0 Requires-Dist: cotyledon>=1.3.0 Requires-Dist: futurist>=1.8.0 Requires-Dist: jsonpath-rw-ext>=1.1.3 Requires-Dist: lxml>=4.5.1 Requires-Dist: msgpack>=0.5.2 Requires-Dist: oslo.concurrency>=3.29.0 Requires-Dist: oslo.config>=8.6.0 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: oslo.log>=3.36.0 Requires-Dist: oslo.reports>=1.18.0 Requires-Dist: oslo.rootwrap>=2.0.0 Requires-Dist: pbr>=2.0.0 Requires-Dist: oslo.messaging>=10.3.0 Requires-Dist: oslo.upgradecheck>=0.1.1 Requires-Dist: oslo.utils>=4.7.0 Requires-Dist: oslo.privsep>=1.32.0 Requires-Dist: keystoneauth1>=3.18.0 Requires-Dist: python-novaclient>=9.1.0 Requires-Dist: python-swiftclient>=3.2.0 Requires-Dist: PyYAML>=5.1 Requires-Dist: requests>=2.25.1 Requires-Dist: stevedore>=1.20.0 Requires-Dist: tenacity>=6.3.1 Requires-Dist: tooz>=1.47.0 Requires-Dist: oslo.cache>=1.26.0 Requires-Dist: gnocchiclient>=7.0.0 Requires-Dist: python-zaqarclient>=1.3.0 Requires-Dist: prometheus-client>=0.20.0 Requires-Dist: aodhclient>=3.8.0 Requires-Dist: awscurl>=0.40 Requires-Dist: botocore>=1.20.0 Requires-Dist: openstacksdk>=4.6.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary ========== Ceilometer ========== .. image:: https://governance.openstack.org/tc/badges/ceilometer.svg .. Change things from this point on -------- Overview -------- Ceilometer is a data collection service that collects event and metering data by monitoring notifications sent from OpenStack services. It publishes collected data to various targets including data stores and message queues. Ceilometer is distributed under the terms of the Apache License, Version 2.0. The full terms and conditions of this license are detailed in the LICENSE file. ------------- Documentation ------------- Release notes are available at https://releases.openstack.org/teams/telemetry.html Developer documentation is available at https://docs.openstack.org/ceilometer/latest/ Launchpad Projects ------------------ - Server: https://launchpad.net/ceilometer Code Repository --------------- - Server: https://opendev.org/openstack/ceilometer Bug Tracking ------------ - Bugs: https://bugs.launchpad.net/ceilometer/ Release Notes ------------- - Server: https://docs.openstack.org/releasenotes/ceilometer/ IRC --- IRC Channel: #openstack-telemetry on `OFTC`_. Mailinglist ----------- Project use http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss as the mailinglist. Please use tag ``[Ceilometer]`` in the subject for new threads. .. _OFTC: https://oftc.net/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1393569 ceilometer-27.0.0.0rc1/README.rst0000664000175000017500000000253715250267452015024 0ustar00zuulzuul========== Ceilometer ========== .. image:: https://governance.openstack.org/tc/badges/ceilometer.svg .. Change things from this point on -------- Overview -------- Ceilometer is a data collection service that collects event and metering data by monitoring notifications sent from OpenStack services. It publishes collected data to various targets including data stores and message queues. Ceilometer is distributed under the terms of the Apache License, Version 2.0. The full terms and conditions of this license are detailed in the LICENSE file. ------------- Documentation ------------- Release notes are available at https://releases.openstack.org/teams/telemetry.html Developer documentation is available at https://docs.openstack.org/ceilometer/latest/ Launchpad Projects ------------------ - Server: https://launchpad.net/ceilometer Code Repository --------------- - Server: https://opendev.org/openstack/ceilometer Bug Tracking ------------ - Bugs: https://bugs.launchpad.net/ceilometer/ Release Notes ------------- - Server: https://docs.openstack.org/releasenotes/ceilometer/ IRC --- IRC Channel: #openstack-telemetry on `OFTC`_. Mailinglist ----------- Project use http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss as the mailinglist. Please use tag ``[Ceilometer]`` in the subject for new threads. .. _OFTC: https://oftc.net/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1393569 ceilometer-27.0.0.0rc1/bindep.txt0000664000175000017500000000041615250267452015331 0ustar00zuulzuullibxml2-dev [platform:dpkg test] libxslt-devel [platform:rpm test] libxslt1-dev [platform:dpkg test] build-essential [platform:dpkg] libffi-dev [platform:dpkg] gettext [platform:dpkg] libvirt-dev [platform:dpkg test] libvirt-devel [platform:rpm test] pkg-config [test] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2321277 ceilometer-27.0.0.0rc1/ceilometer/0000775000175000017500000000000015250267567015465 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1403568 ceilometer-27.0.0.0rc1/ceilometer/__init__.py0000664000175000017500000000117015250267452017566 0ustar00zuulzuul# Copyright 2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class NotImplementedError(NotImplementedError): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1403568 ceilometer-27.0.0.0rc1/ceilometer/agent.py0000664000175000017500000000731715250267452017136 0ustar00zuulzuul# # Copyright 2013 Intel Corp. # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fnmatch import os from oslo_log import log import yaml LOG = log.getLogger(__name__) class ConfigException(Exception): def __init__(self, cfg_type, message, cfg): self.cfg_type = cfg_type self.msg = message self.cfg = cfg def __str__(self): return f'{self.cfg_type} {self.cfg}: {self.msg}' class SourceException(Exception): def __init__(self, message, cfg): self.msg = message self.cfg = cfg def __str__(self): return f'Source definition invalid: {self.msg} ({self.cfg})' class ConfigManagerBase: """Base class for managing configuration file refresh""" def __init__(self, conf): self.conf = conf def load_config(self, cfg_file): """Load a configuration file and set its refresh values.""" if os.path.exists(cfg_file): cfg_loc = cfg_file else: cfg_loc = self.conf.find_file(cfg_file) if not cfg_loc: LOG.debug("No pipeline definitions configuration file found! " "Using default config.") cfg_loc = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'pipeline', 'data', cfg_file) with open(cfg_loc) as fap: conf = yaml.safe_load(fap) LOG.debug("Config file: %s", conf) return conf class Source: """Represents a generic source""" def __init__(self, cfg): self.cfg = cfg try: self.name = cfg['name'] except KeyError as err: raise SourceException( "Required field %s not specified" % err.args[0], cfg) def __str__(self): return self.name def check_source_filtering(self, data, d_type): """Source data rules checking - At least one meaningful datapoint exist - Included type and excluded type can't co-exist on the same pipeline - Included type meter and wildcard can't co-exist at same pipeline """ if not data: raise SourceException('No %s specified' % d_type, self.cfg) if (any(x for x in data if x[0] not in '!*') and any(x for x in data if x[0] == '!')): raise SourceException( 'Both included and excluded %s specified' % d_type, self.cfg) if '*' in data and any(x for x in data if x[0] not in '!*'): raise SourceException( 'Included %s specified with wildcard' % d_type, self.cfg) @staticmethod def is_supported(dataset, data_name): # Support wildcard like storage.* and !disk.* # Start with negation, we consider that the order is deny, allow if any(fnmatch.fnmatch(data_name, datapoint[1:]) for datapoint in dataset if datapoint[0] == '!'): return False if any(fnmatch.fnmatch(data_name, datapoint) for datapoint in dataset if datapoint[0] != '!'): return True # if we only have negation, we suppose the default is allow return all(datapoint.startswith('!') for datapoint in dataset) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2341275 ceilometer-27.0.0.0rc1/ceilometer/alarm/0000775000175000017500000000000015250267567016561 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1403568 ceilometer-27.0.0.0rc1/ceilometer/alarm/__init__.py0000664000175000017500000000000015250267452020651 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1413567 ceilometer-27.0.0.0rc1/ceilometer/alarm/aodh.py0000664000175000017500000000405515250267452020043 0ustar00zuulzuul# # Copyright 2025 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Common code for working with alarm metrics """ from ceilometer.polling import plugin_base from ceilometer import sample DEFAULT_GROUP = "service_credentials" class _Base(plugin_base.PollsterBase): @property def default_discovery(self): return 'alarm' class EvaluationResultPollster(_Base): @staticmethod def get_evaluation_results_metrics(metrics): evaluation_metrics = [] if "evaluation_results" in metrics: for metric in metrics["evaluation_results"]: for state, count in metric["state_counters"].items(): evaluation_metrics.append({ "name": "evaluation_result", "state": state, "count": count, "project_id": metric['project_id'], "alarm_id": metric['alarm_id'] }) return evaluation_metrics def get_samples(self, manager, cache, resources): metrics = self.get_evaluation_results_metrics(resources[0]) for metric in metrics: yield sample.Sample( name='alarm.' + metric['name'], type=sample.TYPE_GAUGE, volume=int(metric['count']), unit='evaluation_result_count', user_id=None, project_id=metric['project_id'], resource_id=metric['alarm_id'], resource_metadata={"alarm_state": metric['state']}, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1413567 ceilometer-27.0.0.0rc1/ceilometer/alarm/discovery.py0000664000175000017500000000333615250267452021140 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from aodhclient import client as aodh_client from aodhclient import exceptions as aodh_exc from oslo_config import cfg from oslo_log import log from ceilometer import keystone_client from ceilometer.polling import plugin_base LOG = log.getLogger(__name__) SERVICE_OPTS = [ cfg.StrOpt('aodh', default='alarming', help='Aodh service type.'), ] class AlarmDiscovery(plugin_base.DiscoveryBase): def __init__(self, conf): super().__init__(conf) creds = conf.service_credentials self.aodh_client = aodh_client.Client( version='2', session=keystone_client.get_session(conf), region_name=creds.region_name, interface=creds.interface, service_type=conf.service_types.aodh) def discover(self, manager, param=None): """Discover resources to monitor.""" try: return [self.aodh_client.metrics.get(all_projects=True)] except aodh_exc.Forbidden as e: # NOTE(jwysogla): aodh will return 403 when the metric # endpoint is disabled in aodh.conf LOG.error('Skipping alarm discovery, aodh issue: %s', e) return [] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1413567 ceilometer-27.0.0.0rc1/ceilometer/cache_utils.py0000664000175000017500000000560015250267452020314 0ustar00zuulzuul# # Copyright 2022 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Simple wrapper for oslo_cache.""" from keystoneauth1 import exceptions as ka_exceptions from oslo_cache import core as cache from oslo_cache import exception from oslo_log import log from ceilometer import keystone_client # Default cache expiration period CACHE_DURATION = 600 LOG = log.getLogger(__name__) class CacheClient: def __init__(self, region, conf): self.region = region self.conf = conf def get(self, key): value = self.region.get(key) if value == cache.NO_VALUE: return None return value def set(self, key, value): return self.region.set(key, value) def delete(self, key): return self.region.delete(key) def resolve_uuid_from_cache(self, attr, uuid): resource_name = self.get(uuid) if resource_name: return resource_name else: # Retrieve project and user names from Keystone only # if ceilometer doesn't have a caching backend resource_name = self._resolve_uuid_from_keystone(attr, uuid) self.set(uuid, resource_name) return resource_name def _resolve_uuid_from_keystone(self, attr, uuid): try: return getattr( keystone_client.get_client(self.conf), attr ).get(uuid).name except AttributeError as e: LOG.warning("Found '%s' while resolving uuid %s to name", e, uuid) except ka_exceptions.NotFound as e: LOG.warning(e.message) def get_client(conf): cache.configure(conf) if conf.cache.enabled: region = get_cache_region(conf) if region: return CacheClient(region, conf) else: # configure oslo_cache.dict backend if # no caching backend is configured region = get_dict_cache_region() return CacheClient(region, conf) def get_dict_cache_region(): region = cache.create_region() region.configure('oslo_cache.dict', expiration_time=CACHE_DURATION) return region def get_cache_region(conf): # configure caching region using params from config try: region = cache.create_region() cache.configure_cache_region(conf, region) return region except exception.ConfigurationError as e: LOG.error("failed to configure oslo_cache: %s", str(e)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1413567 ceilometer-27.0.0.0rc1/ceilometer/cinder_client.py0000664000175000017500000000534215250267452020636 0ustar00zuulzuul# Copyright (C) 2026 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from openstack import connection from oslo_config import cfg from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('cinder', default='volumev3', help='Cinder service type.'), ] class Client: def __init__(self, conf): creds = conf.service_credentials # NOTE(mnederlof): We set 3.64 (the maximum for Wallaby) because: # we need at least 3.41 to get user_id on snapshots. # we need at least 3.56 for user_id and project_id on backups. # we need at least 3.63 for volume_type_id on volumes. self._conn = connection.Connection( block_storage_api_version='3.64', session=keystone_client.get_session(conf), oslo_conf=conf, region_name=creds.region_name, block_storage_interface=creds.interface, service_types={conf.service_types.cinder} ) def list_volumes(self, search_opts=None): search_opts = dict(search_opts or {}) all_projects = search_opts.pop('all_projects', True) return list( self._conn.block_storage.volumes( details=True, all_projects=all_projects, **search_opts)) def list_volume_snapshots(self, search_opts=None): search_opts = dict(search_opts or {}) all_projects = search_opts.pop('all_projects', True) return list( self._conn.block_storage.snapshots( details=True, all_projects=all_projects, **search_opts)) def list_backups(self, search_opts=None): search_opts = dict(search_opts or {}) all_projects = search_opts.pop('all_projects', True) return list( self._conn.block_storage.backups( details=True, all_projects=all_projects, **search_opts)) def list_pools(self): # openstacksdk hardcodes detail=True in the Pools resource URL, so # detailed capacity data is always returned. return list(self._conn.block_storage.backend_pools()) def list_services(self): return list(self._conn.block_storage.services()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2351277 ceilometer-27.0.0.0rc1/ceilometer/cmd/0000775000175000017500000000000015250267567016230 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1423566 ceilometer-27.0.0.0rc1/ceilometer/cmd/__init__.py0000664000175000017500000000000015250267452020320 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1423566 ceilometer-27.0.0.0rc1/ceilometer/cmd/agent_notification.py0000664000175000017500000000203315250267452022435 0ustar00zuulzuul# # Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cotyledon from cotyledon import oslo_config_glue from oslo_log import log from ceilometer import notification from ceilometer import service LOG = log.getLogger(__name__) def main(): conf = service.prepare_service() conf.log_opt_values(LOG, log.DEBUG) sm = cotyledon.ServiceManager() sm.add(notification.NotificationService, workers=conf.notification.workers, args=(conf,)) oslo_config_glue.setup(sm, conf) sm.run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1423566 ceilometer-27.0.0.0rc1/ceilometer/cmd/polling.py0000664000175000017500000000701715250267452020244 0ustar00zuulzuul# # Copyright 2014-2015 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import multiprocessing import shlex import cotyledon from cotyledon import oslo_config_glue from oslo_config import cfg from oslo_log import log from oslo_privsep import priv_context from ceilometer.polling import manager from ceilometer import service from ceilometer import utils LOG = log.getLogger(__name__) class MultiChoicesOpt(cfg.Opt): def __init__(self, name, choices=None, **kwargs): super().__init__( name, type=DeduplicatedCfgList(choices), **kwargs) self.choices = choices def _get_argparse_kwargs(self, group, **kwargs): """Extends the base argparse keyword dict for multi choices options.""" kwargs = super()._get_argparse_kwargs(group) kwargs['nargs'] = '+' choices = kwargs.get('choices', self.choices) if choices: kwargs['choices'] = choices return kwargs class DeduplicatedCfgList(cfg.types.List): def __init__(self, choices=None, **kwargs): super().__init__(**kwargs) self.choices = choices or [] def __call__(self, *args, **kwargs): result = super().__call__(*args, **kwargs) result_set = set(result) if len(result) != len(result_set): LOG.warning("Duplicated values: %s found in CLI options, " "auto de-duplicated", result) result = list(result_set) if self.choices and not (result_set <= set(self.choices)): raise Exception('Valid values are %s, but found %s' % (self.choices, result)) return result CLI_OPTS = [ MultiChoicesOpt('polling-namespaces', default=['compute', 'central'], dest='polling_namespaces', help='Polling namespace(s) to be used while ' 'resource polling') ] def _prepare_config(): conf = cfg.ConfigOpts() conf.register_cli_opts(CLI_OPTS) service.prepare_service(conf=conf) return conf def create_polling_service(worker_id, conf=None, queue=None): if conf is None: conf = _prepare_config() conf.log_opt_values(LOG, log.DEBUG) return manager.AgentManager(worker_id, conf, conf.polling_namespaces, queue) def create_heartbeat_service(worker_id, conf, queue=None): if conf is None: conf = _prepare_config() conf.log_opt_values(LOG, log.DEBUG) return manager.AgentHeartBeatManager(worker_id, conf, conf.polling_namespaces, queue) def main(): sm = cotyledon.ServiceManager() conf = _prepare_config() priv_context.init(root_helper=shlex.split(utils.get_root_helper(conf))) oslo_config_glue.setup(sm, conf) if conf.polling.heartbeat_socket_dir is not None: queue = multiprocessing.Queue() sm.add(create_heartbeat_service, args=(conf, queue)) else: queue = None sm.add(create_polling_service, args=(conf, queue)) sm.run() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1433568 ceilometer-27.0.0.0rc1/ceilometer/cmd/sample.py0000664000175000017500000000604515250267452020061 0ustar00zuulzuul# # Copyright 2012-2014 Julien Danjou # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Command line tool for creating meter for Ceilometer. """ import ast import logging import sys from oslo_config import cfg from oslo_utils import timeutils from ceilometer.pipeline import sample as sample_pipe from ceilometer import sample from ceilometer import service def send_sample(): conf = cfg.ConfigOpts() conf.register_cli_opts([ cfg.StrOpt('sample-name', short='n', help='Meter name.', required=True), cfg.StrOpt('sample-type', short='y', help='Meter type.', default=sample.TYPE_GAUGE, choices=sample.TYPES), cfg.StrOpt('sample-unit', short='U', help='Meter unit.'), cfg.IntOpt('sample-volume', short='l', help='Meter volume value.', default=1), cfg.StrOpt('sample-resource', short='r', help='Meter resource id.', required=True), cfg.StrOpt('sample-user', short='u', help='Meter user id.'), cfg.StrOpt('sample-project', short='p', help='Meter project id.'), cfg.StrOpt('sample-timestamp', short='i', help='Meter timestamp.', default=timeutils.utcnow().isoformat()), cfg.StrOpt('sample-metadata', short='m', help='Meter metadata.'), ]) service.prepare_service(conf=conf) # Set up logging to use the console console = logging.StreamHandler(sys.stderr) console.setLevel(logging.DEBUG) formatter = logging.Formatter('%(message)s') console.setFormatter(formatter) root_logger = logging.getLogger('') root_logger.addHandler(console) root_logger.setLevel(logging.DEBUG) pipeline_manager = sample_pipe.SamplePipelineManager(conf) with pipeline_manager.publisher() as p: p([sample.Sample( name=conf.sample_name, type=conf.sample_type, unit=conf.sample_unit, volume=conf.sample_volume, user_id=conf.sample_user, project_id=conf.sample_project, resource_id=conf.sample_resource, timestamp=conf.sample_timestamp, resource_metadata=conf.sample_metadata and ast.literal_eval( conf.sample_metadata))]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1433568 ceilometer-27.0.0.0rc1/ceilometer/cmd/status.py0000664000175000017500000000300615250267452020115 0ustar00zuulzuul# Copyright (c) 2018 NEC, Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys from oslo_config import cfg from oslo_upgradecheck import upgradecheck from ceilometer.i18n import _ CONF = cfg.CONF class Checks(upgradecheck.UpgradeCommands): """Contains upgrade checks Various upgrade checks should be added as separate methods in this class and added to _upgrade_checks tuple. """ def _sample_check(self): """This is sample check added to test the upgrade check framework It needs to be removed after adding any real upgrade check """ return upgradecheck.Result(upgradecheck.Code.SUCCESS, 'Sample detail') _upgrade_checks = ( # Sample check added for now. # Whereas in future real checks must be added here in tuple (_('Sample Check'), _sample_check), ) def main(): return upgradecheck.main( CONF, project='ceilometer', upgrade_command=Checks()) if __name__ == '__main__': sys.exit(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1433568 ceilometer-27.0.0.0rc1/ceilometer/cmd/storage.py0000664000175000017500000000357315250267452020247 0ustar00zuulzuul# # Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_config import cfg from oslo_log import log import tenacity from ceilometer import service LOG = log.getLogger(__name__) def upgrade(): conf = cfg.ConfigOpts() conf.register_cli_opts([ cfg.BoolOpt('skip-gnocchi-resource-types', help='Skip gnocchi resource-types upgrade.', default=False), cfg.IntOpt('retry', min=0, help='Number of times to retry on failure. ' 'Default is to retry forever.'), ]) service.prepare_service(conf=conf) if conf.skip_gnocchi_resource_types: LOG.info("Skipping Gnocchi resource types upgrade") else: LOG.debug("Upgrading Gnocchi resource types") from ceilometer import gnocchi_client from gnocchiclient import exceptions if conf.retry is None: stop = tenacity.stop_never else: stop = tenacity.stop_after_attempt(conf.retry) tenacity.Retrying( stop=stop, retry=tenacity.retry_if_exception_type(( exceptions.ConnectionFailure, exceptions.UnknownConnectionError, exceptions.ConnectionTimeout, exceptions.SSLError, )) )(gnocchi_client.upgrade_resource_types, conf) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2351277 ceilometer-27.0.0.0rc1/ceilometer/compute/0000775000175000017500000000000015250267567017141 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1433568 ceilometer-27.0.0.0rc1/ceilometer/compute/__init__.py0000664000175000017500000000000015250267452021231 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1443567 ceilometer-27.0.0.0rc1/ceilometer/compute/discovery.py0000664000175000017500000004033015250267452021513 0ustar00zuulzuul# # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import hashlib from lxml import etree import operator import threading import cachetools from novaclient import exceptions from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils from ceilometer.compute.virt.libvirt import utils as libvirt_utils from ceilometer import nova_client from ceilometer.polling import plugin_base OPTS = [ cfg.StrOpt('instance_discovery_method', default='libvirt_metadata', choices=[('naive', 'poll nova to get all instances'), ('workload_partitioning', 'poll nova to get instances of the compute'), ('libvirt_metadata', 'get instances from libvirt metadata but without ' 'instance metadata (recommended)')], help="Ceilometer offers many methods to discover the instance " "running on a compute node"), cfg.IntOpt('resource_update_interval', default=0, min=0, help="New instances will be discovered periodically based" " on this option (in seconds). By default, " "the agent discovers instances according to pipeline " "polling interval. If option is greater than 0, " "the instance list to poll will be updated based " "on this option's interval. Measurements relating " "to the instances will match intervals " "defined in pipeline. This option is only used " "for agent polling to Nova API, so it will work only " "when 'instance_discovery_method' is set to 'naive'."), cfg.IntOpt('resource_cache_expiry', default=3600, min=0, help="The expiry to totally refresh the instances resource " "cache, since the instance may be migrated to another " "host, we need to clean the legacy instances info in " "local cache by totally refreshing the local cache. " "The minimum should be the value of the config option " "of resource_update_interval. This option is only used " "for agent polling to Nova API, so it will work only " "when 'instance_discovery_method' is set to 'naive'."), cfg.BoolOpt('report_stopped_instance_metrics', default=False, help="Report a minimal set of metrics (power.state, vcpus, " "memory, disk.device.capacity, disk.device.usage) for " "stopped (SHUTOFF) instances. Only works with the " "'libvirt_metadata' instance discovery method."), cfg.BoolOpt('fetch_extra_metadata', default=False, help="Whether or not additional instance attributes that " "require Nova API queries should be fetched. Currently " "the only value that requires fetching from Nova API is " "'metadata', the attribute storing user-configured " "server metadata, which is used to fill out some " "optional fields such as the server group of an " "instance. When using Gnocchi this does not need to be " "enabled in most cases as notification meters also " "populate these fields in Gnocchi resources. When using " "Prometheus you may want to enable this if you need " "this metadata set on all samples (e.g. queries by " "server group, Aodh alarms or Heat auto-scaling)."), ] LOG = log.getLogger(__name__) class NovaLikeServer: def __init__(self, **kwargs): self.id = kwargs.pop('id') for k, v in kwargs.items(): setattr(self, k, v) def __repr__(self): return '' % getattr(self, 'name', 'unknown-name') def __eq__(self, other): return self.id == other.id class InstanceDiscovery(plugin_base.DiscoveryBase): method = None def __init__(self, conf): super().__init__(conf) if not self.method: self.method = conf.compute.instance_discovery_method self.nova_cli = nova_client.Client(conf) self.expiration_time = conf.compute.resource_update_interval self.cache_expiry = conf.compute.resource_cache_expiry if self.method == "libvirt_metadata": # 4096 resources on a compute should be enough :) self._flavor_id_cache = cachetools.LRUCache(4096) self._server_cache = cachetools.LRUCache(4096) else: self.lock = threading.Lock() self.instances = {} self.last_run = None self.last_cache_expire = None @property def connection(self): return libvirt_utils.refresh_libvirt_connection(self.conf, self) def discover(self, manager, param=None): """Discover resources to monitor.""" if self.method != "libvirt_metadata": return self.discover_nova_polling(manager, param=None) else: return self.discover_libvirt_polling(manager, param=None) @staticmethod def _safe_find_int(xml, path): elem = xml.find("./%s" % path) if elem is not None: return int(elem.text) return 0 def _get_flavor_id(self, flavor_xml, instance_id): flavor_name = flavor_xml.attrib["name"] # Flavor ID is available in libvirt metadata from 2025.2 onwards. flavor_id = flavor_xml.attrib.get("id") if flavor_id: return flavor_id # If not found in libvirt metadata, fallback to API queries. # If we already have the server metadata get the flavor ID from there. if self.conf.compute.fetch_extra_metadata: server = self.get_server(instance_id) if server: return server.flavor["id"] # If server metadata is not otherwise fetched, or the query failed, # query just the flavor for better cache hit rates. return (self.get_flavor_id(flavor_name) or flavor_name) def _get_flavor_extra_specs(self, flavor_xml): # Extra specs are available in libvirt metadata from 2025.2 onwards. # Note that this checks for existence of the element, not whether # or not it is empty, as it *can* exist but have nothing set in it. extra_specs = flavor_xml.find("./extraSpecs") if extra_specs is not None: return { extra_spec.attrib["name"]: extra_spec.text for extra_spec in extra_specs.findall("./extraSpec")} # If not found in libvirt metadata, return None to signify # "not fetched", as we don't support performing additional # API queries just for the extra specs. return None @cachetools.cachedmethod(operator.attrgetter('_flavor_id_cache')) def get_flavor_id(self, name): LOG.debug("Querying metadata for flavor %s from Nova API", name) try: return self.nova_cli.nova_client.flavors.find( name=name, is_public=None).id except exceptions.NotFound: return None @cachetools.cachedmethod(operator.attrgetter('_server_cache')) def get_server(self, uuid): LOG.debug("Querying metadata for instance %s from Nova API", uuid) try: return self.nova_cli.nova_client.servers.get(uuid) except exceptions.NotFound: return None @libvirt_utils.retry_on_disconnect def discover_libvirt_polling(self, manager, param=None): instances = [] for domain in self.connection.listAllDomains(): instance_id = domain.UUIDString() xml_string = libvirt_utils.instance_metadata(domain) if xml_string is None: continue full_xml = etree.fromstring(domain.XMLDesc()) os_type_xml = full_xml.find("./os/type") metadata_xml = etree.fromstring(xml_string) try: flavor_xml = metadata_xml.find( "./flavor") user_id = metadata_xml.find( "./owner/user").attrib["uuid"] project_id = metadata_xml.find( "./owner/project").attrib["uuid"] instance_name = metadata_xml.find( "./name").text instance_arch = os_type_xml.attrib["arch"] extra_specs = self._get_flavor_extra_specs(flavor_xml) flavor = { "id": self._get_flavor_id(flavor_xml, instance_id), "name": flavor_xml.attrib["name"], "vcpus": self._safe_find_int(flavor_xml, "vcpus"), "ram": self._safe_find_int(flavor_xml, "memory"), "disk": self._safe_find_int(flavor_xml, "disk"), "ephemeral": self._safe_find_int(flavor_xml, "ephemeral"), "swap": self._safe_find_int(flavor_xml, "swap"), } if extra_specs is not None: flavor["extra_specs"] = extra_specs image_xml = metadata_xml.find("./root[@type='image']") image = ({'id': image_xml.attrib['uuid']} if image_xml is not None else None) image_meta_xml = metadata_xml.find("./image") if image_meta_xml is not None: # If the element exists at all, Nova supports # image_meta in libvirt metadata. Add it to the instance # attributes even if all the required values are empty. image_meta = {} base_image_ref = image_meta_xml.attrib.get("uuid") if base_image_ref is not None: image_meta["base_image_ref"] = base_image_ref # The following properties get special treatment # because they are set as such in SM_INHERITABLE_KEYS, # as defined in nova/utils.py. container_format_xml = image_meta_xml.find( "./containerFormat") if container_format_xml is not None: image_meta["container_format"] = ( container_format_xml.text) disk_format_xml = image_meta_xml.find("./diskFormat") if disk_format_xml is not None: image_meta["disk_format"] = disk_format_xml.text min_disk_xml = image_meta_xml.find("./minDisk") if min_disk_xml is not None: image_meta["min_disk"] = min_disk_xml.text min_ram_xml = image_meta_xml.find("./minRam") if min_ram_xml is not None: image_meta["min_ram"] = min_ram_xml.text # Get additional properties defined in image_meta. properties_xml = image_meta_xml.find("./properties") if properties_xml is not None: for prop in properties_xml.findall("./property"): image_meta[prop.attrib["name"]] = prop.text else: # None for "no image_meta found". image_meta = None # Getting the server metadata requires expensive Nova API # queries, and may potentially contain sensitive user info, # so it is only fetched when configured to do so. if self.conf.compute.fetch_extra_metadata: server = self.get_server(instance_id) metadata = server.metadata if server is not None else {} else: metadata = {} except AttributeError: LOG.error( "Fail to get domain uuid %s metadata: " "metadata was missing expected attributes", instance_id) continue dom_state = domain.state()[0] vm_state = libvirt_utils.LIBVIRT_POWER_STATE.get(dom_state) status = libvirt_utils.LIBVIRT_STATUS.get(dom_state) # From: # https://github.com/openstack/nova/blob/852f40fd0c6e9d8878212ff3120556668023f1c4/nova/api/openstack/compute/views/servers.py#L214-L220 host_id = hashlib.sha224( (project_id + self.conf.host).encode('utf-8')).hexdigest() instance_data = { "id": instance_id, "name": instance_name, "flavor": flavor, "image": image, "os_type": os_type_xml.text, "architecture": instance_arch, "OS-EXT-SRV-ATTR:instance_name": domain.name(), "OS-EXT-SRV-ATTR:host": self.conf.host, "OS-EXT-STS:vm_state": vm_state, "tenant_id": project_id, "user_id": user_id, "hostId": host_id, "status": status, # NOTE(sileht): Other fields that Ceilometer tracks # where we can't get the value here, but their are # retrieved by notification "metadata": metadata, # "OS-EXT-STS:task_state" # 'reservation_id', # 'OS-EXT-AZ:availability_zone', # 'kernel_id', # 'ramdisk_id', # some image detail } if image_meta is not None: instance_data["image_meta"] = image_meta LOG.debug("instance data: %s", instance_data) instances.append(NovaLikeServer(**instance_data)) return instances def discover_nova_polling(self, manager, param=None): secs_from_last_update = 0 utc_now = timeutils.utcnow(True) secs_from_last_expire = 0 if self.last_run: secs_from_last_update = timeutils.delta_seconds( self.last_run, utc_now) if self.last_cache_expire: secs_from_last_expire = timeutils.delta_seconds( self.last_cache_expire, utc_now) instances = [] # NOTE(ityaptin) we update make a nova request only if # it's a first discovery or resources expired with self.lock: if (not self.last_run or secs_from_last_update >= self.expiration_time): try: if (secs_from_last_expire < self.cache_expiry and self.last_run): since = self.last_run.isoformat() else: since = None self.instances.clear() self.last_cache_expire = utc_now instances = self.nova_cli.instance_get_all_by_host( self.conf.host, since) self.last_run = utc_now except Exception: # NOTE(zqfan): instance_get_all_by_host is wrapped and will # log exception when there is any error. It is no need to # raise it again and print one more time. return [] for instance in instances: if getattr(instance, 'OS-EXT-STS:vm_state', None) in [ 'deleted', 'error']: self.instances.pop(instance.id, None) else: self.instances[instance.id] = instance return self.instances.values() @property def group_id(self): return self.conf.host ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2361276 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/0000775000175000017500000000000015250267567021170 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1453567 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/__init__.py0000664000175000017500000001746415250267452023306 0ustar00zuulzuul# Copyright 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections from time import monotonic as now from oslo_log import log from oslo_utils import timeutils import ceilometer from ceilometer.compute.pollsters import util from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) class NoVolumeException(Exception): pass class GenericComputePollster(plugin_base.PollsterBase): """This class aims to cache instance statistics data First polled pollsters that inherit of this will retrieve and cache stats of an instance, then other pollsters will just build the samples without queyring the backend anymore. """ sample_name = None sample_unit = '' sample_type = sample.TYPE_GAUGE sample_stats_key = None inspector_method = None def setup_environment(self): super().setup_environment() self.inspector = GenericComputePollster._get_inspector(self.conf) @staticmethod def aggregate_method(stats): # Don't aggregate anything by default return stats @staticmethod def _get_inspector(conf): # FIXME(sileht): This doesn't looks threadsafe... try: inspector = GenericComputePollster._inspector except AttributeError: inspector = virt_inspector.get_hypervisor_inspector(conf) GenericComputePollster._inspector = inspector return inspector @property def default_discovery(self): return 'local_instances' def _record_poll_time(self): """Method records current time as the poll time. :return: time in seconds since the last poll time was recorded """ current_time = timeutils.utcnow() duration = None if hasattr(self, '_last_poll_time'): duration = timeutils.delta_seconds(self._last_poll_time, current_time) self._last_poll_time = current_time return duration @staticmethod def get_additional_metadata(instance, stats): pass @staticmethod def get_resource_id(instance, stats): return instance.id def _inspect_cached(self, cache, instance, duration): cache.setdefault(self.inspector_method, {}) if instance.id not in cache[self.inspector_method]: result = getattr(self.inspector, self.inspector_method)( instance, duration) polled_time = now() # Ensure we don't cache an iterator if isinstance(result, collections.abc.Iterable): result = list(result) else: result = [result] cache[self.inspector_method][instance.id] = (polled_time, result) return cache[self.inspector_method][instance.id] def _stats_to_sample(self, instance, stats, polled_time): volume = getattr(stats, self.sample_stats_key) LOG.debug( "%(instance_id)s/%(name)s volume: %(volume)s", {'name': self.sample_name, 'instance_id': instance.id, 'volume': (volume if volume is not None else 'Unavailable')}) if volume is None: raise NoVolumeException() return util.make_sample_from_instance( self.conf, instance, name=self.sample_name, unit=self.sample_unit, type=self.sample_type, resource_id=self.get_resource_id(instance, stats), volume=volume, additional_metadata=self.get_additional_metadata( instance, stats), monotonic_time=polled_time, ) def get_samples(self, manager, cache, resources): self._inspection_duration = self._record_poll_time() for instance in resources: try: polled_time, result = self._inspect_cached( cache, instance, self._inspection_duration) if not result: continue for stats in self.aggregate_method(result): yield self._stats_to_sample(instance, stats, polled_time) except NoVolumeException: # FIXME(sileht): This should be a removed... but I will # not change the test logic for now LOG.warning("%(name)s statistic in not available for " "instance %(instance_id)s", {'name': self.sample_name, 'instance_id': instance.id}) except virt_inspector.InstanceNotFoundException as err: # Instance was deleted while getting samples. Ignore it. LOG.debug('Exception while getting samples %s', err) except virt_inspector.InstanceShutOffException as e: LOG.debug('Instance %(instance_id)s was shut off while ' 'getting sample of %(name)s: %(exc)s', {'instance_id': instance.id, 'name': self.sample_name, 'exc': e}) except virt_inspector.NoDataException as e: LOG.warning('Cannot inspect data of %(pollster)s for ' '%(instance_id)s, non-fatal reason: %(exc)s', {'pollster': self.__class__.__name__, 'instance_id': instance.id, 'exc': e}) except ceilometer.NotImplementedError: # Selected inspector does not implement this pollster. LOG.debug('%(inspector)s does not provide data for ' '%(pollster)s', {'inspector': self.inspector.__class__.__name__, 'pollster': self.__class__.__name__}) raise plugin_base.PollsterPermanentError(resources) except Exception as err: LOG.exception( 'Could not get %(name)s events for %(id)s: %(e)s', { 'name': self.sample_name, 'id': instance.id, 'e': err}) class InstanceMetadataPollster(plugin_base.PollsterBase): """A base class for implementing a pollster using instance metadata. This metadata is originally supplied by Nova, but if instance_discovery_method is set to libvirt_metadata, metadata is fetched from the local libvirt socket, just like with the standard compute pollsters. """ sample_name = None sample_unit = '' sample_type = sample.TYPE_GAUGE @property def default_discovery(self): return 'local_instances' def get_resource_id(self, instance): return instance.id def get_volume(self, instance): raise ceilometer.NotImplementedError def get_additional_metadata(self, instance): return {} def get_samples(self, manager, cache, resources): for instance in resources: yield util.make_sample_from_instance( self.conf, instance, name=self.sample_name, unit=self.sample_unit, type=self.sample_type, resource_id=self.get_resource_id(instance), volume=self.get_volume(instance), additional_metadata=self.get_additional_metadata(instance), monotonic_time=now(), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1453567 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/disk.py0000664000175000017500000000651115250267452022470 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # Copyright 2014 Cisco Systems, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.compute import pollsters from ceilometer import sample class PerDeviceDiskPollster(pollsters.GenericComputePollster): inspector_method = "inspect_disks" @staticmethod def get_resource_id(instance, stats): return f"{instance.id}-{stats.device}" @staticmethod def get_additional_metadata(instance, stats): return {'disk_name': stats.device} class PerDeviceReadRequestsPollster(PerDeviceDiskPollster): sample_name = 'disk.device.read.requests' sample_unit = 'request' sample_type = sample.TYPE_CUMULATIVE sample_stats_key = 'read_requests' class PerDeviceReadBytesPollster(PerDeviceDiskPollster): sample_name = 'disk.device.read.bytes' sample_unit = 'B' sample_type = sample.TYPE_CUMULATIVE sample_stats_key = 'read_bytes' class PerDeviceWriteRequestsPollster(PerDeviceDiskPollster): sample_name = 'disk.device.write.requests' sample_unit = 'request' sample_type = sample.TYPE_CUMULATIVE sample_stats_key = 'write_requests' class PerDeviceWriteBytesPollster(PerDeviceDiskPollster): sample_name = 'disk.device.write.bytes' sample_unit = 'B' sample_type = sample.TYPE_CUMULATIVE sample_stats_key = 'write_bytes' class PerDeviceCapacityPollster(PerDeviceDiskPollster): inspector_method = 'inspect_disk_info' sample_name = 'disk.device.capacity' sample_unit = 'B' sample_stats_key = 'capacity' class PerDeviceAllocationPollster(PerDeviceDiskPollster): inspector_method = 'inspect_disk_info' sample_name = 'disk.device.allocation' sample_unit = 'B' sample_stats_key = 'allocation' class PerDevicePhysicalPollster(PerDeviceDiskPollster): inspector_method = 'inspect_disk_info' sample_name = 'disk.device.usage' sample_unit = 'B' sample_stats_key = 'physical' class PerDeviceDiskReadLatencyPollster(PerDeviceDiskPollster): sample_name = 'disk.device.read.latency' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'ns' sample_stats_key = 'rd_total_times' class PerDeviceDiskWriteLatencyPollster(PerDeviceDiskPollster): sample_name = 'disk.device.write.latency' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'ns' sample_stats_key = 'wr_total_times' class EphemeralSizePollster(pollsters.InstanceMetadataPollster): sample_name = 'disk.ephemeral.size' sample_unit = 'GiB' def get_volume(self, instance): return int(instance.flavor['ephemeral']) class RootSizePollster(pollsters.InstanceMetadataPollster): sample_name = 'disk.root.size' sample_unit = 'GiB' def get_volume(self, instance): return (int(instance.flavor['disk']) - int(instance.flavor['ephemeral'])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1463568 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/instance_stats.py0000664000175000017500000000546615250267452024570 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.compute import pollsters from ceilometer import sample class InstanceStatsPollster(pollsters.GenericComputePollster): inspector_method = 'inspect_instance' class PowerStatePollster(InstanceStatsPollster): sample_name = 'power.state' sample_stats_key = 'power_state' class CPUPollster(InstanceStatsPollster): sample_name = 'cpu' sample_unit = 'ns' sample_stats_key = 'cpu_time' sample_type = sample.TYPE_CUMULATIVE @staticmethod def get_additional_metadata(instance, c_data): return {'cpu_number': c_data.cpu_number} class VCPUsPollster(InstanceStatsPollster): sample_name = 'vcpus' sample_unit = 'vcpu' sample_stats_key = 'cpu_number' class MemoryPollster(InstanceStatsPollster): sample_name = 'memory' sample_unit = 'MiB' sample_stats_key = 'memory_actual' class MemoryAvailablePollster(InstanceStatsPollster): sample_name = 'memory.available' sample_unit = 'MiB' sample_stats_key = 'memory_available' class MemoryUsagePollster(InstanceStatsPollster): sample_name = 'memory.usage' sample_unit = 'MiB' sample_stats_key = 'memory_usage' class MemoryResidentPollster(InstanceStatsPollster): sample_name = 'memory.resident' sample_unit = 'MiB' sample_stats_key = 'memory_resident' class MemorySwapInPollster(InstanceStatsPollster): sample_name = 'memory.swap.in' sample_unit = 'MiB' sample_stats_key = 'memory_swap_in' sample_type = sample.TYPE_CUMULATIVE class MemorySwapOutPollster(InstanceStatsPollster): sample_name = 'memory.swap.out' sample_unit = 'MiB' sample_stats_key = 'memory_swap_out' sample_type = sample.TYPE_CUMULATIVE class PerfCPUCyclesPollster(InstanceStatsPollster): sample_name = 'perf.cpu.cycles' sample_stats_key = 'cpu_cycles' class PerfInstructionsPollster(InstanceStatsPollster): sample_name = 'perf.instructions' sample_stats_key = 'instructions' class PerfCacheReferencesPollster(InstanceStatsPollster): sample_name = 'perf.cache.references' sample_stats_key = 'cache_references' class PerfCacheMissesPollster(InstanceStatsPollster): sample_name = 'perf.cache.misses' sample_stats_key = 'cache_misses' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1463568 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/net.py0000664000175000017500000000745315250267452022332 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.compute import pollsters from ceilometer.compute.pollsters import util from ceilometer import sample class NetworkPollster(pollsters.GenericComputePollster): inspector_method = "inspect_vnics" @staticmethod def get_additional_metadata(instance, stats): additional_stats = {k: getattr(stats, k) for k in ["name", "mac", "fref", "parameters"]} if stats.fref is not None: additional_stats['vnic_name'] = stats.fref else: additional_stats['vnic_name'] = stats.name return additional_stats @staticmethod def get_resource_id(instance, stats): if stats.fref is not None: return stats.fref else: instance_name = util.instance_name(instance) return f"{instance_name}-{instance.id}-{stats.name}" class IncomingBytesPollster(NetworkPollster): sample_name = 'network.incoming.bytes' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'B' sample_stats_key = 'rx_bytes' class IncomingPacketsPollster(NetworkPollster): sample_name = 'network.incoming.packets' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'rx_packets' class OutgoingBytesPollster(NetworkPollster): sample_name = 'network.outgoing.bytes' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'B' sample_stats_key = 'tx_bytes' class OutgoingPacketsPollster(NetworkPollster): sample_name = 'network.outgoing.packets' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'tx_packets' class IncomingBytesRatePollster(NetworkPollster): inspector_method = "inspect_vnic_rates" sample_name = 'network.incoming.bytes.rate' sample_unit = 'B/s' sample_stats_key = 'rx_bytes_rate' class OutgoingBytesRatePollster(NetworkPollster): inspector_method = "inspect_vnic_rates" sample_name = 'network.outgoing.bytes.rate' sample_unit = 'B/s' sample_stats_key = 'tx_bytes_rate' class IncomingDropPollster(NetworkPollster): sample_name = 'network.incoming.packets.drop' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'rx_drop' class OutgoingDropPollster(NetworkPollster): sample_name = 'network.outgoing.packets.drop' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'tx_drop' class IncomingErrorsPollster(NetworkPollster): sample_name = 'network.incoming.packets.error' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'rx_errors' class OutgoingErrorsPollster(NetworkPollster): sample_name = 'network.outgoing.packets.error' sample_type = sample.TYPE_CUMULATIVE sample_unit = 'packet' sample_stats_key = 'tx_errors' class IncomingBytesDeltaPollster(NetworkPollster): sample_name = 'network.incoming.bytes.delta' sample_type = sample.TYPE_DELTA sample_unit = 'B' sample_stats_key = 'rx_bytes_delta' class OutgoingBytesDeltaPollster(NetworkPollster): sample_name = 'network.outgoing.bytes.delta' sample_type = sample.TYPE_DELTA sample_unit = 'B' sample_stats_key = 'tx_bytes_delta' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1473567 ceilometer-27.0.0.0rc1/ceilometer/compute/pollsters/util.py0000664000175000017500000000706515250267452022520 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import sample INSTANCE_PROPERTIES = [ # Identity properties 'reservation_id', # Type properties 'architecture', 'OS-EXT-AZ:availability_zone', 'kernel_id', 'os_type', 'ramdisk_id', ] def _get_metadata_from_object(conf, instance): """Return a metadata dictionary for the instance.""" instance_type = instance.flavor['name'] if instance.flavor else None metadata = { 'display_name': instance.name, 'name': getattr(instance, 'OS-EXT-SRV-ATTR:instance_name', ''), 'instance_id': instance.id, 'instance_type': instance_type, 'host': instance.hostId, 'instance_host': getattr(instance, 'OS-EXT-SRV-ATTR:host', ''), 'flavor': instance.flavor, 'status': instance.status.lower(), 'state': getattr(instance, 'OS-EXT-STS:vm_state', ''), 'task_state': getattr(instance, 'OS-EXT-STS:task_state', ''), } # Image properties if instance.image: metadata['image'] = instance.image metadata['image_ref'] = instance.image['id'] # Images that come through the conductor API in the nova notifier # plugin will not have links. if instance.image.get('links'): metadata['image_ref_url'] = instance.image['links'][0]['href'] else: metadata['image_ref_url'] = None else: metadata['image'] = None metadata['image_ref'] = None metadata['image_ref_url'] = None if hasattr(instance, 'image_meta') and instance.image_meta: metadata['image_meta'] = instance.image_meta for name in INSTANCE_PROPERTIES: if hasattr(instance, name): metadata[name] = getattr(instance, name) metadata['vcpus'] = instance.flavor['vcpus'] metadata['memory_mb'] = instance.flavor['ram'] metadata['disk_gb'] = instance.flavor['disk'] metadata['ephemeral_gb'] = instance.flavor['ephemeral'] metadata['root_gb'] = (int(metadata['disk_gb']) - int(metadata['ephemeral_gb'])) return sample.add_reserved_user_metadata(conf, instance.metadata, metadata) def make_sample_from_instance(conf, instance, name, type, unit, volume, resource_id=None, additional_metadata=None, monotonic_time=None): additional_metadata = additional_metadata or {} resource_metadata = _get_metadata_from_object(conf, instance) resource_metadata.update(additional_metadata) return sample.Sample( name=name, type=type, unit=unit, volume=volume, user_id=instance.user_id, project_id=instance.tenant_id, resource_id=resource_id or instance.id, resource_metadata=resource_metadata, monotonic_time=monotonic_time, ) def instance_name(instance): """Shortcut to get instance name.""" return getattr(instance, 'OS-EXT-SRV-ATTR:instance_name', None) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2371275 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/0000775000175000017500000000000015250267567020125 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1473567 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/__init__.py0000664000175000017500000000000015250267452022215 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1483567 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/inspector.py0000664000175000017500000002062615250267452022504 0ustar00zuulzuul# # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Inspector abstraction for read-only access to hypervisors.""" import collections from oslo_config import cfg from oslo_log import log from stevedore import driver import ceilometer OPTS = [ cfg.StrOpt('hypervisor_inspector', default='libvirt', choices=['libvirt'], deprecated_for_removal=True, deprecated_reason='libvirt is the only supported hypervisor', help='Inspector to use for inspecting the hypervisor layer.') ] LOG = log.getLogger(__name__) # Named tuple representing instance statistics class InstanceStats: fields = [ 'power_state', # the power state of the domain 'cpu_number', # number: number of CPUs 'cpu_time', # time: cumulative CPU time 'memory_actual', # actual: Amount of allocated memory 'memory_available', # available: Amount of usable memory 'memory_usage', # usage: Amount of memory used 'memory_resident', # 'memory_swap_in', # memory swap in 'memory_swap_out', # memory swap out 'cpu_cycles', # cpu_cycles: the number of cpu cycles one # instruction needs 'instructions', # instructions: the count of instructions 'cache_references', # cache_references: the count of cache hits 'cache_misses', # cache_misses: the count of caches misses ] def __init__(self, **kwargs): for k in self.fields: setattr(self, k, kwargs.pop(k, None)) if kwargs: raise AttributeError( "'InstanceStats' object has no attributes '%s'" % kwargs) # Named tuple representing vNIC statistics. # # name: the name of the vNIC # mac: the MAC address # fref: the filter ref # parameters: miscellaneous parameters # rx_bytes: number of received bytes # rx_packets: number of received packets # tx_bytes: number of transmitted bytes # tx_packets: number of transmitted packets # InterfaceStats = collections.namedtuple('InterfaceStats', ['name', 'mac', 'fref', 'parameters', 'rx_bytes', 'tx_bytes', 'rx_packets', 'tx_packets', 'rx_drop', 'tx_drop', 'rx_errors', 'tx_errors', 'rx_bytes_delta', 'tx_bytes_delta']) # Named tuple representing vNIC rate statistics. # # name: the name of the vNIC # mac: the MAC address # fref: the filter ref # parameters: miscellaneous parameters # rx_bytes_rate: rate of received bytes # tx_bytes_rate: rate of transmitted bytes # InterfaceRateStats = collections.namedtuple('InterfaceRateStats', ['name', 'mac', 'fref', 'parameters', 'rx_bytes_rate', 'tx_bytes_rate']) # Named tuple representing disk statistics. # # read_bytes: number of bytes read # read_requests: number of read operations # write_bytes: number of bytes written # write_requests: number of write operations # errors: number of errors # DiskStats = collections.namedtuple('DiskStats', ['device', 'read_bytes', 'read_requests', 'write_bytes', 'write_requests', 'errors', 'wr_total_times', 'rd_total_times']) # Named tuple representing disk rate statistics. # # read_bytes_rate: number of bytes read per second # read_requests_rate: number of read operations per second # write_bytes_rate: number of bytes written per second # write_requests_rate: number of write operations per second # DiskRateStats = collections.namedtuple('DiskRateStats', ['device', 'read_bytes_rate', 'read_requests_rate', 'write_bytes_rate', 'write_requests_rate']) # Named tuple representing disk Information. # # capacity: capacity of the disk # allocation: allocation of the disk # physical: usage of the disk DiskInfo = collections.namedtuple('DiskInfo', ['device', 'capacity', 'allocation', 'physical']) # Exception types # class InspectorException(Exception): def __init__(self, message=None): super().__init__(message) class InstanceNotFoundException(InspectorException): pass class InstanceShutOffException(InspectorException): pass class NoDataException(InspectorException): pass # Main virt inspector abstraction layering over the hypervisor API. # class Inspector: def __init__(self, conf): self.conf = conf def inspect_instance(self, instance, duration): """Inspect the CPU statistics for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: the instance stats """ raise ceilometer.NotImplementedError def inspect_vnics(self, instance, duration): """Inspect the vNIC statistics for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: for each vNIC, the number of bytes & packets received and transmitted """ raise ceilometer.NotImplementedError def inspect_vnic_rates(self, instance, duration): """Inspect the vNIC rate statistics for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: for each vNIC, the rate of bytes & packets received and transmitted """ raise ceilometer.NotImplementedError def inspect_disks(self, instance, duration): """Inspect the disk statistics for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: for each disk, the number of bytes & operations read and written, and the error count """ raise ceilometer.NotImplementedError def inspect_disk_rates(self, instance, duration): """Inspect the disk statistics as rates for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: for each disk, the number of bytes & operations read and written per second, with the error count """ raise ceilometer.NotImplementedError def inspect_disk_info(self, instance, duration): """Inspect the disk information for an instance. :param instance: the target instance :param duration: the last 'n' seconds, over which the value should be inspected :return: for each disk , capacity , allocation and usage """ raise ceilometer.NotImplementedError def get_hypervisor_inspector(conf): try: namespace = 'ceilometer.compute.virt' mgr = driver.DriverManager(namespace, conf.hypervisor_inspector, invoke_on_load=True, invoke_args=(conf, )) return mgr.driver except ImportError as e: LOG.error("Unable to load the hypervisor inspector: %s", e) return Inspector(conf) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2371275 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/libvirt/0000775000175000017500000000000015250267567021600 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1483567 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/libvirt/__init__.py0000664000175000017500000000000015250267452023670 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1483567 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/libvirt/inspector.py0000664000175000017500000002741615250267452024163 0ustar00zuulzuul# # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Implementation of Inspector abstraction for libvirt.""" from lxml import etree from oslo_log import log as logging from oslo_utils import units try: import libvirt except ImportError: libvirt = None from ceilometer.compute.pollsters import util from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.compute.virt.libvirt import utils as libvirt_utils from ceilometer.i18n import _ LOG = logging.getLogger(__name__) class LibvirtInspector(virt_inspector.Inspector): def __init__(self, conf): super().__init__(conf) # NOTE(sileht): create a connection on startup self.connection self.cache = {} @property def connection(self): return libvirt_utils.refresh_libvirt_connection(self.conf, self) def _lookup_by_uuid(self, instance): instance_name = util.instance_name(instance) try: return self.connection.lookupByUUIDString(instance.id) except libvirt.libvirtError as ex: if libvirt_utils.is_disconnection_exception(ex): raise msg = _("Error from libvirt while looking up instance " ": " "[Error Code %(error_code)s] " "%(ex)s") % {'name': instance_name, 'id': instance.id, 'error_code': ex.get_error_code(), 'ex': ex} raise virt_inspector.InstanceNotFoundException(msg) except Exception as ex: raise virt_inspector.InspectorException(str(ex)) def _get_domain(self, instance, raise_if_shutoff): instance_name = util.instance_name(instance) domain = self._lookup_by_uuid(instance) if raise_if_shutoff: state = domain.info()[0] if state == libvirt.VIR_DOMAIN_SHUTOFF: msg = _('Failed to inspect data of instance ' ', ' 'domain state is SHUTOFF.') % { 'name': instance_name, 'id': instance.id} raise virt_inspector.InstanceShutOffException(msg) return domain @libvirt_utils.retry_on_disconnect def inspect_vnics(self, instance, duration): domain = self._get_domain(instance, True) tree = etree.fromstring(domain.XMLDesc(0)) for iface in tree.findall('devices/interface'): target = iface.find('target') if target is not None: name = target.get('dev') else: continue mac = iface.find('mac') if mac is not None: mac_address = mac.get('address') else: continue fref = iface.find('filterref') if fref is not None: fref = fref.get('filter') params = {p.get('name').lower(): p.get('value') for p in iface.findall('filterref/parameter')} # Extract interface ID try: interfaceid = iface.find('virtualport').find( 'parameters').get('interfaceid') except AttributeError: interfaceid = None # Extract source bridge try: bridge = iface.find('source').get('bridge') except AttributeError: bridge = None params['interfaceid'] = interfaceid params['bridge'] = bridge try: dom_stats = domain.interfaceStats(name) except libvirt.libvirtError as ex: LOG.warning("Error from libvirt when running interfaceStats, " "This may not be harmful, but please check : " "%(ex)s", {'ex': ex}) continue # Retrieve previous values prev = self.cache.get(name) # Store values for next call self.cache[name] = dom_stats if prev: # Compute stats rx_delta = dom_stats[0] - prev[0] tx_delta = dom_stats[4] - prev[4] # Avoid negative values if rx_delta < 0: rx_delta = dom_stats[0] if tx_delta < 0: tx_delta = dom_stats[4] else: LOG.debug('No delta meter predecessor for %s / %s', instance.id, name) rx_delta = 0 tx_delta = 0 yield virt_inspector.InterfaceStats(name=name, mac=mac_address, fref=fref, parameters=params, rx_bytes=dom_stats[0], rx_packets=dom_stats[1], rx_errors=dom_stats[2], rx_drop=dom_stats[3], rx_bytes_delta=rx_delta, tx_bytes=dom_stats[4], tx_packets=dom_stats[5], tx_errors=dom_stats[6], tx_drop=dom_stats[7], tx_bytes_delta=tx_delta) @staticmethod def _get_disk_devices(domain): tree = etree.fromstring(domain.XMLDesc(0)) return filter(bool, [target.get("dev") for target in tree.findall('devices/disk/target') if target.getparent().find('source') is not None]) @libvirt_utils.retry_on_disconnect def inspect_disks(self, instance, duration): domain = self._get_domain(instance, True) for device in self._get_disk_devices(domain): try: block_stats = domain.blockStats(device) block_stats_flags = domain.blockStatsFlags(device, 0) yield virt_inspector.DiskStats( device=device, read_requests=block_stats[0], read_bytes=block_stats[1], write_requests=block_stats[2], write_bytes=block_stats[3], errors=block_stats[4], wr_total_times=block_stats_flags['wr_total_times'], rd_total_times=block_stats_flags['rd_total_times']) except libvirt.libvirtError as ex: # raised error even if lock is acquired while live migration, # even it looks normal. LOG.warning("Error from libvirt while checking blockStats, " "This may not be harmful, but please check : " "%(ex)s", {'ex': ex}) pass @libvirt_utils.retry_on_disconnect def inspect_disk_info(self, instance, duration): domain = self._get_domain( instance, not self.conf.compute.report_stopped_instance_metrics) for device in self._get_disk_devices(domain): block_info = domain.blockInfo(device) # if vm mount cdrom, libvirt will align by 4K bytes, capacity may # be smaller than physical, avoid with this. # https://libvirt.org/html/libvirt-libvirt-domain.html disk_capacity = max(block_info[0], block_info[2]) yield virt_inspector.DiskInfo(device=device, capacity=disk_capacity, allocation=block_info[1], physical=block_info[2]) @libvirt_utils.raise_nodata_if_unsupported @libvirt_utils.retry_on_disconnect def inspect_instance(self, instance, duration=None): domain = self._get_domain( instance, not self.conf.compute.report_stopped_instance_metrics) if self.conf.compute.report_stopped_instance_metrics: dom_info = domain.info() state = dom_info[0] if state == libvirt.VIR_DOMAIN_SHUTOFF: return virt_inspector.InstanceStats( power_state=state, cpu_number=dom_info[3], memory_actual=dom_info[1] / units.Ki, ) memory_actual = None memory_available = None memory_used = memory_resident = None memory_swap_in = memory_swap_out = None memory_stats = domain.memoryStats() # Stat provided from libvirt is in KiB, converting it to MiB. if 'actual' in memory_stats: memory_actual = memory_stats['actual'] / units.Ki if 'available' in memory_stats: memory_available = memory_stats['available'] / units.Ki if 'usable' in memory_stats and 'available' in memory_stats: memory_used = (memory_stats['available'] - memory_stats['usable']) / units.Ki elif 'available' in memory_stats and 'unused' in memory_stats: memory_used = (memory_stats['available'] - memory_stats['unused']) / units.Ki if 'rss' in memory_stats: memory_resident = memory_stats['rss'] / units.Ki if 'swap_in' in memory_stats and 'swap_out' in memory_stats: memory_swap_in = memory_stats['swap_in'] / units.Ki memory_swap_out = memory_stats['swap_out'] / units.Ki # TODO(sileht): stats also have the disk/vnic info # we could use that instead of the old method for Queen stats = self.connection.domainListGetStats([domain], 0)[0][1] cpu_time = 0 current_cpus = stats.get('vcpu.current') # Iterate over the maximum number of CPUs here, and count the # actual number encountered, since the vcpu.x structure can # have holes according to # https://libvirt.org/git/?p=libvirt.git;a=blob;f=src/libvirt-domain.c # virConnectGetAllDomainStats() for vcpu in range(stats.get('vcpu.maximum', 0)): try: cpu_time += (stats.get('vcpu.%s.time' % vcpu) + stats.get('vcpu.%s.wait' % vcpu)) current_cpus -= 1 except TypeError: # pass here, if there are too many holes, the cpu count will # not match, so don't need special error handling. pass if current_cpus: # There wasn't enough data, so fall back cpu_time = stats.get('cpu.time') return virt_inspector.InstanceStats( power_state=domain.info()[0], cpu_number=stats.get('vcpu.current'), cpu_time=cpu_time, memory_actual=memory_actual, memory_available=memory_available, memory_usage=memory_used, memory_resident=memory_resident, memory_swap_in=memory_swap_in, memory_swap_out=memory_swap_out, cpu_cycles=stats.get("perf.cpu_cycles"), instructions=stats.get("perf.instructions"), cache_references=stats.get("perf.cache_references"), cache_misses=stats.get("perf.cache_misses") ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1483567 ceilometer-27.0.0.0rc1/ceilometer/compute/virt/libvirt/utils.py0000664000175000017500000001273115250267452023307 0ustar00zuulzuul# # Copyright 2016 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import errno from oslo_config import cfg from oslo_log import log as logging import tenacity try: import libvirt except ImportError: libvirt = None from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.i18n import _ LOG = logging.getLogger(__name__) OPTS = [ cfg.StrOpt('libvirt_type', default='kvm', choices=['kvm', 'lxc', 'qemu', 'parallels'], help='Libvirt domain type.'), cfg.StrOpt('libvirt_uri', default='', help='Override the default libvirt URI ' '(which is dependent on libvirt_type).'), ] LIBVIRT_PER_TYPE_URIS = dict( parallels='parallels:///system', lxc='lxc:///') # We don't use the libvirt constants in case of libvirt is not available VIR_DOMAIN_NOSTATE = 0 VIR_DOMAIN_RUNNING = 1 VIR_DOMAIN_BLOCKED = 2 VIR_DOMAIN_PAUSED = 3 VIR_DOMAIN_SHUTDOWN = 4 VIR_DOMAIN_SHUTOFF = 5 VIR_DOMAIN_CRASHED = 6 VIR_DOMAIN_PMSUSPENDED = 7 # Stolen from nova LIBVIRT_POWER_STATE = { VIR_DOMAIN_NOSTATE: 'pending', VIR_DOMAIN_RUNNING: 'running', VIR_DOMAIN_BLOCKED: 'running', VIR_DOMAIN_PAUSED: 'paused', VIR_DOMAIN_SHUTDOWN: 'shutdown', VIR_DOMAIN_SHUTOFF: 'shutdown', VIR_DOMAIN_CRASHED: 'crashed', VIR_DOMAIN_PMSUSPENDED: 'suspended', } # NOTE(sileht): This is a guessing of the nova # status, should be true 99.9% on the time, # but can be wrong during some transition state # like shelving/rescuing LIBVIRT_STATUS = { VIR_DOMAIN_NOSTATE: 'building', VIR_DOMAIN_RUNNING: 'active', VIR_DOMAIN_BLOCKED: 'active', VIR_DOMAIN_PAUSED: 'paused', VIR_DOMAIN_SHUTDOWN: 'stopped', VIR_DOMAIN_SHUTOFF: 'stopped', VIR_DOMAIN_CRASHED: 'error', VIR_DOMAIN_PMSUSPENDED: 'suspended', } # NOTE(pas-ha) in the order from newest to oldest NOVA_METADATA_VERSIONS = ( "http://openstack.org/xmlns/libvirt/nova/1.1", "http://openstack.org/xmlns/libvirt/nova/1.0", ) def new_libvirt_connection(conf): if not libvirt: raise ImportError("python-libvirt module is missing") uri = (conf.libvirt_uri or LIBVIRT_PER_TYPE_URIS.get(conf.libvirt_type, 'qemu:///system')) LOG.debug('Connecting to libvirt: %s', uri) return libvirt.openReadOnly(uri) def refresh_libvirt_connection(conf, klass): connection = getattr(klass, '_libvirt_connection', None) if not connection or not connection.isAlive(): connection = new_libvirt_connection(conf) setattr(klass, '_libvirt_connection', connection) return connection def is_disconnection_exception(e): if not libvirt: return False if not isinstance(e, libvirt.libvirtError): return False is_libvirt_error = ( e.get_error_code() in (libvirt.VIR_ERR_SYSTEM_ERROR, libvirt.VIR_ERR_INTERNAL_ERROR) and e.get_error_domain() in (libvirt.VIR_FROM_REMOTE, libvirt.VIR_FROM_RPC)) is_system_error = ( e.get_error_domain() == libvirt.VIR_FROM_RPC and e.get_error_code() == errno.EPIPE) return is_libvirt_error or is_system_error retry_on_disconnect = tenacity.retry( retry=tenacity.retry_if_exception(is_disconnection_exception), stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_exponential(multiplier=3, min=1, max=60)) def raise_nodata_if_unsupported(method): def inner(in_self, instance, *args, **kwargs): try: return method(in_self, instance, *args, **kwargs) except libvirt.libvirtError as e: # NOTE(sileht): At this point libvirt connection error # have been reraise as tenacity.RetryError() msg = _('Failed to inspect instance %(instance_uuid)s stats, ' 'can not get info from libvirt: %(error)s') % { "instance_uuid": instance.id, "error": e} raise virt_inspector.NoDataException(msg) return inner @retry_on_disconnect def instance_metadata(domain): xml_string = None last_error = None for meta_version in NOVA_METADATA_VERSIONS: try: xml_string = domain.metadata( libvirt.VIR_DOMAIN_METADATA_ELEMENT, meta_version) break except libvirt.libvirtError as exc: if exc.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN_METADATA: LOG.debug("Failed to find metadata %s in domain %s", meta_version, domain.UUIDString()) last_error = exc continue elif is_disconnection_exception(exc): # Re-raise the exception so it's handled and retries raise last_error = exc if xml_string is None: LOG.error( "Fail to get domain uuid %s metadata, libvirtError: %s", domain.UUIDString(), last_error ) return xml_string ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2071276 ceilometer-27.0.0.0rc1/ceilometer/data/0000775000175000017500000000000015250267567016376 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2381277 ceilometer-27.0.0.0rc1/ceilometer/data/meters.d/0000775000175000017500000000000015250267567020117 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1493568 ceilometer-27.0.0.0rc1/ceilometer/data/meters.d/meters.yaml0000664000175000017500000003250615250267452022301 0ustar00zuulzuul--- metric: # Image - name: "image.size" event_type: - "image.upload" - "image.delete" - "image.update" type: "gauge" unit: B volume: $.payload.size resource_id: $.payload.id project_id: $.payload.owner - name: "image.download" event_type: "image.send" type: "delta" unit: "B" volume: $.payload.bytes_sent resource_id: $.payload.image_id user_id: $.payload.receiver_user_id project_id: $.payload.receiver_tenant_id - name: "image.serve" event_type: "image.send" type: "delta" unit: "B" volume: $.payload.bytes_sent resource_id: $.payload.image_id project_id: $.payload.owner_id - name: 'volume.provider.capacity.total' event_type: 'capacity.backend.*' type: 'gauge' unit: 'GiB' volume: $.payload.total resource_id: $.payload.name_to_id - name: 'volume.provider.capacity.free' event_type: 'capacity.backend.*' type: 'gauge' unit: 'GiB' volume: $.payload.free resource_id: $.payload.name_to_id - name: 'volume.provider.capacity.allocated' event_type: 'capacity.backend.*' type: 'gauge' unit: 'GiB' volume: $.payload.allocated resource_id: $.payload.name_to_id - name: 'volume.provider.capacity.provisioned' event_type: 'capacity.backend.*' type: 'gauge' unit: 'GiB' volume: $.payload.provisioned resource_id: $.payload.name_to_id - name: 'volume.provider.capacity.virtual_free' event_type: 'capacity.backend.*' type: 'gauge' unit: 'GiB' volume: $.payload.virtual_free resource_id: $.payload.name_to_id - name: 'volume.provider.pool.capacity.total' event_type: 'capacity.pool.*' type: 'gauge' unit: 'GiB' volume: $.payload.total resource_id: $.payload.name_to_id metadata: &provider_pool_meta provider: $.payload.name_to_id.`split(#, 0, 1)` - name: 'volume.provider.pool.capacity.free' event_type: 'capacity.pool.*' type: 'gauge' unit: 'GiB' volume: $.payload.free resource_id: $.payload.name_to_id metadata: <<: *provider_pool_meta - name: 'volume.provider.pool.capacity.allocated' event_type: 'capacity.pool.*' type: 'gauge' unit: 'GiB' volume: $.payload.allocated resource_id: $.payload.name_to_id metadata: <<: *provider_pool_meta - name: 'volume.provider.pool.capacity.provisioned' event_type: 'capacity.pool.*' type: 'gauge' unit: 'GiB' volume: $.payload.provisioned resource_id: $.payload.name_to_id metadata: <<: *provider_pool_meta - name: 'volume.provider.pool.capacity.virtual_free' event_type: 'capacity.pool.*' type: 'gauge' unit: 'GiB' volume: $.payload.virtual_free resource_id: $.payload.name_to_id metadata: <<: *provider_pool_meta - name: 'volume.size' event_type: - 'volume.exists' - 'volume.retype' - 'volume.create.*' - 'volume.delete.*' - 'volume.resize.*' - 'volume.attach.*' - 'volume.detach.*' - 'volume.update.*' - 'volume.manage.*' type: 'gauge' unit: 'GiB' volume: $.payload.size user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.volume_id metadata: display_name: $.payload.display_name volume_type: $.payload.volume_type volume_type_id: $.payload.volume_type image_id: $.payload.glance_metadata[?key=image_id].value instance_id: $.payload.volume_attachment[0].server_id - name: 'snapshot.size' event_type: - 'snapshot.exists' - 'snapshot.create.*' - 'snapshot.delete.*' - 'snapshot.manage.*' type: 'gauge' unit: 'GiB' volume: $.payload.volume_size user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.snapshot_id metadata: display_name: $.payload.display_name - name: 'backup.size' event_type: - 'backup.exists' - 'backup.create.*' - 'backup.delete.*' - 'backup.restore.*' type: 'gauge' unit: 'GiB' volume: $.payload.size user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.backup_id metadata: display_name: $.payload.display_name # Magnum - name: $.payload.metrics.[*].name event_type: 'magnum.bay.metrics.*' type: 'gauge' unit: $.payload.metrics.[*].unit volume: $.payload.metrics.[*].value user_id: $.payload.user_id project_id: $.payload.project_id resource_id: $.payload.resource_id lookup: ['name', 'unit', 'volume'] # Swift - name: $.payload.measurements.[*].metric.[*].name event_type: 'objectstore.http.request' type: 'delta' unit: $.payload.measurements.[*].metric.[*].unit volume: $.payload.measurements.[*].result resource_id: $.payload.target.id user_id: $.payload.initiator.id project_id: $.payload.initiator.project_id lookup: ['name', 'unit', 'volume'] - name: 'memory' event_type: &instance_events compute.instance.(?!create.start|update).* type: 'gauge' unit: 'MiB' volume: $.payload.memory_mb user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_metadata: $.payload.metadata metadata: &instance_meta host: $.payload.host flavor_id: $.payload.instance_flavor_id flavor_name: $.payload.instance_type display_name: $.payload.display_name image_ref: $.payload.image_meta.base_image_ref image_meta: $.payload.image_meta launched_at: $.payload.launched_at created_at: $.payload.created_at deleted_at: $.payload.deleted_at - name: 'vcpus' event_type: *instance_events type: 'gauge' unit: 'vcpu' volume: $.payload.vcpus user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_metadata: $.payload.metadata metadata: <<: *instance_meta - name: 'compute.instance.booting.time' event_type: 'compute.instance.create.end' type: 'gauge' unit: 'sec' volume: fields: [$.payload.created_at, $.payload.launched_at] plugin: 'timedelta' project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_metadata: $.payload.metadata metadata: <<: *instance_meta - name: 'disk.root.size' event_type: *instance_events type: 'gauge' unit: 'GiB' volume: $.payload.root_gb user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_metadata: $.payload.metadata metadata: <<: *instance_meta - name: 'disk.ephemeral.size' event_type: *instance_events type: 'gauge' unit: 'GiB' volume: $.payload.ephemeral_gb user_id: $.payload.user_id project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_metadata: $.payload.metadata metadata: <<: *instance_meta - name: 'bandwidth' event_type: 'l3.meter' type: 'delta' unit: 'B' volume: $.payload.bytes project_id: $.payload.tenant_id resource_id: $.payload.label_id - name: 'compute.node.cpu.frequency' event_type: 'compute.metrics.update' type: 'gauge' unit: 'MHz' volume: $.payload.metrics[?(@.name='cpu.frequency')].value resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.frequency')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.frequency')].source - name: 'compute.node.cpu.user.time' event_type: 'compute.metrics.update' type: 'cumulative' unit: 'ns' volume: $.payload.metrics[?(@.name='cpu.user.time')].value resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.user.time')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.user.time')].source - name: 'compute.node.cpu.kernel.time' event_type: 'compute.metrics.update' type: 'cumulative' unit: 'ns' volume: $.payload.metrics[?(@.name='cpu.kernel.time')].value resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.kernel.time')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.kernel.time')].source - name: 'compute.node.cpu.idle.time' event_type: 'compute.metrics.update' type: 'cumulative' unit: 'ns' volume: $.payload.metrics[?(@.name='cpu.idle.time')].value resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.idle.time')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.idle.time')].source - name: 'compute.node.cpu.iowait.time' event_type: 'compute.metrics.update' type: 'cumulative' unit: 'ns' volume: $.payload.metrics[?(@.name='cpu.iowait.time')].value resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.iowait.time')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.iowait.time')].source - name: 'compute.node.cpu.kernel.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: $.payload.metrics[?(@.name='cpu.kernel.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.kernel.percent')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.kernel.percent')].source - name: 'compute.node.cpu.idle.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: $.payload.metrics[?(@.name='cpu.idle.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.idle.percent')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.idle.percent')].source - name: 'compute.node.cpu.user.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: $.payload.metrics[?(@.name='cpu.user.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.user.percent')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.user.percent')].source - name: 'compute.node.cpu.iowait.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: $.payload.metrics[?(@.name='cpu.iowait.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.iowait.percent')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.iowait.percent')].source - name: 'compute.node.cpu.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: $.payload.metrics[?(@.name='cpu.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename timestamp: $.payload.metrics[?(@.name='cpu.percent')].timestamp metadata: event_type: $.event_type host: $.publisher_id source: $.payload.metrics[?(@.name='cpu.percent')].source # Identity # NOTE(gordc): hack because jsonpath-rw-ext can't concat starting with string. - name: $.payload.outcome.`sub(/.*/, )` + 'identity.authenticate.' + $.payload.outcome type: 'delta' unit: 'user' volume: 1 event_type: - 'identity.authenticate' resource_id: $.payload.initiator.id user_id: $.payload.initiator.id # DNS - name: 'dns.domain.exists' event_type: 'dns.domain.exists' type: 'cumulative' unit: 's' volume: fields: [$.payload.audit_period_beginning, $.payload.audit_period_ending] plugin: 'timedelta' project_id: $.payload.tenant_id resource_id: $.payload.id user_id: $.ctxt.user metadata: status: $.payload.status pool_id: $.payload.pool_id host: $.publisher_id # Trove - name: 'trove.instance.exists' event_type: 'trove.instance.exists' type: 'cumulative' unit: 's' volume: fields: [$.payload.audit_period_beginning, $.payload.audit_period_ending] plugin: 'timedelta' project_id: $.payload.tenant_id resource_id: $.payload.instance_id user_id: $.payload.user_id metadata: nova_instance_id: $.payload.nova_instance_id state: $.payload.state service_id: $.payload.service_id instance_type: $.payload.instance_type instance_type_id: $.payload.instance_type_id # Manila - name: 'manila.share.size' event_type: - 'share.create.*' - 'share.delete.*' - 'share.extend.*' - 'share.shrink.*' type: 'gauge' unit: 'GiB' volume: $.payload.size user_id: $.payload.user_id project_id: $.payload.project_id resource_id: $.payload.share_id metadata: name: $.payload.name host: $.payload.host status: $.payload.status availability_zone: $.payload.availability_zone protocol: $.payload.proto ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1503568 ceilometer-27.0.0.0rc1/ceilometer/declarative.py0000664000175000017500000001542715250267452020324 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os from jsonpath_rw_ext import parser from oslo_log import log import yaml from ceilometer.i18n import _ LOG = log.getLogger(__name__) class DefinitionException(Exception): def __init__(self, message, definition_cfg=None): msg = '{} {}: {}'.format( self.__class__.__name__, definition_cfg, message) super().__init__(msg) self.brief_message = message class MeterDefinitionException(DefinitionException): pass class EventDefinitionException(DefinitionException): pass class ResourceDefinitionException(DefinitionException): pass class DynamicPollsterException(DefinitionException): pass class DynamicPollsterDefinitionException(DynamicPollsterException): pass class InvalidResponseTypeException(DynamicPollsterException): pass class NonOpenStackApisDynamicPollsterException\ (DynamicPollsterDefinitionException): pass class Definition: JSONPATH_RW_PARSER = parser.ExtentedJsonPathParser() GETTERS_CACHE = {} def __init__(self, name, cfg, plugin_manager): self.cfg = cfg self.name = name self.plugin = None if isinstance(cfg, dict): if 'fields' not in cfg: raise DefinitionException( _("The field 'fields' is required for %s") % name, self.cfg) if 'plugin' in cfg: plugin_cfg = cfg['plugin'] if isinstance(plugin_cfg, str): plugin_name = plugin_cfg plugin_params = {} else: try: plugin_name = plugin_cfg['name'] except KeyError: raise DefinitionException( _('Plugin specified, but no plugin name supplied ' 'for %s') % name, self.cfg) plugin_params = plugin_cfg.get('parameters') if plugin_params is None: plugin_params = {} try: plugin_ext = plugin_manager[plugin_name] except KeyError: raise DefinitionException( _('No plugin named %(plugin)s available for ' '%(name)s') % dict( plugin=plugin_name, name=name), self.cfg) plugin_class = plugin_ext.plugin self.plugin = plugin_class(**plugin_params) fields = cfg['fields'] else: # Simple definition "foobar: jsonpath" fields = cfg if isinstance(fields, list): # NOTE(mdragon): if not a string, we assume a list. if len(fields) == 1: fields = fields[0] else: fields = '|'.join('(%s)' % path for path in fields) if isinstance(fields, int): self.getter = fields else: try: self.getter = self.make_getter(fields) except Exception as e: raise DefinitionException( _("Parse error in JSONPath specification " "'%(jsonpath)s' for %(name)s: %(err)s") % dict(jsonpath=fields, name=name, err=e), self.cfg) def _get_path(self, match): if match.context is not None: yield from self._get_path(match.context) yield str(match.path) def parse(self, obj, return_all_values=False): if callable(self.getter): values = self.getter(obj) else: return self.getter values = [match for match in values if return_all_values or match.value is not None] if self.plugin is not None: if return_all_values and not self.plugin.support_return_all_values: raise DefinitionException("Plugin %s don't allows to " "return multiple values" % self.cfg["plugin"]["name"], self.cfg) values_map = [('.'.join(self._get_path(match)), match.value) for match in values] values = [v for v in self.plugin.trait_values(values_map) if v is not None] else: values = [match.value for match in values if match is not None] if return_all_values: return values else: return values[0] if values else None def make_getter(self, fields): if fields in self.GETTERS_CACHE: return self.GETTERS_CACHE[fields] else: getter = self.JSONPATH_RW_PARSER.parse(fields).find self.GETTERS_CACHE[fields] = getter return getter def load_definitions(conf, defaults, config_file, fallback_file=None): """Setup a definitions from yaml config file.""" if not os.path.exists(config_file): config_file = conf.find_file(config_file) if not config_file and fallback_file is not None: LOG.debug("No Definitions configuration file found! " "Using default config.") config_file = fallback_file if config_file is not None: LOG.debug("Loading definitions configuration file: %s", config_file) with open(config_file) as cf: config = cf.read() try: definition_cfg = yaml.safe_load(config) except yaml.YAMLError as err: if hasattr(err, 'problem_mark'): mark = err.problem_mark errmsg = (_("Invalid YAML syntax in Definitions file " "%(file)s at line: %(line)s, column: %(column)s.") % dict(file=config_file, line=mark.line + 1, column=mark.column + 1)) else: errmsg = (_("YAML error reading Definitions file " "%(file)s") % dict(file=config_file)) LOG.error(errmsg) raise else: LOG.debug("No Definitions configuration file found! " "Using default config.") definition_cfg = defaults LOG.debug("Definitions: %s", definition_cfg) return definition_cfg ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1503568 ceilometer-27.0.0.0rc1/ceilometer/designate_client.py0000664000175000017500000000276015250267452021336 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack from oslo_config import cfg from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('designate', default='dns', help='Designate service type.'), ] class Client: """A client which gets information via openstacksdk.""" def __init__(self, conf): """Initialize a Designate client object.""" creds = conf.service_credentials self.conn = openstack.connection.Connection( session=keystone_client.get_session(conf), region_name=creds.region_name, dns_interface=creds.interface, dns_service_type=conf.service_types.designate ) def zones_list(self): """Return a list of DNS zones from all projects.""" return self.conn.dns.zones(all_projects=True) def recordsets_list(self, zone): """Return a list of recordsets for a zone.""" return self.conn.dns.recordsets(zone, all_projects=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2381277 ceilometer-27.0.0.0rc1/ceilometer/dns/0000775000175000017500000000000015250267567016251 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1503568 ceilometer-27.0.0.0rc1/ceilometer/dns/__init__.py0000664000175000017500000000000015250267452020341 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1513567 ceilometer-27.0.0.0rc1/ceilometer/dns/designate.py0000664000175000017500000001134615250267452020564 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer import designate_client from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) # Designate zone status values mapped to numeric values ZONE_STATUS = { 'active': 1, 'pending': 2, 'error': 3, 'deleted': 4, } # Designate zone action values mapped to numeric values ZONE_ACTION = { 'none': 0, 'create': 1, 'delete': 2, 'update': 3, } class _BaseZonePollster(plugin_base.PollsterBase): """Base pollster for Designate DNS zone metrics.""" FIELDS = ['name', 'email', 'ttl', 'description', 'type', 'status', 'action', 'serial', 'pool_id', ] @property def default_discovery(self): return 'dns_zones' @staticmethod def extract_metadata(zone): return {k: getattr(zone, k, None) for k in _BaseZonePollster.FIELDS} class ZoneStatusPollster(_BaseZonePollster): """Pollster for Designate DNS zone status.""" @staticmethod def get_status_id(value): if not value: return -1 status = value.lower() return ZONE_STATUS.get(status, -1) def get_samples(self, manager, cache, resources): for zone in resources or []: LOG.debug("DNS ZONE: %s", zone) status = self.get_status_id(zone.status) if status == -1: LOG.warning( "Unknown status %(status)s for DNS zone " "%(name)s (%(id)s), setting volume to -1", {"status": zone.status, "name": zone.name, "id": zone.id}) yield sample.Sample( name='dns.zone.status', type=sample.TYPE_GAUGE, unit='status', volume=status, user_id=None, project_id=zone.project_id, resource_id=zone.id, resource_metadata=self.extract_metadata(zone) ) class ZoneRecordsetCountPollster(_BaseZonePollster): """Pollster for Designate DNS zone recordset count.""" def __init__(self, conf): super().__init__(conf) self.designate_cli = designate_client.Client(conf) def get_samples(self, manager, cache, resources): for zone in resources or []: LOG.debug("DNS ZONE: %s", zone) recordsets = list(self.designate_cli.recordsets_list(zone)) count = len(recordsets) yield sample.Sample( name='dns.zone.recordsets', type=sample.TYPE_GAUGE, unit='recordset', volume=count, user_id=None, project_id=zone.project_id, resource_id=zone.id, resource_metadata=self.extract_metadata(zone) ) class ZoneTTLPollster(_BaseZonePollster): """Pollster for Designate DNS zone TTL.""" def get_samples(self, manager, cache, resources): for zone in resources or []: LOG.debug("DNS ZONE: %s", zone) ttl = zone.ttl if zone.ttl is not None else 0 yield sample.Sample( name='dns.zone.ttl', type=sample.TYPE_GAUGE, unit='second', volume=ttl, user_id=None, project_id=zone.project_id, resource_id=zone.id, resource_metadata=self.extract_metadata(zone) ) class ZoneSerialPollster(_BaseZonePollster): """Pollster for Designate DNS zone serial number.""" def get_samples(self, manager, cache, resources): for zone in resources or []: LOG.debug("DNS ZONE: %s", zone) serial = zone.serial if zone.serial is not None else 0 yield sample.Sample( name='dns.zone.serial', type=sample.TYPE_GAUGE, unit='serial', volume=serial, user_id=None, project_id=zone.project_id, resource_id=zone.id, resource_metadata=self.extract_metadata(zone) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1513567 ceilometer-27.0.0.0rc1/ceilometer/dns/discovery.py0000664000175000017500000000203715250267452020625 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import designate_client from ceilometer.polling import plugin_base class ZoneDiscovery(plugin_base.DiscoveryBase): """Discovery class for Designate DNS zones.""" KEYSTONE_REQUIRED_FOR_SERVICE = 'designate' def __init__(self, conf): super().__init__(conf) self.designate_client = designate_client.Client(conf) def discover(self, manager, param=None): """Discover DNS zone resources to monitor.""" return self.designate_client.zones_list() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2391276 ceilometer-27.0.0.0rc1/ceilometer/event/0000775000175000017500000000000015250267567016606 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1523566 ceilometer-27.0.0.0rc1/ceilometer/event/__init__.py0000664000175000017500000000000015250267452020676 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1523566 ceilometer-27.0.0.0rc1/ceilometer/event/converter.py0000664000175000017500000002704315250267452021166 0ustar00zuulzuul# # Copyright 2013 Rackspace Hosting. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fnmatch import os from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils from ceilometer import declarative from ceilometer.event import models from ceilometer.i18n import _ OPTS = [ cfg.StrOpt('definitions_cfg_file', default="event_definitions.yaml", help="Configuration file for event definitions." ), cfg.BoolOpt('drop_unmatched_notifications', default=False, help='Drop notifications if no event definition matches. ' '(Otherwise, we convert them with just the default traits)'), cfg.MultiStrOpt('store_raw', default=[], help='Store the raw notification for select priority ' 'levels (info and/or error). By default, raw details are ' 'not captured.') ] LOG = log.getLogger(__name__) class TraitDefinition(declarative.Definition): def __init__(self, name, trait_cfg, plugin_manager): super().__init__(name, trait_cfg, plugin_manager) type_name = (trait_cfg.get('type', 'text') if isinstance(trait_cfg, dict) else 'text') self.trait_type = models.Trait.get_type_by_name(type_name) if self.trait_type is None: raise declarative.EventDefinitionException( _("Invalid trait type '%(type)s' for trait %(trait)s") % dict(type=type_name, trait=name), self.cfg) def to_trait(self, notification_body): value = self.parse(notification_body) if value is None: return None # NOTE(mdragon): some openstack projects (mostly Nova) emit '' # for null fields for things like dates. if self.trait_type != models.Trait.TEXT_TYPE and value == '': return None value = models.Trait.convert_value(self.trait_type, value) return models.Trait(self.name, self.trait_type, value) class EventDefinition: DEFAULT_TRAITS = dict( service=dict(type='text', fields='publisher_id'), request_id=dict(type='text', fields='ctxt.request_id'), project_id=dict(type='text', fields=['payload.tenant_id', 'ctxt.project_id']), user_id=dict(type='text', fields=['payload.user_id', 'ctxt.user_id']), # TODO(dikonoor):tenant_id is old terminology and should # be deprecated tenant_id=dict(type='text', fields=['payload.tenant_id', 'ctxt.project_id']), ) def __init__(self, definition_cfg, trait_plugin_mgr, raw_levels): self._included_types = [] self._excluded_types = [] self.traits = dict() self.cfg = definition_cfg self.raw_levels = raw_levels try: event_type = definition_cfg['event_type'] traits = definition_cfg['traits'] except KeyError as err: raise declarative.EventDefinitionException( _("Required field %s not specified") % err.args[0], self.cfg) if isinstance(event_type, str): event_type = [event_type] for t in event_type: if t.startswith('!'): self._excluded_types.append(t[1:]) else: self._included_types.append(t) if self._excluded_types and not self._included_types: self._included_types.append('*') for trait_name in self.DEFAULT_TRAITS: self.traits[trait_name] = TraitDefinition( trait_name, self.DEFAULT_TRAITS[trait_name], trait_plugin_mgr) for trait_name in traits: self.traits[trait_name] = TraitDefinition( trait_name, traits[trait_name], trait_plugin_mgr) def included_type(self, event_type): for t in self._included_types: if fnmatch.fnmatch(event_type, t): return True return False def excluded_type(self, event_type): for t in self._excluded_types: if fnmatch.fnmatch(event_type, t): return True return False def match_type(self, event_type): return (self.included_type(event_type) and not self.excluded_type(event_type)) @property def is_catchall(self): return '*' in self._included_types and not self._excluded_types def to_event(self, priority, notification_body): event_type = notification_body['event_type'] message_id = notification_body['metadata']['message_id'] when = timeutils.normalize_time(timeutils.parse_isotime( notification_body['metadata']['timestamp'])) traits = (self.traits[t].to_trait(notification_body) for t in self.traits) # Only accept non-None value traits ... traits = [trait for trait in traits if trait is not None] raw = notification_body if priority in self.raw_levels else {} event = models.Event(message_id, event_type, when, traits, raw) return event class NotificationEventsConverter: """Notification Event Converter The NotificationEventsConverter handles the conversion of Notifications from openstack systems into Ceilometer Events. The conversion is handled according to event definitions in a config file. The config is a list of event definitions. Order is significant, a notification will be processed according to the LAST definition that matches it's event_type. (We use the last matching definition because that allows you to use YAML merge syntax in the definitions file.) Each definition is a dictionary with the following keys (all are required): - event_type: this is a list of notification event_types this definition will handle. These can be wildcarded with unix shell glob (not regex!) wildcards. An exclusion listing (starting with a '!') will exclude any types listed from matching. If ONLY exclusions are listed, the definition will match anything not matching the exclusions. This item can also be a string, which will be taken as equivalent to 1 item list. Examples: * ['compute.instance.exists'] will only match compute.instance.exists notifications * "compute.instance.exists" Same as above. * ["image.create", "image.delete"] will match image.create and image.delete, but not anything else. * "compute.instance.*" will match compute.instance.create.start but not image.upload * ['*.start','*.end', '!scheduler.*'] will match compute.instance.create.start, and image.delete.end, but NOT compute.instance.exists or scheduler.run_instance.start * '!image.*' matches any notification except image notifications. * ['*', '!image.*'] same as above. - traits: (dict) The keys are trait names, the values are the trait definitions. Each trait definition is a dictionary with the following keys: - type (optional): The data type for this trait. (as a string) Valid options are: 'text', 'int', 'float' and 'datetime', defaults to 'text' if not specified. - fields: a path specification for the field(s) in the notification you wish to extract. The paths can be specified with a dot syntax (e.g. 'payload.host') or dictionary syntax (e.g. 'payload[host]') is also supported. In either case, if the key for the field you are looking for contains special characters, like '.', it will need to be quoted (with double or single quotes) like so:: "payload.image_meta.'org.openstack__1__architecture'" The syntax used for the field specification is a variant of JSONPath, and is fairly flexible. (see: https://github.com/kennknowles/python-jsonpath-rw for more info) Specifications can be written to match multiple possible fields, the value for the trait will be derived from the matching fields that exist and have a non-null (i.e. is not None) values in the notification. By default the value will be the first such field. (plugins can alter that, if they wish) This configuration value is normally a string, for convenience, it can be specified as a list of specifications, which will be OR'ed together (a union query in jsonpath terms) - plugin (optional): (dictionary) with the following keys: - name: (string) name of a plugin to load - parameters: (optional) Dictionary of keyword args to pass to the plugin on initialization. See documentation on each plugin to see what arguments it accepts. For convenience, this value can also be specified as a string, which is interpreted as a plugin name, which will be loaded with no parameters. """ def __init__(self, conf, events_config, trait_plugin_mgr): self.conf = conf raw_levels = [level.lower() for level in self.conf.event.store_raw] self.definitions = [ EventDefinition(event_def, trait_plugin_mgr, raw_levels) for event_def in reversed(events_config)] add_catchall = not self.conf.event.drop_unmatched_notifications if add_catchall and not any(d.is_catchall for d in self.definitions): event_def = dict(event_type='*', traits={}) self.definitions.append(EventDefinition(event_def, trait_plugin_mgr, raw_levels)) def to_event(self, priority, notification_body): event_type = notification_body['event_type'] message_id = notification_body['metadata']['message_id'] edef = None for d in self.definitions: if d.match_type(event_type): edef = d break if edef is None: if self.conf.event.drop_unmatched_notifications: msg_level = log.DEBUG else: # If drop_unmatched_notifications is False, this should # never happen. (mdragon) msg_level = log.ERROR LOG.log(msg_level, 'Dropping Notification %(type)s (uuid:%(msgid)s)', dict(type=event_type, msgid=message_id)) return None return edef.to_event(priority, notification_body) def setup_events(conf, trait_plugin_mgr): """Setup the event definitions from yaml config file.""" return NotificationEventsConverter( conf, declarative.load_definitions( conf, [], conf.event.definitions_cfg_file, os.path.join( os.path.dirname(os.path.abspath(__file__)), '..', 'pipeline', 'data', 'event_definitions.yaml')), trait_plugin_mgr) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1523566 ceilometer-27.0.0.0rc1/ceilometer/event/models.py0000664000175000017500000001124415250267452020436 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Model classes for use in the events storage API. """ from oslo_utils import timeutils def serialize_dt(value): """Serializes parameter if it is datetime.""" return value.isoformat() if hasattr(value, 'isoformat') else value class Model: """Base class for storage API models.""" def __init__(self, **kwds): self.fields = list(kwds) for k, v in kwds.items(): setattr(self, k, v) def as_dict(self): d = {} for f in self.fields: v = getattr(self, f) if isinstance(v, Model): v = v.as_dict() elif isinstance(v, list) and v and isinstance(v[0], Model): v = [sub.as_dict() for sub in v] d[f] = v return d def __eq__(self, other): return self.as_dict() == other.as_dict() def __ne__(self, other): return not self.__eq__(other) class Event(Model): """A raw event from the source system. Events have Traits. Metrics will be derived from one or more Events. """ DUPLICATE = 1 UNKNOWN_PROBLEM = 2 INCOMPATIBLE_TRAIT = 3 def __init__(self, message_id, event_type, generated, traits, raw): """Create a new event. :param message_id: Unique ID for the message this event stemmed from. This is different than the Event ID, which comes from the underlying storage system. :param event_type: The type of the event. :param generated: UTC time for when the event occurred. :param traits: list of Traits on this Event. :param raw: Unindexed raw notification details. """ Model.__init__(self, message_id=message_id, event_type=event_type, generated=generated, traits=traits, raw=raw) def append_trait(self, trait_model): self.traits.append(trait_model) def __repr__(self): trait_list = [] if self.traits: trait_list = [str(trait) for trait in self.traits] return ("" % (self.message_id, self.event_type, self.generated, " ".join(trait_list))) def serialize(self): return {'message_id': self.message_id, 'event_type': self.event_type, 'generated': serialize_dt(self.generated), 'traits': [trait.serialize() for trait in self.traits], 'raw': self.raw} class Trait(Model): """A Trait is a key/value pair of data on an Event. The value is variant record of basic data types (int, date, float, etc). """ NONE_TYPE = 0 TEXT_TYPE = 1 INT_TYPE = 2 FLOAT_TYPE = 3 DATETIME_TYPE = 4 type_names = { NONE_TYPE: "none", TEXT_TYPE: "string", INT_TYPE: "integer", FLOAT_TYPE: "float", DATETIME_TYPE: "datetime" } def __init__(self, name, dtype, value): if not dtype: dtype = Trait.NONE_TYPE Model.__init__(self, name=name, dtype=dtype, value=value) def __repr__(self): return "" % (self.name, self.dtype, self.value) def serialize(self): return self.name, self.dtype, serialize_dt(self.value) def get_type_name(self): return self.get_name_by_type(self.dtype) @classmethod def get_type_by_name(cls, type_name): return getattr(cls, '%s_TYPE' % type_name.upper(), None) @classmethod def get_type_names(cls): return cls.type_names.values() @classmethod def get_name_by_type(cls, type_id): return cls.type_names.get(type_id, "none") @classmethod def convert_value(cls, trait_type, value): if trait_type is cls.INT_TYPE: return int(value) if trait_type is cls.FLOAT_TYPE: return float(value) if trait_type is cls.DATETIME_TYPE: return timeutils.normalize_time(timeutils.parse_isotime(value)) # Cropping the text value to match the TraitText value size if isinstance(value, bytes): return value.decode('utf-8')[:255] return str(value)[:255] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1523566 ceilometer-27.0.0.0rc1/ceilometer/event/trait_plugins.py0000664000175000017500000002376115250267452022046 0ustar00zuulzuul# # Copyright 2013 Rackspace Hosting. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc from oslo_log import log from oslo_utils import timeutils LOG = log.getLogger(__name__) class TraitPluginBase(metaclass=abc.ABCMeta): """Base class for plugins. It converts notification fields to Trait values. """ support_return_all_values = False """If True, an exception will be raised if the user expect the plugin to return one trait per match_list, but the plugin doesn't allow/support that. """ def __init__(self, **kw): """Setup the trait plugin. For each Trait definition a plugin is used on in a conversion definition, a new instance of the plugin will be created, and initialized with the parameters (if any) specified in the config file. :param kw: the parameters specified in the event definitions file. """ super().__init__() @abc.abstractmethod def trait_values(self, match_list): """Convert a set of fields to one or multiple Trait values. This method is called each time a trait is attempted to be extracted from a notification. It will be called *even if* no matching fields are found in the notification (in that case, the match_list will be empty). If this method returns None, the trait *will not* be added to the event. Any other value returned by this method will be used as the value for the trait. Values returned will be coerced to the appropriate type for the trait. :param match_list: A list (may be empty if no matches) of *tuples*. Each tuple is (field_path, value) where field_path is the jsonpath for that specific field. Example:: trait's fields definition: ['payload.foobar', 'payload.baz', 'payload.thing.*'] notification body: { 'metadata': {'message_id': '12345'}, 'publisher': 'someservice.host', 'payload': { 'foobar': 'test', 'thing': { 'bar': 12, 'boing': 13, } } } match_list will be: [('payload.foobar','test'), ('payload.thing.bar',12), ('payload.thing.boing',13)] Here is a plugin that emulates the default (no plugin) behavior: .. code-block:: python class DefaultPlugin(TraitPluginBase): "Plugin that returns the first field value." def __init__(self, **kw): super(DefaultPlugin, self).__init__() def trait_values(self, match_list): if not match_list: return None return [ match[1] for match in match_list] """ class SplitterTraitPlugin(TraitPluginBase): """Plugin that splits a piece off of a string value.""" support_return_all_values = True def __init__(self, separator=".", segment=0, max_split=None, **kw): """Setup how do split the field. :param separator: String to split on. default "." :param segment: Which segment to return. (int) default 0 :param max_split: Limit number of splits. Default: None (no limit) """ LOG.warning('split plugin is deprecated, ' 'add ".`split(%(sep)s, %(segment)d, ' '%(max_split)d)`" to your jsonpath instead', dict(sep=separator, segment=segment, max_split=(-1 if max_split is None else max_split))) self.separator = separator self.segment = segment self.max_split = max_split super().__init__(**kw) def trait_values(self, match_list): return [self._trait_value(match) for match in match_list] def _trait_value(self, match): value = str(match[1]) if self.max_split is not None: values = value.split(self.separator, self.max_split) else: values = value.split(self.separator) try: return values[self.segment] except IndexError: return None class BitfieldTraitPlugin(TraitPluginBase): """Plugin to set flags on a bitfield.""" def __init__(self, initial_bitfield=0, flags=None, **kw): """Setup bitfield trait. :param initial_bitfield: (int) initial value for the bitfield Flags that are set will be OR'ed with this. :param flags: List of dictionaries defining bitflags to set depending on data in the notification. Each one has the following keys: path: jsonpath of field to match. bit: (int) number of bit to set (lsb is bit 0) value: set bit if corresponding field's value matches this. If value is not provided, bit will be set if the field exists (and is non-null), regardless of its value. """ self.initial_bitfield = initial_bitfield if flags is None: flags = [] self.flags = flags super().__init__(**kw) def trait_values(self, match_list): matches = dict(match_list) bitfield = self.initial_bitfield for flagdef in self.flags: path = flagdef['path'] bit = 2 ** int(flagdef['bit']) if path in matches: if 'value' in flagdef: if matches[path] == flagdef['value']: bitfield |= bit else: bitfield |= bit return [bitfield] class TimedeltaPluginMissedFields(Exception): def __init__(self): msg = ('It is required to use two timestamp field with Timedelta ' 'plugin.') super().__init__(msg) class TimedeltaPlugin(TraitPluginBase): """Setup timedelta meter volume of two timestamps fields. Example:: trait's fields definition: ['payload.created_at', 'payload.launched_at'] value is been created as total seconds between 'launched_at' and 'created_at' timestamps. """ # TODO(idegtiarov): refactor code to have meter_plugins separate from # trait_plugins def trait_values(self, match_list): if len(match_list) != 2: LOG.warning('Timedelta plugin is required two timestamp fields' ' to create timedelta value.') return [None] start, end = match_list try: start_time = timeutils.parse_isotime(start[1]) end_time = timeutils.parse_isotime(end[1]) except Exception as err: LOG.warning('Failed to parse date from set fields, both ' 'fields %(start)s and %(end)s must be datetime: ' '%(err)s', dict(start=start[0], end=end[0], err=err)) return [None] return [abs((end_time - start_time).total_seconds())] class MapTraitPlugin(TraitPluginBase): """A trait plugin for mapping one set of values to another.""" def __init__(self, values=None, default=None, case_sensitive=True, **kw): """Setup map trait. :param values: (dict[Any, Any]) Mapping of values to their desired target values. :param default: (Any) Value to set if no mapping for a value is found. :param case_sensitive: (bool) Perform case-sensitive string lookups. """ if not values: raise ValueError("The 'values' parameter is required " "for the map trait plugin") if not isinstance(values, dict): raise ValueError("The 'values' parameter needs to be a dict " "for the map trait plugin") self.case_sensitive = case_sensitive if not self.case_sensitive: self.values = {(k.casefold() if isinstance(k, str) else k): v for k, v in values.items()} else: self.values = dict(values) self.default = default super().__init__(**kw) def trait_values(self, match_list): mapped_values = [] for match in match_list: key = match[1] folded_key = ( key.casefold() if not self.case_sensitive and isinstance(key, str) else key) try: value = self.values[folded_key] except KeyError: LOG.warning( ('Unknown value %s found when mapping %s, ' 'mapping to default value of %s'), repr(key), match[0], repr(self.default)) value = self.default else: LOG.debug('Value %s for %s mapped to value %s', repr(key), match[0], repr(value)) mapped_values.append(value) return mapped_values ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1523566 ceilometer-27.0.0.0rc1/ceilometer/exceptions.py0000664000175000017500000000243515250267452020215 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Ceilometer exception definitions.""" class CeilometerException(Exception): """Base exception class for Ceilometer.""" def __init__(self, message=None): self.message = self.__class__.__name__ if message is None else message super().__init__(self.message) class NotFound(CeilometerException): """Resource not found. Raised when a find operation matches no resources. """ def __init__(self, message, details=None): self.details = details if details is not None else {} super().__init__(message) class NoUniqueMatch(CeilometerException): """Multiple entities found instead of one. Raised when a find() operation matches multiple resources when exactly one was expected. """ pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1533568 ceilometer-27.0.0.0rc1/ceilometer/gnocchi_client.py0000664000175000017500000003413215250267452021003 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from gnocchiclient import client from gnocchiclient import exceptions as gnocchi_exc import keystoneauth1.session from oslo_log import log from oslo_utils import versionutils from ceilometer import keystone_client LOG = log.getLogger(__name__) def get_gnocchiclient(conf, request_timeout=None): group = conf.gnocchi.auth_section session = keystone_client.get_session(conf, group=group, timeout=request_timeout) adapter = keystoneauth1.session.TCPKeepAliveAdapter( pool_maxsize=conf.max_parallel_requests) session.mount("http://", adapter) session.mount("https://", adapter) interface = conf[group].interface region_name = conf[group].region_name gnocchi_url = session.get_endpoint(service_type='metric', service_name='gnocchi', interface=interface, region_name=region_name) return client.Client( '1', session, adapter_options={'connect_retries': 3, 'interface': interface, 'region_name': region_name, 'endpoint_override': gnocchi_url}) # NOTE(sileht): This is the initial resource types created in Gnocchi # This list must never change to keep in sync with what Gnocchi early # database contents was containing resources_initial = { "image": { "name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "container_format": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "disk_format": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, }, "instance": { "flavor_id": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "image_ref": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "host": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "display_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "server_group": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, }, "instance_disk": { "name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "instance_id": {"type": "uuid", "required": True}, }, "instance_network_interface": { "name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "instance_id": {"type": "uuid", "required": True}, }, "volume": { "display_name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, }, "swift_account": {}, "ceph_account": {}, "network": {}, "identity": {}, "ipmi": {}, "stack": {}, "host": { "host_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, }, "host_network_interface": { "host_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "device_name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, }, "host_disk": { "host_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "device_name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, }, } # NOTE(sileht): Order matter this have to be considered like alembic migration # code, because it updates the resources schema of Gnocchi resources_update_operations = [ {"desc": "add volume_type to volume", "type": "update_attribute_type", "resource_type": "volume", "data": [{ "op": "add", "path": "/attributes/volume_type", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} }]}, {"desc": "add flavor_name to instance", "type": "update_attribute_type", "resource_type": "instance", "data": [{ "op": "add", "path": "/attributes/flavor_name", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": True, "options": {'fill': ''}} }]}, {"desc": "add nova_compute resource type", "type": "create_resource_type", "resource_type": "nova_compute", "data": [{ "attributes": {"host_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}} }]}, {"desc": "add manila share type", "type": "create_resource_type", "resource_type": "manila_share", "data": [{ "attributes": {"name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "host": {"type": "string", "min_length": 0, "max_length": 255, "required": True}, "protocol": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "availability_zone": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "status": {"type": "string", "min_length": 0, "max_length": 255, "required": True}} }]}, {"desc": "add volume provider resource type", "type": "create_resource_type", "resource_type": "volume_provider", "data": [{ "attributes": {} }]}, {"desc": "add volume provider pool resource type", "type": "create_resource_type", "resource_type": "volume_provider_pool", "data": [{ "attributes": {"provider": {"type": "string", "min_length": 0, "max_length": 255, "required": True}} }]}, {"desc": "add ipmi sensor resource type", "type": "create_resource_type", "resource_type": "ipmi_sensor", "data": [{ "attributes": {"node": {"type": "string", "min_length": 0, "max_length": 255, "required": True}} }]}, {"desc": "add launched_at to instance", "type": "update_attribute_type", "resource_type": "instance", "data": [ {"op": "add", "path": "/attributes/launched_at", "value": {"type": "datetime", "required": False}}, {"op": "add", "path": "/attributes/created_at", "value": {"type": "datetime", "required": False}}, {"op": "add", "path": "/attributes/deleted_at", "value": {"type": "datetime", "required": False}}, ]}, {"desc": "add instance_id/image_id to volume", "type": "update_attribute_type", "resource_type": "volume", "data": [ {"op": "add", "path": "/attributes/image_id", "value": {"type": "uuid", "required": False}}, {"op": "add", "path": "/attributes/instance_id", "value": {"type": "uuid", "required": False}}, ]}, {"desc": "add availability_zone to instance", "type": "update_attribute_type", "resource_type": "instance", "data": [{ "op": "add", "path": "/attributes/availability_zone", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} }]}, {"desc": "add volume_type_id to volume", "type": "update_attribute_type", "resource_type": "volume", "data": [{ "op": "add", "path": "/attributes/volume_type_id", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} }]}, {"desc": "add storage_policy to swift_account", "type": "update_attribute_type", "resource_type": "swift_account", "data": [{ "op": "add", "path": "/attributes/storage_policy", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} # Only containers have a storage policy }]}, {"desc": "make host optional for instance", "type": "update_attribute_type", "resource_type": "instance", "data": [{ "op": "add", # Usually update, the attribute likely already exists "path": "/attributes/host", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} # Allow the hypervisor to be withheld }]}, {"desc": "make container_format optional for image", "type": "update_attribute_type", "resource_type": "image", "data": [{ "op": "add", # Usually update, the attribute likely already exists "path": "/attributes/container_format", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} # This can be null in certain cases }]}, {"desc": "make disk_format optional for image", "type": "update_attribute_type", "resource_type": "image", "data": [{ "op": "add", # Usually update, the attribute likely already exists "path": "/attributes/disk_format", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": False} # This can be null in certain cases }]}, {"desc": "add loadbalancer resource type", "type": "create_resource_type", "resource_type": "loadbalancer", "data": [{ "attributes": { "name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "availability_zone": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "vip_address": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "provider": {"type": "string", "min_length": 0, "max_length": 255, "required": False} } }]}, {"desc": "add dns_zone resource type", "type": "create_resource_type", "resource_type": "dns_zone", "data": [{ "attributes": { "zone_name": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "email": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "zone_type": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "pool_id": {"type": "string", "min_length": 0, "max_length": 255, "required": False} } }]}, {"desc": "add volume_service resource type", "type": "create_resource_type", "resource_type": "volume_service", "data": [{ "attributes": { "host": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "binary": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "zone": {"type": "string", "min_length": 0, "max_length": 255, "required": False}, "status": {"type": "string", "min_length": 0, "max_length": 255, "required": False} } }]}, ] REQUIRED_VERSION = "4.2.0" def upgrade_resource_types(conf): gnocchi = get_gnocchiclient(conf) gnocchi_version = gnocchi.build.get() if not versionutils.is_compatible(REQUIRED_VERSION, gnocchi_version): raise Exception("required gnocchi version is %s, got %s" % (REQUIRED_VERSION, gnocchi_version)) for name, attributes in resources_initial.items(): try: gnocchi.resource_type.get(name=name) except (gnocchi_exc.ResourceTypeNotFound, gnocchi_exc.NotFound): rt = {'name': name, 'attributes': attributes} gnocchi.resource_type.create(resource_type=rt) for ops in resources_update_operations: if ops['type'] == 'update_attribute_type': rt = gnocchi.resource_type.get(name=ops['resource_type']) first_op = ops['data'][0] attrib = first_op['path'].replace('/attributes/', '') # Options are only used when adding/updating attributes. # Make a shallow copy of the new value type, and remove options # from the copy to make sure it isn't included in checks. value = first_op['value'].copy() value.pop('options', None) if (first_op['op'] == 'add' and attrib in rt['attributes'] and value == rt['attributes'][attrib]): continue if first_op['op'] == 'remove' and attrib not in rt['attributes']: continue gnocchi.resource_type.update(ops['resource_type'], ops['data']) elif ops['type'] == 'create_resource_type': try: gnocchi.resource_type.get(name=ops['resource_type']) except (gnocchi_exc.ResourceTypeNotFound, gnocchi_exc.NotFound): rt = {'name': ops['resource_type'], 'attributes': ops['data'][0]['attributes']} gnocchi.resource_type.create(resource_type=rt) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2391276 ceilometer-27.0.0.0rc1/ceilometer/hacking/0000775000175000017500000000000015250267567017071 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1533568 ceilometer-27.0.0.0rc1/ceilometer/hacking/__init__.py0000664000175000017500000000000015250267452021161 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1533568 ceilometer-27.0.0.0rc1/ceilometer/i18n.py0000664000175000017500000000204715250267452016612 0ustar00zuulzuul# Copyright 2014 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/usage.html """ import oslo_i18n DOMAIN = 'ceilometer' _translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) # The primary translation function using the well-known name "_" _ = _translators.primary def translate(value, user_locale): return oslo_i18n.translate(value, user_locale) def get_available_languages(): return oslo_i18n.get_available_languages(DOMAIN) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2401276 ceilometer-27.0.0.0rc1/ceilometer/image/0000775000175000017500000000000015250267567016547 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1533568 ceilometer-27.0.0.0rc1/ceilometer/image/__init__.py0000664000175000017500000000000015250267452020637 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1543567 ceilometer-27.0.0.0rc1/ceilometer/image/discovery.py0000664000175000017500000000223415250267452021122 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack from ceilometer import keystone_client from ceilometer.polling import plugin_base class ImagesDiscovery(plugin_base.DiscoveryBase): def __init__(self, conf): super().__init__(conf) creds = conf.service_credentials self.image_client = openstack.connection.Connection( image_api_version='2', session=keystone_client.get_session(conf), region_name=creds.region_name, image_interface=creds.interface) def discover(self, manager, param=None): """Discover resources to monitor.""" return self.image_client.image.images() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1543567 ceilometer-27.0.0.0rc1/ceilometer/image/glance.py0000664000175000017500000000346315250267452020351 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Common code for working with images """ from ceilometer.polling import plugin_base from ceilometer import sample class _Base(plugin_base.PollsterBase): @property def default_discovery(self): return 'images' @staticmethod def extract_image_metadata(image): return { k: getattr(image, k) for k in [ "status", "visibility", "name", "container_format", "created_at", "disk_format", "updated_at", "min_disk", "is_protected", "checksum", "min_ram", "tags", "virtual_size" ] } class ImageSizePollster(_Base): def get_samples(self, manager, cache, resources): for image in resources: yield sample.Sample( name='image.size', type=sample.TYPE_GAUGE, unit='B', volume=image.size, user_id=None, project_id=image.owner, resource_id=image.id, resource_metadata=self.extract_image_metadata(image), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2401276 ceilometer-27.0.0.0rc1/ceilometer/ipmi/0000775000175000017500000000000015250267567016423 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1553566 ceilometer-27.0.0.0rc1/ceilometer/ipmi/__init__.py0000664000175000017500000000000015250267452020513 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2401276 ceilometer-27.0.0.0rc1/ceilometer/ipmi/notifications/0000775000175000017500000000000015250267567021274 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1553566 ceilometer-27.0.0.0rc1/ceilometer/ipmi/notifications/__init__.py0000664000175000017500000000000015250267452023364 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1553566 ceilometer-27.0.0.0rc1/ceilometer/ipmi/notifications/ironic.py0000664000175000017500000001316215250267452023125 0ustar00zuulzuul# # Copyright 2014 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Converters for producing hardware sensor data sample messages from notification events. """ from oslo_log import log from ceilometer.pipeline import sample as endpoint from ceilometer import sample LOG = log.getLogger(__name__) # Map unit name to SI UNIT_MAP = { 'Watts': 'W', 'Volts': 'V', } def validate_reading(data): """Some sensors read "Disabled".""" return data != 'Disabled' def transform_id(data): return data.lower().replace(' ', '_') def parse_reading(data): try: volume, unit = data.split(' ', 1) unit = unit.rsplit(' ', 1)[-1] return float(volume), UNIT_MAP.get(unit, unit) except ValueError: raise InvalidSensorData('unable to parse sensor reading: %s' % data) class InvalidSensorData(ValueError): pass class SensorNotification(endpoint.SampleEndpoint): """A generic class for extracting samples from sensor data notifications. A notification message can contain multiple samples from multiple sensors, all with the same basic structure: the volume for the sample is found as part of the value of a 'Sensor Reading' key. The unit is in the same value. Subclasses exist solely to allow flexibility with stevedore configuration. """ event_types = ['hardware.ipmi.*'] metric = None def _get_sample(self, message): try: return (payload for _, payload in message['payload'][self.metric].items()) except KeyError: return [] @staticmethod def _package_payload(message, payload): # NOTE(chdent): How much of the payload should we keep? # FIXME(gordc): ironic adds timestamp and event_type in its payload # which we are using below. we should probably just use oslo.messaging # values instead? payload['node'] = message['payload']['node_uuid'] info = {'publisher_id': message['publisher_id'], 'timestamp': message['payload']['timestamp'], 'event_type': message['payload']['event_type'], 'user_id': message['payload'].get('user_id'), 'project_id': message['payload'].get('project_id'), 'payload': payload} return info def build_sample(self, message): """Read and process a notification. The guts of a message are in dict value of a 'payload' key which then itself has a payload key containing a dict of multiple sensor readings. If expected keys in the payload are missing or values are not in the expected form for transformations, KeyError and ValueError are caught and the current sensor payload is skipped. """ payloads = self._get_sample(message['payload']) for payload in payloads: try: # Provide a fallback resource_id in case parts are missing. resource_id = 'missing id' try: resource_id = '{nodeid}-{sensorid}'.format( nodeid=message['payload']['node_uuid'], sensorid=transform_id(payload['Sensor ID']) ) except KeyError as exc: raise InvalidSensorData('missing key in payload: %s' % exc) # Do not pick up power consumption metrics from Current sensor if ( self.metric == 'Current' and 'Pwr Consumption' in payload['Sensor ID'] ): continue info = self._package_payload(message, payload) try: sensor_reading = info['payload']['Sensor Reading'] except KeyError: raise InvalidSensorData( "missing 'Sensor Reading' in payload" ) if validate_reading(sensor_reading): volume, unit = parse_reading(sensor_reading) yield sample.Sample.from_notification( name='hardware.ipmi.%s' % self.metric.lower(), type=sample.TYPE_GAUGE, unit=unit, volume=volume, resource_id=resource_id, message=info, user_id=info['user_id'], project_id=info['project_id'], timestamp=info['timestamp']) except InvalidSensorData as exc: LOG.warning( 'invalid sensor data for %(resource)s: %(error)s', dict(resource=resource_id, error=exc) ) continue class TemperatureSensorNotification(SensorNotification): metric = 'Temperature' class CurrentSensorNotification(SensorNotification): metric = 'Current' class FanSensorNotification(SensorNotification): metric = 'Fan' class VoltageSensorNotification(SensorNotification): metric = 'Voltage' class PowerSensorNotification(SensorNotification): metric = 'Power' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2411275 ceilometer-27.0.0.0rc1/ceilometer/ipmi/platform/0000775000175000017500000000000015250267567020247 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1553566 ceilometer-27.0.0.0rc1/ceilometer/ipmi/platform/__init__.py0000664000175000017500000000000015250267452022337 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1563568 ceilometer-27.0.0.0rc1/ceilometer/ipmi/platform/exception.py0000664000175000017500000000124415250267452022611 0ustar00zuulzuul# Copyright 2014 Intel Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class IPMIException(Exception): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1563568 ceilometer-27.0.0.0rc1/ceilometer/ipmi/platform/ipmi_sensor.py0000664000175000017500000001021615250267452023141 0ustar00zuulzuul# Copyright 2014 Intel Corporation. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """IPMI sensor to collect various sensor data of compute node""" from ceilometer.i18n import _ from ceilometer.ipmi.platform import exception as ipmiexcept from ceilometer.ipmi.platform import ipmitool IPMICMD = {"sdr_info": "sdr info", "sensor_dump": "sdr -v", "sensor_dump_temperature": "sdr -v type Temperature", "sensor_dump_current": "sdr -v type Current", "sensor_dump_fan": "sdr -v type Fan", "sensor_dump_voltage": "sdr -v type Voltage", "sensor_dump_power": "sensor get 'Pwr Consumption'"} # Requires translation of output into dict DICT_TRANSLATE_TEMPLATE = {"translate": 1} class IPMISensor: """The python implementation of IPMI sensor using ipmitool The class implements the IPMI sensor to get various sensor data of compute node. It uses ipmitool to execute the IPMI command and parse the output into dict. """ _inited = False _instance = None def __new__(cls, *args, **kwargs): """Singleton to avoid duplicated initialization.""" if not cls._instance: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance def __init__(self): if not (self._instance and self._inited): self.ipmi_support = False self._inited = True self.ipmi_support = self.check_ipmi() @ipmitool.execute_ipmi_cmd() def _get_sdr_info(self): """Get the SDR info.""" return IPMICMD['sdr_info'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_all(self): """Get the sensor data for type.""" return IPMICMD['sensor_dump'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_temperature(self): """Get the sensor data for Temperature.""" return IPMICMD['sensor_dump_temperature'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_voltage(self): """Get the sensor data for Voltage.""" return IPMICMD['sensor_dump_voltage'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_power(self): """Get the sensor data for Power.""" return IPMICMD['sensor_dump_power'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_current(self): """Get the sensor data for Current.""" return IPMICMD['sensor_dump_current'] @ipmitool.execute_ipmi_cmd(DICT_TRANSLATE_TEMPLATE) def _read_sensor_fan(self): """Get the sensor data for Fan.""" return IPMICMD['sensor_dump_fan'] def read_sensor_any(self, sensor_type=''): """Get the sensor data for type.""" if not self.ipmi_support: return {} mapping = {'': self._read_sensor_all, 'Temperature': self._read_sensor_temperature, 'Fan': self._read_sensor_fan, 'Voltage': self._read_sensor_voltage, 'Current': self._read_sensor_current, 'Power': self._read_sensor_power} try: return mapping[sensor_type]() except KeyError: raise ipmiexcept.IPMIException(_('Wrong sensor type')) def check_ipmi(self): """IPMI capability checking This function is used to detect if compute node is IPMI capable platform. Just run a simple IPMI command to get SDR info for check. """ try: self._get_sdr_info() except ipmiexcept.IPMIException: return False return True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1573567 ceilometer-27.0.0.0rc1/ceilometer/ipmi/platform/ipmitool.py0000664000175000017500000001064115250267452022450 0ustar00zuulzuul# Copyright 2014 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Utils to run ipmitool for data collection""" from oslo_concurrency import processutils from ceilometer.i18n import _ from ceilometer.ipmi.platform import exception as ipmiexcept import ceilometer.privsep.ipmitool import shlex # Following 2 functions are copied from ironic project to handle ipmitool's # sensor data output. Need code clean and sharing in future. # Check ironic/drivers/modules/ipmitool.py def _get_sensor_type(sensor_data_dict): # Have only three sensor type name IDs: 'Sensor Type (Analog)' # 'Sensor Type (Discrete)' and 'Sensor Type (Threshold)' for key in ('Sensor Type (Analog)', 'Sensor Type (Discrete)', 'Sensor Type (Threshold)'): try: return sensor_data_dict[key].split(' ', 1)[0] except KeyError: continue raise ipmiexcept.IPMIException(_("parse IPMI sensor data failed," "unknown sensor type")) def _process_sensor(sensor_data): sensor_data_fields = sensor_data.split('\n') sensor_data_dict = {} for field in sensor_data_fields: if not field: continue kv_value = field.split(':') if len(kv_value) != 2: continue sensor_data_dict[kv_value[0].strip()] = kv_value[1].strip() return sensor_data_dict def _translate_output(output): """Translate the return value into JSON dict :param output: output of the execution of IPMI command(sensor reading) """ sensors_data_dict = {} sensors_data_array = output.split('\n\n') for sensor_data in sensors_data_array: sensor_data_dict = _process_sensor(sensor_data) if not sensor_data_dict: continue sensor_type = _get_sensor_type(sensor_data_dict) # ignore the sensors which have no current 'Sensor Reading' data sensor_id = sensor_data_dict['Sensor ID'] if 'Sensor Reading' in sensor_data_dict: sensors_data_dict.setdefault(sensor_type, {})[sensor_id] = sensor_data_dict # get nothing, no valid sensor data if not sensors_data_dict: raise ipmiexcept.IPMIException(_("parse IPMI sensor data failed," "No data retrieved from given input")) return sensors_data_dict def _parse_output(output, template): """Parse the return value of IPMI command into dict :param output: output of the execution of IPMI command :param template: a dict that contains the expected items of IPMI command and its length. """ ret = {} index = 0 if not (output and template): return ret if "translate" in template: ret = _translate_output(output) else: output_list = output.strip().replace('\n', '').split(' ') if sum(template.values()) != len(output_list): raise ipmiexcept.IPMIException(_("ipmitool output " "length mismatch")) for item in template.items(): index_end = index + item[1] update_value = output_list[index: index_end] ret[item[0]] = update_value index = index_end return ret def execute_ipmi_cmd(template=None): """Decorator for the execution of IPMI command. It parses the output of IPMI command into dictionary. """ template = template or [] def _execute_ipmi_cmd(f): def _execute(self, **kwargs): args = ['ipmitool'] command = f(self, **kwargs) args.extend(shlex.split(command)) try: (out, __) = ceilometer.privsep.ipmitool.ipmi(*args) except processutils.ProcessExecutionError: raise ipmiexcept.IPMIException(_("running ipmitool failure")) return _parse_output(out, template) return _execute return _execute_ipmi_cmd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2411275 ceilometer-27.0.0.0rc1/ceilometer/ipmi/pollsters/0000775000175000017500000000000015250267567020452 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1583567 ceilometer-27.0.0.0rc1/ceilometer/ipmi/pollsters/__init__.py0000664000175000017500000000167415250267452022564 0ustar00zuulzuul# Copyright 2014 Intel Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Pollsters for IPMI and Intel Node Manager """ from oslo_config import cfg OPTS = [ cfg.IntOpt('polling_retry', default=3, help='Tolerance of IPMI/NM polling failures ' 'before disable this pollster. ' 'Negative indicates retrying forever.') ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1583567 ceilometer-27.0.0.0rc1/ceilometer/ipmi/pollsters/sensor.py0000664000175000017500000001145415250267452022333 0ustar00zuulzuul# Copyright 2014 Intel # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.ipmi.notifications import ironic as parser from ceilometer.ipmi.platform import exception as ipmiexcept from ceilometer.ipmi.platform import ipmi_sensor from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) class InvalidSensorData(ValueError): pass class SensorPollster(plugin_base.PollsterBase): METRIC = None def setup_environment(self): super().setup_environment() self.ipmi = ipmi_sensor.IPMISensor() self.polling_failures = 0 # Do not load this extension if no IPMI support if not self.ipmi.ipmi_support: raise plugin_base.ExtensionLoadError( "IPMITool not supported on host") @property def default_discovery(self): return 'local_node' @staticmethod def _get_sensor_types(data, sensor_type): # Ipmitool reports 'Pwr Consumption' as sensor type 'Current'. # Set sensor_type to 'Current' when polling 'Power' metrics. if sensor_type == 'Power': sensor_type = 'Current' try: return (sensor_type_data for _, sensor_type_data in data[sensor_type].items()) except KeyError: return [] def get_samples(self, manager, cache, resources): # Only one resource for IPMI pollster try: stats = self.ipmi.read_sensor_any(self.METRIC) except ipmiexcept.IPMIException: self.polling_failures += 1 LOG.warning( 'Polling %(mtr)s sensor failed for %(cnt)s times!', {'mtr': self.METRIC, 'cnt': self.polling_failures}) if 0 <= self.conf.ipmi.polling_retry < self.polling_failures: LOG.warning('Pollster for %s is disabled!', self.METRIC) raise plugin_base.PollsterPermanentError(resources) else: return self.polling_failures = 0 sensor_type_data = self._get_sensor_types(stats, self.METRIC) for sensor_data in sensor_type_data: # Continue if sensor_data is not parseable. try: sensor_reading = sensor_data['Sensor Reading'] sensor_id = sensor_data['Sensor ID'] except KeyError: continue # Do not pick up power consumption metrics from 'Current' sensor if self.METRIC == 'Current' and 'Pwr Consumption' in sensor_id: continue if not parser.validate_reading(sensor_reading): continue try: volume, unit = parser.parse_reading(sensor_reading) except parser.InvalidSensorData: continue resource_id = '%(host)s-%(sensor-id)s' % { 'host': self.conf.host, 'sensor-id': parser.transform_id(sensor_id) } metadata = { 'node': self.conf.host } extra_metadata = self.get_extra_sensor_metadata(sensor_data) if extra_metadata: metadata.update(extra_metadata) yield sample.Sample( name='hardware.ipmi.%s' % self.METRIC.lower(), type=sample.TYPE_GAUGE, unit=unit, volume=volume, user_id=None, project_id=None, resource_id=resource_id, resource_metadata=metadata) def get_extra_sensor_metadata(self, sensor_data): # override get_extra_sensor_metadata to add specific metrics for # each sensor return {} class TemperatureSensorPollster(SensorPollster): METRIC = 'Temperature' class CurrentSensorPollster(SensorPollster): METRIC = 'Current' class FanSensorPollster(SensorPollster): METRIC = 'Fan' def get_extra_sensor_metadata(self, sensor_data): try: return { "maximum_rpm": sensor_data['Normal Maximum'], } except KeyError: # Maximum rpm might not be reported when usage # is reported as percent return {} class VoltageSensorPollster(SensorPollster): METRIC = 'Voltage' class PowerSensorPollster(SensorPollster): METRIC = 'Power' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1593568 ceilometer-27.0.0.0rc1/ceilometer/keystone_client.py0000664000175000017500000002331115250267452021227 0ustar00zuulzuul# # Copyright 2015 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import dataclasses import os from keystoneauth1 import loading as ka_loading from openstack import connection from oslo_config import cfg from ceilometer import exceptions as ceilo_exc DEFAULT_GROUP = "service_credentials" # List of group that can set auth_section to use a different # credentials section OVERRIDABLE_GROUPS = ['gnocchi', 'zaqar'] @dataclasses.dataclass(frozen=True) class Project: id: str name: str domain_id: str is_enabled: bool description: str = '' parent_id: str | None = None is_domain: bool = False @classmethod def from_openstacksdk(cls, sdk_project): return cls( id=sdk_project.id, name=sdk_project.name, domain_id=sdk_project.domain_id, is_enabled=sdk_project.is_enabled, description=sdk_project.description or '', parent_id=sdk_project.parent_id, is_domain=sdk_project.is_domain, ) @dataclasses.dataclass(frozen=True) class Domain: id: str name: str is_enabled: bool description: str = '' @classmethod def from_openstacksdk(cls, sdk_domain): return cls( id=sdk_domain.id, name=sdk_domain.name, is_enabled=sdk_domain.is_enabled, description=sdk_domain.description or '', ) class Client: """Client for retrieving keystone resources. Accesses Projects and Domains from keystone service. """ def __init__(self, session, **kwargs): """Instantiate a Client that can access keystone. :param session: A ``keystoneauth1.session.Session`` instance used for all HTTP communication. :param kwargs: Additional keyword arguments forwarded verbatim to ``openstack.connection.Connection`` (e.g. ``interface``, ``region_name``). """ self._connection = connection.Connection( session=session, service_types={"identity"}, **kwargs ) self.session = session def find_project(self, **kwargs): """Find a single project matching the given attribute filters. Delegates to the OpenStack SDK connection via ``search_projects(name_or_id, filters, domain_id)``. :param kwargs: Attribute filters used to locate the project, e.g. ``name='myproject'``, ``domain_id=''``. All keyword arguments are forwarded to the underlying SDK call. :returns: A single ``Project`` resource object whose attributes match all supplied filters. :raises ceilometer.exceptions.NotFound: if no project matches the filters. :raises ceilometer.exceptions.NoUniqueMatch: if more than one project matches the filters. """ filters = dict(**kwargs) name_or_id = filters.pop('id', None) if name_or_id is None: name_or_id = filters.pop('name', None) domain_id = filters.pop('domain_id', None) project = self._connection.search_projects( name_or_id=name_or_id, filters=filters or None, domain_id=domain_id) if len(project) > 1: raise ceilo_exc.NoUniqueMatch("ClientException") if len(project) == 0: raise ceilo_exc.NotFound( "No matching resources found", f"No Project matching {dict(**kwargs)}.") return Project.from_openstacksdk(project[0]) def list_projects(self, domain, **filters): """List projects within a domain, with optional attribute filters. :param domain: The domain whose projects should be returned. Accepts either a domain ID string or a :class:`Domain` object. :param filters: Optional keyword arguments used as additional query filters, e.g. ``enabled=True`` to restrict results to enabled projects. :returns: A list of :class:`Project` objects belonging to the given domain. Returns an empty list when no projects match. """ # domain can be a string. domain_id = getattr(domain, 'id', domain) projects = self._connection.list_projects( domain_id=domain_id, filters=filters or None) return [Project.from_openstacksdk(p) for p in projects] def find_domain(self, **kwargs): """Find a single domain matching the given attribute filters. :param kwargs: Attribute filters used to locate the domain, e.g. ``name='Default'``. :returns: A single :class:`Domain` object whose attributes match all supplied filters. :raises ceilometer.exceptions.NotFound: if no domain matches the filters. :raises ceilometer.exceptions.NoUniqueMatch: if more than one domain matches the filters. """ filters = dict(**kwargs) name_or_id = filters.pop('id', None) if name_or_id is None: name_or_id = filters.pop('name', None) domains = self._connection.search_domains( name_or_id=name_or_id, filters=filters or None) if len(domains) > 1: raise ceilo_exc.NoUniqueMatch("ClientException") if len(domains) == 0: raise ceilo_exc.NotFound( "No matching resources found", f"No Domain matching {dict(kwargs)}.") return Domain.from_openstacksdk(domains[0]) def list_domains(self, **filters): """List all domains, with optional attribute filters. :param filters: Optional keyword arguments used as query filters, e.g. ``enabled=True`` to restrict results to enabled domains. :returns: A list of :class:`Domain` objects. """ return [Domain.from_openstacksdk(d) for d in self._connection.list_domains( filters=filters or None)] def get_session(conf, requests_session=None, group=None, timeout=None): """Get a ceilometer service credentials auth session.""" group = group or DEFAULT_GROUP auth_plugin = ka_loading.load_auth_from_conf_options(conf, group) kwargs = {'auth': auth_plugin, 'session': requests_session} if timeout is not None: kwargs['timeout'] = timeout session = ka_loading.load_session_from_conf_options(conf, group, **kwargs) return session def get_connection( conf, service_type="identity", requests_session=None, group=DEFAULT_GROUP): """Get an openstacksdk connection to interact with openstack services.""" sess = get_session( conf, requests_session=requests_session, group=group) # https://github.com/openstack/nova/blob/master/nova/utils.py#L905 conn = connection.Connection( session=sess, oslo_conf=conf, service_types={service_type} ) return conn def get_client(conf, requests_session=None, group=DEFAULT_GROUP): """Return a client for keystone v3 endpoint.""" session = get_session(conf, requests_session=requests_session, group=group) return Client(session=session, interface=conf[group].interface, region_name=conf[group].region_name) def get_service_catalog(client): return client.session.auth.get_access(client.session).service_catalog def url_for( client, service_type=None, service_name=None, interface=None, region_name=None): return get_service_catalog(client).url_for( service_type=service_type, service_name=service_name, interface=interface, region_name=region_name) def get_urls( client, service_type=None, service_name=None, interface=None, region_name=None): return get_service_catalog(client).get_urls( service_type=service_type, service_name=service_name, interface=interface, region_name=region_name) def get_auth_token(client): return client.session.auth.get_access(client.session).auth_token CLI_OPTS = [ cfg.StrOpt('region-name', default=os.environ.get('OS_REGION_NAME'), help='Region name to use for OpenStack service endpoints.'), cfg.StrOpt('interface', default=os.environ.get('OS_INTERFACE', 'public'), choices=('public', 'internal', 'admin', 'publicURL', 'internalURL', 'adminURL'), help='Type of endpoint in Identity service catalog to use for ' 'communication with OpenStack services.'), ] def register_keystoneauth_opts(conf): _register_keystoneauth_group(conf, DEFAULT_GROUP) for group in OVERRIDABLE_GROUPS: _register_keystoneauth_group(conf, group) conf.set_default('auth_section', DEFAULT_GROUP, group=group) def _register_keystoneauth_group(conf, group): ka_loading.register_auth_conf_options(conf, group) ka_loading.register_session_conf_options(conf, group) conf.register_opts(CLI_OPTS, group=group) def post_register_keystoneauth_opts(conf): for group in OVERRIDABLE_GROUPS: if conf[group].auth_section != DEFAULT_GROUP: # NOTE(sileht): We register this again after the auth_section have # been read from the configuration file _register_keystoneauth_group(conf, conf[group].auth_section) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2421277 ceilometer-27.0.0.0rc1/ceilometer/load_balancer/0000775000175000017500000000000015250267567020233 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1593568 ceilometer-27.0.0.0rc1/ceilometer/load_balancer/__init__.py0000664000175000017500000000000015250267452022323 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1603568 ceilometer-27.0.0.0rc1/ceilometer/load_balancer/discovery.py0000664000175000017500000000204715250267452022610 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import octavia_client from ceilometer.polling import plugin_base class LoadBalancerDiscovery(plugin_base.DiscoveryBase): """Discovery class for Octavia load balancers.""" KEYSTONE_REQUIRED_FOR_SERVICE = 'octavia' def __init__(self, conf): super().__init__(conf) self.octavia_cli = octavia_client.Client(conf) def discover(self, manager, param=None): """Discover load balancer resources to monitor.""" return self.octavia_cli.loadbalancers_list() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1603568 ceilometer-27.0.0.0rc1/ceilometer/load_balancer/octavia.py0000664000175000017500000000777215250267452022241 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) # Octavia operating_status values mapped to numeric values OPERATING_STATUS = { 'online': 1, 'draining': 2, 'offline': 3, 'degraded': 4, 'error': 5, 'no_monitor': 6, } # Octavia provisioning_status values mapped to numeric values PROVISIONING_STATUS = { 'active': 1, 'deleted': 2, 'error': 3, 'pending_create': 4, 'pending_update': 5, 'pending_delete': 6, } class _BaseLoadBalancerPollster(plugin_base.PollsterBase): """Base pollster for Octavia load balancer metrics.""" FIELDS = ['name', 'availability_zone', 'vip_address', 'vip_port_id', 'provisioning_status', 'operating_status', 'provider', 'flavor_id', ] @property def default_discovery(self): return 'lb_services' @staticmethod def extract_metadata(lb): return {k: getattr(lb, k, None) for k in _BaseLoadBalancerPollster.FIELDS} class LoadBalancerOperatingStatusPollster(_BaseLoadBalancerPollster): """Pollster for Octavia load balancer operating status.""" @staticmethod def get_status_id(value): if not value: return -1 status = value.lower() return OPERATING_STATUS.get(status, -1) def get_samples(self, manager, cache, resources): for lb in resources or []: LOG.debug("LOAD BALANCER: %s", lb) status = self.get_status_id(lb.operating_status) if status == -1: LOG.warning( "Unknown operating status %(status)s for load balancer " "%(name)s (%(id)s), setting volume to -1", {"status": lb.operating_status, "name": lb.name, "id": lb.id}) yield sample.Sample( name='loadbalancer.operating', type=sample.TYPE_GAUGE, unit='status', volume=status, user_id=None, project_id=lb.project_id, resource_id=lb.id, resource_metadata=self.extract_metadata(lb) ) class LoadBalancerProvisioningStatusPollster(_BaseLoadBalancerPollster): """Pollster for Octavia load balancer provisioning status.""" @staticmethod def get_status_id(value): if not value: return -1 status = value.lower() return PROVISIONING_STATUS.get(status, -1) def get_samples(self, manager, cache, resources): for lb in resources or []: LOG.debug("LOAD BALANCER: %s", lb) status = self.get_status_id(lb.provisioning_status) if status == -1: LOG.warning( "Unknown provisioning status %(status)s for load " "balancer %(name)s (%(id)s), setting volume to -1", {"status": lb.provisioning_status, "name": lb.name, "id": lb.id}) yield sample.Sample( name='loadbalancer.provisioning', type=sample.TYPE_GAUGE, unit='status', volume=status, user_id=None, project_id=lb.project_id, resource_id=lb.id, resource_metadata=self.extract_metadata(lb) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2111278 ceilometer-27.0.0.0rc1/ceilometer/locale/0000775000175000017500000000000015250267567016724 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2091277 ceilometer-27.0.0.0rc1/ceilometer/locale/de/0000775000175000017500000000000015250267567017314 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2421277 ceilometer-27.0.0.0rc1/ceilometer/locale/de/LC_MESSAGES/0000775000175000017500000000000015250267567021101 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1613567 ceilometer-27.0.0.0rc1/ceilometer/locale/de/LC_MESSAGES/ceilometer.po0000664000175000017500000000766115250267452023574 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Carsten Duch , 2014 # Christian Berendt , 2014 # Ettore Atalan , 2014 # Andreas Jaeger , 2016. #zanata # Andreas Jaeger , 2018. #zanata # Andreas Jaeger , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-10-03 08:55+0000\n" "Last-Translator: Andreas Jaeger \n" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: German\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Fehler von libvirt während Suche nach Instanz : " "[Fehlercode %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "Fehler beim Überprüfen von Daten der Instanz , " "Domänenstatus ist ABGESCHALTET." #, python-format msgid "" "Failed to inspect instance %(instance_uuid)s stats, can not get info from " "libvirt: %(error)s" msgstr "" "Fehler beim Überprüfen der Statistik der Instanz %(instance_uuid)s, " "Informationen können nicht von libvirt abgerufen werden: %(error)s" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Ungültige YAML-Syntax in Definitionsdatei %(file)s in Zeile: %(line)s, " "Spalte: %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "Ungültiger Traittyp '%(type)s' für Trait %(trait)s" #, python-format msgid "Invalid type %s specified" msgstr "Ungültiger Typ %s angegeben" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "Kein Plug-in mit dem Namen %(plugin)s verfügbar für %(name)s." #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Analysefehler in JSONPath-Spezifikation '%(jsonpath)s' für %(name)s: %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Plug-in angegeben, aber kein Plug-in-Name für %s angegeben." #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW-AdminOps-API hat Folgendes zurückgegeben: %(status)s %(reason)s" #, python-format msgid "Required field %(field)s should be a %(type)s" msgstr "Erforderliches Feld %(field)s muss %(type)s sein" #, python-format msgid "Required field %s not specified" msgstr "Erforderliches Feld %s nicht angegeben" #, python-format msgid "Required fields %s not specified" msgstr "Erforderliche Felder %s nicht angegeben." #, python-format msgid "The field 'fields' is required for %s" msgstr "Das Feld 'fields' ist erforderlich für %s" msgid "Wrong sensor type" msgstr "Falscher Sensortyp" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "YAML-Fehler beim Lesen von Definitionsdatei %(file)s." msgid "ipmitool output length mismatch" msgstr "Abweichung bei ipmitool-Ausgabelänge" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "Analyse von IPMI-Sensordaten fehlgeschlagen, keine Daten von angegebener " "Eingabe abgerufen" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "Analyse von IPMI-Sensordaten fehlgeschlagen, unbekannter Sensortyp" msgid "running ipmitool failure" msgstr "Fehler beim Ausführen von ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2091277 ceilometer-27.0.0.0rc1/ceilometer/locale/en_GB/0000775000175000017500000000000015250267567017676 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2421277 ceilometer-27.0.0.0rc1/ceilometer/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000015250267567021463 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1623569 ceilometer-27.0.0.0rc1/ceilometer/locale/en_GB/LC_MESSAGES/ceilometer.po0000664000175000017500000000725115250267452024151 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Andi Chandler , 2013-2014 # Andreas Jaeger , 2016. #zanata # Andi Chandler , 2017. #zanata # Andi Chandler , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-11-14 11:07+0000\n" "Last-Translator: Andi Chandler \n" "Language: en_GB\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: English (United Kingdom)\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "Failed to inspect data of instance , domain state " "is SHUTOFF." #, python-format msgid "" "Failed to inspect instance %(instance_uuid)s stats, can not get info from " "libvirt: %(error)s" msgstr "" "Failed to inspect instance %(instance_uuid)s stats, can not get info from " "libvirt: %(error)s" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "Invalid trait type '%(type)s' for trait %(trait)s" #, python-format msgid "Invalid type %s specified" msgstr "Invalid type %s specified" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "No plugin named %(plugin)s available for %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Plugin specified, but no plugin name supplied for %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW AdminOps API returned %(status)s %(reason)s" #, python-format msgid "Required field %(field)s should be a %(type)s" msgstr "Required field %(field)s should be a %(type)s" #, python-format msgid "Required field %s not specified" msgstr "Required field %s not specified" #, python-format msgid "Required fields %s not specified" msgstr "Required fields %s not specified" msgid "Sample Check" msgstr "Sample Check" #, python-format msgid "The field 'fields' is required for %s" msgstr "The field 'fields' is required for %s" msgid "Wrong sensor type" msgstr "Wrong sensor type" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "YAML error reading Definitions file %(file)s" msgid "ipmitool output length mismatch" msgstr "ipmitool output length mismatch" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "parse IPMI sensor data failed,No data retrieved from given input" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "parse IPMI sensor data failed,unknown sensor type" msgid "running ipmitool failure" msgstr "running ipmitool failure" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2091277 ceilometer-27.0.0.0rc1/ceilometer/locale/es/0000775000175000017500000000000015250267567017333 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2431276 ceilometer-27.0.0.0rc1/ceilometer/locale/es/LC_MESSAGES/0000775000175000017500000000000015250267567021120 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1633568 ceilometer-27.0.0.0rc1/ceilometer/locale/es/LC_MESSAGES/ceilometer.po0000664000175000017500000000636615250267452023614 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Rafael Rivero , 2015 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:26+0000\n" "Last-Translator: Copied by Zanata \n" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Spanish\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Error de libvirt al buscar la instancia : [Código " "de error %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "No se han podido analizar los datos de la instancia , el estado del dominio es SHUTOFF." #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Sintaxis de YAML no válida en archivo de definiciones %(file)s en la línea: " "%(line)s, columna: %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "Tipo de rasgo no válido '%(type)s' para el rasgo %(trait)s" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "No hay ningún plug-in denominado %(plugin)s disponible para %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Error de análisis en especificación de JSONPath '%(jsonpath)s' para " "%(name)s: %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "" "Se ha especificado un plug-in, pero no se ha proporcionado ningún nombre de " "plug-in para %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "La API de RGW AdminOps ha devuelto %(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "Campo necesario %s no especificado" #, python-format msgid "The field 'fields' is required for %s" msgstr "El campo 'campos' es obligatorio para %s" msgid "Wrong sensor type" msgstr "Tipo de sensor incorrecto" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "Error de YAML al leer el archivo de definiciones %(file)s" msgid "ipmitool output length mismatch" msgstr "la longitud de salida de ipmitool no coincide" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "ha fallado el análisis de datos de sensor IPMI,no se ha recuperado ningún " "dato de la entrada" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "" "ha fallado el análisis de datos de sensor IPMI,tipo de sensor desconocido" msgid "running ipmitool failure" msgstr "fallo de ejecución de ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2091277 ceilometer-27.0.0.0rc1/ceilometer/locale/fr/0000775000175000017500000000000015250267567017333 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2431276 ceilometer-27.0.0.0rc1/ceilometer/locale/fr/LC_MESSAGES/0000775000175000017500000000000015250267567021120 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1633568 ceilometer-27.0.0.0rc1/ceilometer/locale/fr/LC_MESSAGES/ceilometer.po0000664000175000017500000001011215250267452023574 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Corinne Verheyde , 2013 # CHABERT Loic , 2013 # Christophe kryskool , 2013 # Corinne Verheyde , 2013-2014 # EVEILLARD , 2013-2014 # Francesco Vollero , 2015 # Jonathan Dupart , 2014 # CHABERT Loic , 2013 # Maxime COQUEREL , 2014 # Nick Barcet , 2013 # Nick Barcet , 2013 # Andrew Melim , 2014 # Patrice LACHANCE , 2013 # Patrice LACHANCE , 2013 # Rémi Le Trocquer , 2014 # EVEILLARD , 2013 # Corinne Verheyde , 2013 # Corinne Verheyde , 2013 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:26+0000\n" "Last-Translator: Copied by Zanata \n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: French\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Erreur de libvirt lors de la recherche de l'instance : [Code d'erreur %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "Echec de l'inspection des données de l'instance . " "Le domaine est à l'état SHUTOFF (INTERRUPTION)." #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Syntaxe YAML non valide dans le fichier de définitions %(file)s à la ligne : " "%(line)s, colonne : %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "Type de trait non valide '%(type)s' pour le trait %(trait)s" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "Aucun plugin nommé %(plugin)s n'est disponible pour %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Erreur d'analyse dans la spécification JSONPath '%(jsonpath)s' pour " "%(name)s : %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Plugin spécifié, mais aucun nom de plugin n'est fourni pour %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "L'API AdminOps RGW a renvoyé %(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "Champ requis %s non spécifiée" #, python-format msgid "The field 'fields' is required for %s" msgstr "Le champ 'fields' est requis pour %s" msgid "Wrong sensor type" msgstr "Type de détecteur incorrect" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "Erreur YAML lors de la lecture du fichier de définitions %(file)s" msgid "ipmitool output length mismatch" msgstr "Non-concordance de longueur de la sortie ipmitool" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "Echec de l'analyse des données du détecteur IPMI, aucune donnée extraite à " "partir de l'entrée fournie" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "" "Echec de l'analyse des données du détecteur IPMI, type de détecteur inconnu" msgid "running ipmitool failure" msgstr "Echec d'exécution d'ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2101276 ceilometer-27.0.0.0rc1/ceilometer/locale/it/0000775000175000017500000000000015250267567017340 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2431276 ceilometer-27.0.0.0rc1/ceilometer/locale/it/LC_MESSAGES/0000775000175000017500000000000015250267567021125 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1643567 ceilometer-27.0.0.0rc1/ceilometer/locale/it/LC_MESSAGES/ceilometer.po0000664000175000017500000000631415250267452023612 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Stefano Maffulli , 2013 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:26+0000\n" "Last-Translator: Copied by Zanata \n" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Italian\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Errore da libvirt durante la ricerca dell'istanza : [Codice di errore %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "Impossibile ispezionare i dati dell'istanza , " "stato dominio SHUTOFF." #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Sintassi YAML non valida nel file delle definizioni %(file)s alla riga: " "%(line)s, colonna: %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "" "Tipo di caratteristica non valido '%(type)s' per la caratteristica %(trait)s" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "Nessun plug-in con nome %(plugin)s disponibile per %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Errore di analisi nella specifica JSONPath '%(jsonpath)s' per %(name)s: " "%(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Plug-in specificato, ma nessun nome di plug-in fornito per %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "L'API RGW AdminOps ha restituito %(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "Campo richiesto %s non specificato" #, python-format msgid "The field 'fields' is required for %s" msgstr "Il campo 'fields' è obbligatorio per %s" msgid "Wrong sensor type" msgstr "Tipo di sensore errato" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "Errore YAML durante la lettura del file definizioni %(file)s" msgid "ipmitool output length mismatch" msgstr "mancata corrispondenza della lunghezza dell'output ipmitool" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "analisi dei dati del sensore IPMI non riuscita, nessun dato recuperato " "dall'input fornito" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "" "analisi dei dati del sensore IPMI non riuscita, tipo di sensore sconosciuto" msgid "running ipmitool failure" msgstr "errore nell'esecuzione ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2101276 ceilometer-27.0.0.0rc1/ceilometer/locale/ja/0000775000175000017500000000000015250267567017316 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2431276 ceilometer-27.0.0.0rc1/ceilometer/locale/ja/LC_MESSAGES/0000775000175000017500000000000015250267567021103 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1653566 ceilometer-27.0.0.0rc1/ceilometer/locale/ja/LC_MESSAGES/ceilometer.po0000664000175000017500000000670415250267452023573 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Tomoyuki KATO , 2013 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:26+0000\n" "Last-Translator: Copied by Zanata \n" "Language: ja\n" "Plural-Forms: nplurals=1; plural=0;\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Japanese\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "インスタンス �検索中� libvirt �エラー�発生��" "��: [エラーコード %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "インスタンス �データを検査����ん���。ドメ" "イン状態� SHUTOFF ��。" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "%(line)s 行目� %(column)s 列�定義ファイル %(file)s � YAML 構文 �無効�" "�。" #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "特性 %(trait)s �特性タイプ '%(type)s' �無効��" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "%(name)s �使用��る %(plugin)s ������プラグイン��り��ん" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "%(name)s �関�る JSONPath �指定 '%(jsonpath)s' �エラーを解����: " "%(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "プラグイン�指定�れ������%s �プラグイン���供�れ����ん" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW AdminOps API �ら %(status)s %(reason)s �返�れ���" #, python-format msgid "Required field %s not specified" msgstr "必須フィールド %s �指定�れ����ん" #, python-format msgid "The field 'fields' is required for %s" msgstr "%s ��フィールド 'fields' �必���" msgid "Wrong sensor type" msgstr "センサー種別�正���り��ん" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "定義ファイル %(file)s ��読��り� YAML エラー" msgid "ipmitool output length mismatch" msgstr "ipmitool 出力�長��一致���ん" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "IPMI センサーデータ�解��失敗����。指定�れ�入力�らデータ��得�れ�" "�ん���" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "IPMI センサーデータ�解��失敗����。�明�センサー種別��。" msgid "running ipmitool failure" msgstr "ipmitool �実行�失敗����" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2101276 ceilometer-27.0.0.0rc1/ceilometer/locale/ko_KR/0000775000175000017500000000000015250267567017731 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2431276 ceilometer-27.0.0.0rc1/ceilometer/locale/ko_KR/LC_MESSAGES/0000775000175000017500000000000015250267567021516 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1663568 ceilometer-27.0.0.0rc1/ceilometer/locale/ko_KR/LC_MESSAGES/ceilometer.po0000664000175000017500000000717615250267452024212 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Seong-ho Cho , 2014 # Seunghyo Chun , 2013 # Seunghyo Chun , 2013 # Sungjin Kang , 2013 # Sungjin Kang , 2013 # Andreas Jaeger , 2016. #zanata # Lee Jongwon , 2020. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2020-10-05 01:55+0000\n" "Last-Translator: Lee Jongwon \n" "Language: ko_KR\n" "Plural-Forms: nplurals=1; plural=0;\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Korean (South Korea)\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "�스턴스 검색 중 libvirt�서 오류 발�: [오류 코" "드 %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "�스턴스 <�름=%(name)s, id=%(id)s>� ��터 검사 실패, �메� �태가 SHUTOFF" "입니다." #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "다��서 정� 파� %(file)s� 올바르지 않� YAML 구문: 행: %(line)s, 열: " "%(column)s" #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "특성 %(trait)s� 대한 올바르지 않� 특성 유형 '%(type)s'" #, python-format msgid "Invalid type %s specified" msgstr "올바르지 않� 유형 %s�(가) 지정�" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "%(name)s� 대해 %(plugin)s(�)�는 플러그�� 사용할 수 없�" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" " %(name)s� 대한 JSONPath 스펙 '%(jsonpath)s'� 구문 분� 오류: %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "플러그�� 지정�지 않았지만, %s� 플러그� �름� 제공�지 않�" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW AdminOps API가 %(status)s %(reason)s�(를) 리턴함" #, python-format msgid "Required field %s not specified" msgstr "필수 필드 %s�(가) 지정�지 않�" msgid "Sample Check" msgstr "샘플 체�" #, python-format msgid "The field 'fields' is required for %s" msgstr "%s� 'fields' 필드 필요" msgid "Wrong sensor type" msgstr "잘못� 센서 유형" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "정� 파� %(file)s�(를) �는 중� YAML 오류 발�" msgid "ipmitool output length mismatch" msgstr "ipmitool 출력 길� 불�치" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "IPMI 센서 ��터 구문 분�� 실패했�, 제공� 입력�서 검색� ��터가 없�" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "IPMI 센서 ��터 구문 분�� 실패했�, 알 수 없는 센서 유형" msgid "running ipmitool failure" msgstr "ipmitool 실행 실패" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2101276 ceilometer-27.0.0.0rc1/ceilometer/locale/pt_BR/0000775000175000017500000000000015250267567017732 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2441275 ceilometer-27.0.0.0rc1/ceilometer/locale/pt_BR/LC_MESSAGES/0000775000175000017500000000000015250267567021517 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1673567 ceilometer-27.0.0.0rc1/ceilometer/locale/pt_BR/LC_MESSAGES/ceilometer.po0000664000175000017500000000625015250267452024203 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Gabriel Wainer, 2013 # Gabriel Wainer, 2013 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:27+0000\n" "Last-Translator: Copied by Zanata \n" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Portuguese (Brazil)\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Erro de libvirt ao consultar instância : [Código " "de Erro %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "Falha ao inspecionar os dados da instância , " "estado do domínio é SHUTOFF." #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "Sintaxe YAML inválida no arquivo de definições %(file)s na linha: %(line)s, " "coluna: %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "Tipo de traço inválido '%(type)s' para traço %(trait)s" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "Nenhum plug-in nomeado %(plugin)s disponível para %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Erro de análise na especificação JSONPath '%(jsonpath)s' para %(name)s: " "%(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Plug-in especificado, mas nenhum nome de plug-in fornecido para %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "A API AdminOps RGW retornou %(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "Campo obrigatório %s não especificado" #, python-format msgid "The field 'fields' is required for %s" msgstr "O campo 'fields' é necessário para %s" msgid "Wrong sensor type" msgstr "Tipo de sensor errado" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "Erro YAML ao ler o arquivo de definições %(file)s" msgid "ipmitool output length mismatch" msgstr "incompatibilidade no comprimento da saída de ipmitool" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "análise dos dados do sensor IPMI com falha, nenhum dado recuperado da " "entrada fornecida" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "análise dos dados do sensor IPMI com falha,tipo de sensor desconhecido" msgid "running ipmitool failure" msgstr "executando falha de ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2111278 ceilometer-27.0.0.0rc1/ceilometer/locale/ru/0000775000175000017500000000000015250267567017352 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2441275 ceilometer-27.0.0.0rc1/ceilometer/locale/ru/LC_MESSAGES/0000775000175000017500000000000015250267567021137 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1673567 ceilometer-27.0.0.0rc1/ceilometer/locale/ru/LC_MESSAGES/ceilometer.po0000664000175000017500000001075615250267452023631 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Andreas Jaeger , 2016. #zanata # Roman Gorshunov , 2021. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2021-09-06 03:48+0000\n" "Last-Translator: Roman Gorshunov \n" "Language: ru\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || " "(n%100>=11 && n%100<=14)? 2 : 3);\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Russian\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "Возникла ошибка в libvirt при пои�ке �кземпл�ра <им�=%(name)s, ИД=%(id)s>: " "[Код ошибки: %(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "�е удало�ь проверить данные �кземпл�ра <им�=%(name)s, ИД=%(id)s>, �о�то�ние " "домена - SHUTOFF." #, python-format msgid "" "Failed to inspect instance %(instance_uuid)s stats, can not get info from " "libvirt: %(error)s" msgstr "" "�е удало�ь проверить �тати�тику ин�тан�а %(instance_uuid)s, не удало�ь " "получить информацию от libvirt: %(error)s" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "" "�едопу�тимый �интак�и� YAML в файле определений %(file)s; �трока: %(line)s, " "�толбец: %(column)s." #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "�едопу�тимый тип о�обенно�ти %(type)s дл� о�обенно�ти %(trait)s" #, python-format msgid "Invalid type %s specified" msgstr "Указан недопу�тимый тип %s" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "�ет до�тупного модул� %(plugin)s дл� %(name)s" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "" "Ошибка анализа �пецификации JSONPath %(jsonpath)s дл� %(name)s: %(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "Указан модуль, но не передано им� модул� дл� %s" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "Функци� API RGW AdminOps вернула %(status)s %(reason)s" #, python-format msgid "Required field %(field)s should be a %(type)s" msgstr "Об�зательное поле %(field)s должно быть типа %(type)s" #, python-format msgid "Required field %s not specified" msgstr "�е указано об�зательное поле %s" #, python-format msgid "Required fields %s not specified" msgstr "�е указаны об�зательные пол� %s" msgid "Sample Check" msgstr "Те�това� проверка" #, python-format msgid "The field 'fields' is required for %s" msgstr "Поле 'fields' �вл�ет�� об�зательным дл� %s" msgid "Wrong sensor type" msgstr "�еверный тип датчика" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "Ошибка YAML при чтении файла определений %(file)s" msgid "ipmitool output length mismatch" msgstr "не�оответ�твие длины вывода ipmitool" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "" "�бой анализа данных датчика IPMI, не получены данные из переданного ввода" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "�бой анализа данных датчика IPMI, неизве�тный тип датчика" msgid "running ipmitool failure" msgstr "�бой выполнени� ipmitool" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2111278 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_CN/0000775000175000017500000000000015250267567017725 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2441275 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_CN/LC_MESSAGES/0000775000175000017500000000000015250267567021512 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1683567 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_CN/LC_MESSAGES/ceilometer.po0000664000175000017500000000640415250267452024177 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # aji.zqfan , 2015 # yelu , 2013 # Tom Fifield , 2013 # 颜海峰 , 2014 # yelu , 2013 # Yu Zhang, 2013 # Yu Zhang, 2013 # 颜海峰 , 2014 # English translations for ceilometer. # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:27+0000\n" "Last-Translator: Copied by Zanata \n" "Language: zh_CN\n" "Language-Team: Chinese (China)\n" "Plural-Forms: nplurals=1; plural=0\n" "Generated-By: Babel 2.2.0\n" "X-Generator: Zanata 4.3.3\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "查找实例 <�称为 %(name)s,标识为 %(id)s> 时,libvirt 中出错:[错误代� " "%(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "" "为虚拟机获�监控数�失败了,虚拟机状�为SHUTOFF" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "定义文件%(file)s中有�法YAML语法,行:%(line)s,列%(column)s。" #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "特�%(trait)s包�了��法的特�类型'%(type)s' " #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "未对 %(name)s �供�为 %(plugin)s 的�件" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "对 %(name)s 指定的 JSONPath(�“%(jsonpath)s�)存在解�错误:%(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "指定了�件,但未对 %s �供�件�" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW AdminOps接�返回%(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "必填项%s没有填写" #, python-format msgid "The field 'fields' is required for %s" msgstr "%s 需�字段“fields�" msgid "Wrong sensor type" msgstr "错误的传感器类型" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "读�定义文件%(file)s时�到YAML错误" msgid "ipmitool output length mismatch" msgstr "ipmi输出长度�匹�" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "解�IPMI传感器数�失败,从给定的输入中无法检索到数�" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "解�IPMI传感器数�失败,未知的传感器类型" msgid "running ipmitool failure" msgstr "�行ipmitool时失败了" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2111278 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_TW/0000775000175000017500000000000015250267567017757 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2441275 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_TW/LC_MESSAGES/0000775000175000017500000000000015250267567021544 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1693568 ceilometer-27.0.0.0rc1/ceilometer/locale/zh_TW/LC_MESSAGES/ceilometer.po0000664000175000017500000000577515250267452024243 0ustar00zuulzuul# Translations template for ceilometer. # Copyright (C) 2015 ORGANIZATION # This file is distributed under the same license as the ceilometer project. # # Translators: # Stefano Maffulli , 2013 # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: ceilometer VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2026-01-06 08:12+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 04:27+0000\n" "Last-Translator: Copied by Zanata \n" "Language: zh_TW\n" "Plural-Forms: nplurals=1; plural=0;\n" "Generated-By: Babel 2.0\n" "X-Generator: Zanata 4.3.3\n" "Language-Team: Chinese (Taiwan)\n" #, python-format msgid "" "Error from libvirt while looking up instance : " "[Error Code %(error_code)s] %(ex)s" msgstr "" "查閱實例 <�稱=%(name)s,ID=%(id)s> 時,libvirt 中發生錯誤:[錯誤碼 " "%(error_code)s] %(ex)s" #, python-format msgid "" "Failed to inspect data of instance , domain state " "is SHUTOFF." msgstr "無法檢查實例 <�稱=%(name)s,ID=%(id)s> 的資料,網域狀態為 SHUTOFF。" #, python-format msgid "" "Invalid YAML syntax in Definitions file %(file)s at line: %(line)s, column: " "%(column)s." msgstr "定義檔 %(file)s 第 %(line)s 行第 %(column)s 列中的 YAML 語法無效。" #, python-format msgid "Invalid trait type '%(type)s' for trait %(trait)s" msgstr "特徵 %(trait)s 的特徵類型 '%(type)s' 無效" #, python-format msgid "No plugin named %(plugin)s available for %(name)s" msgstr "沒有�為 %(plugin)s 的外掛程��供 %(name)s 使用" #, python-format msgid "" "Parse error in JSONPath specification '%(jsonpath)s' for %(name)s: %(err)s" msgstr "%(name)s 的 JSONPath �格 '%(jsonpath)s' 中發生剖�錯誤:%(err)s" #, python-format msgid "Plugin specified, but no plugin name supplied for %s" msgstr "已指定外掛程�,但�未� %s �供外掛程��稱" #, python-format msgid "RGW AdminOps API returned %(status)s %(reason)s" msgstr "RGW AdminOps API 傳回了 %(status)s %(reason)s" #, python-format msgid "Required field %s not specified" msgstr "未指定必�欄� %s" #, python-format msgid "The field 'fields' is required for %s" msgstr "%s 需�欄�「欄��" msgid "Wrong sensor type" msgstr "感應器類型錯誤" #, python-format msgid "YAML error reading Definitions file %(file)s" msgstr "讀�定義檔 %(file)s 時發生 YAML 錯誤" msgid "ipmitool output length mismatch" msgstr "ipmitool 輸出長度�符" msgid "parse IPMI sensor data failed,No data retrieved from given input" msgstr "剖� IPMI 感應器資料失敗,未從給定的輸入擷�任何資料" msgid "parse IPMI sensor data failed,unknown sensor type" msgstr "剖� IPMI 感應器資料失敗,感應器類型�明" msgid "running ipmitool failure" msgstr "執行 ipmitool 失敗" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1693568 ceilometer-27.0.0.0rc1/ceilometer/manila_client.py0000664000175000017500000000256715250267452020641 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack from oslo_config import cfg from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('manila', default='sharev2', help='Manila service type.'), ] class Client: """A client which gets information via openstacksdk.""" def __init__(self, conf): """Initialize a Manila client object.""" creds = conf.service_credentials self.conn = openstack.connection.Connection( session=keystone_client.get_session(conf), region_name=creds.region_name, shared_file_system_interface=creds.interface, shared_file_system_service_type=conf.service_types.manila ) def shares_list(self): """Return a list of shares from all projects.""" return self.conn.shared_file_system.shares(all_projects=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1703568 ceilometer-27.0.0.0rc1/ceilometer/messaging.py0000664000175000017500000000642515250267452020014 0ustar00zuulzuul# Copyright 2013-2015 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_config import cfg import oslo_messaging from oslo_messaging._drivers import impl_rabbit from oslo_messaging.notify import notifier from oslo_messaging import serializer as oslo_serializer DEFAULT_URL = "__default__" TRANSPORTS = {} def setup(): oslo_messaging.set_transport_defaults('ceilometer') # NOTE(sileht): When batch is not enabled, oslo.messaging read all messages # in the queue and can consume a lot of memory, that works for rpc because # you never have a lot of message, but sucks for notification. The # default is not changeable on oslo.messaging side. And we can't expose # this option to set set_transport_defaults because it a driver option. # 100 allow to prefetch a lot of messages but limit memory to 1G per # workers in worst case (~ 1M Nova notification) # And even driver options are located in private module, this is not going # to break soon. cfg.set_defaults( impl_rabbit.rabbit_opts, rabbit_qos_prefetch_count=100, ) def get_transport(conf, url=None, optional=False, cache=True): """Initialise the oslo_messaging layer.""" global TRANSPORTS, DEFAULT_URL cache_key = url or DEFAULT_URL transport = TRANSPORTS.get(cache_key) if not transport or not cache: try: transport = notifier.get_notification_transport(conf, url) except (oslo_messaging.InvalidTransportURL, oslo_messaging.DriverLoadFailure): if not optional or url: # NOTE(sileht): oslo_messaging is configured but unloadable # so reraise the exception raise return None else: if cache: TRANSPORTS[cache_key] = transport return transport def cleanup(): """Cleanup the oslo_messaging layer.""" global TRANSPORTS, NOTIFIERS NOTIFIERS = {} for url in TRANSPORTS: TRANSPORTS[url].cleanup() del TRANSPORTS[url] _SERIALIZER = oslo_serializer.JsonPayloadSerializer() def get_batch_notification_listener(transport, targets, endpoints, allow_requeue=False, batch_size=1, batch_timeout=None): """Return a configured oslo_messaging notification listener.""" return oslo_messaging.get_batch_notification_listener( transport, targets, endpoints, executor='threading', allow_requeue=allow_requeue, batch_size=batch_size, batch_timeout=batch_timeout) def get_notifier(transport, publisher_id): """Return a configured oslo_messaging notifier.""" notifier = oslo_messaging.Notifier(transport, serializer=_SERIALIZER) return notifier.prepare(publisher_id=publisher_id) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2451277 ceilometer-27.0.0.0rc1/ceilometer/meter/0000775000175000017500000000000015250267567016601 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1703568 ceilometer-27.0.0.0rc1/ceilometer/meter/__init__.py0000664000175000017500000000000015250267452020671 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1703568 ceilometer-27.0.0.0rc1/ceilometer/meter/notifications.py0000664000175000017500000002223315250267452022017 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import glob import itertools import os import re from ceilometer import cache_utils from oslo_config import cfg from oslo_log import log from stevedore import extension from ceilometer import declarative from ceilometer.i18n import _ from ceilometer.pipeline import sample as endpoint from ceilometer import sample as sample_util OPTS = [ cfg.MultiStrOpt('meter_definitions_dirs', default=["/etc/ceilometer/meters.d", os.path.abspath( os.path.join( os.path.split( os.path.dirname(__file__))[0], "data", "meters.d"))], help="List directory to find files of " "defining meter notifications." ), ] LOG = log.getLogger(__name__) class MeterDefinition: SAMPLE_ATTRIBUTES = ["name", "type", "volume", "unit", "timestamp", "user_id", "project_id", "resource_id"] REQUIRED_FIELDS = ['name', 'type', 'event_type', 'unit', 'volume', 'resource_id'] def __init__(self, definition_cfg, conf, plugin_manager): self.conf = conf self.cfg = definition_cfg self._cache = cache_utils.get_client(self.conf) missing = [field for field in self.REQUIRED_FIELDS if not self.cfg.get(field)] if missing: raise declarative.MeterDefinitionException( _("Required fields %s not specified") % missing, self.cfg) self._event_type = self.cfg.get('event_type') if isinstance(self._event_type, str): self._event_type = [self._event_type] self._event_type = [re.compile(etype) for etype in self._event_type] if ('type' not in self.cfg.get('lookup', []) and self.cfg['type'] not in sample_util.TYPES): raise declarative.MeterDefinitionException( _("Invalid type %s specified") % self.cfg['type'], self.cfg) self._fallback_user_id = declarative.Definition( 'user_id', "ctxt.user_id|ctxt.user", plugin_manager) self._fallback_project_id = declarative.Definition( 'project_id', "ctxt.project_id|ctxt.tenant_id", plugin_manager) self._attributes = {} self._metadata_attributes = {} self._user_meta = None self._name_discovery = self.conf.polling.identity_name_discovery for name in self.SAMPLE_ATTRIBUTES: attr_cfg = self.cfg.get(name) if attr_cfg: self._attributes[name] = declarative.Definition( name, attr_cfg, plugin_manager) metadata = self.cfg.get('metadata', {}) for name in metadata: self._metadata_attributes[name] = declarative.Definition( name, metadata[name], plugin_manager) user_meta = self.cfg.get('user_metadata') if user_meta: self._user_meta = declarative.Definition(None, user_meta, plugin_manager) # List of fields we expected when multiple meter are in the payload self.lookup = self.cfg.get('lookup') if isinstance(self.lookup, str): self.lookup = [self.lookup] def match_type(self, meter_name): for t in self._event_type: if t.match(meter_name): return True def to_samples(self, message, all_values=False): # Sample defaults sample = { 'name': self.cfg["name"], 'type': self.cfg["type"], 'unit': self.cfg["unit"], 'volume': None, 'timestamp': None, 'user_id': self._fallback_user_id.parse(message), 'project_id': self._fallback_project_id.parse(message), 'resource_id': None, 'message': message, 'metadata': {}, } for name, parser in self._metadata_attributes.items(): value = parser.parse(message) if value: sample['metadata'][name] = value if self._user_meta: meta = self._user_meta.parse(message) if meta: sample_util.add_reserved_user_metadata( self.conf, meta, sample['metadata']) # NOTE(sileht): We expect multiple samples in the payload # so put each attribute into a list if self.lookup: for name in sample: sample[name] = [sample[name]] for name in self.SAMPLE_ATTRIBUTES: parser = self._attributes.get(name) if parser is not None: value = parser.parse(message, bool(self.lookup)) # NOTE(sileht): If we expect multiple samples # some attributes are overridden even we don't get any # result. Also note in this case value is always a list if ((not self.lookup and value is not None) or (self.lookup and ((name in self.lookup + ["name"]) or value))): sample[name] = value if self.lookup: nb_samples = len(sample['name']) # skip if no meters in payload if nb_samples <= 0: return attributes = self.SAMPLE_ATTRIBUTES + ["message", "metadata"] samples_values = [] for name in attributes: values = sample.get(name) nb_values = len(values) if nb_values == nb_samples: samples_values.append(values) elif nb_values == 1 and name not in self.lookup: samples_values.append(itertools.cycle(values)) else: nb = (0 if nb_values == 1 and values[0] is None else nb_values) LOG.warning('Only %(nb)d fetched meters contain ' '"%(name)s" field instead of %(total)d.', dict(name=name, nb=nb, total=nb_samples)) return # NOTE(sileht): Transform the sample with multiple values per # attribute into multiple samples with one value per attribute. for values in zip(*samples_values): sample = {attributes[idx]: value for idx, value in enumerate(values)} if self._name_discovery and self._cache: # populate user_name and project_name fields in the sample # created from notifications if sample['user_id']: sample['user_name'] = \ self._cache.resolve_uuid_from_cache( 'users', sample['user_id']) if sample['project_id']: sample['project_name'] = \ self._cache.resolve_uuid_from_cache( 'projects', sample['project_id']) yield sample else: yield sample class ProcessMeterNotifications(endpoint.SampleEndpoint): event_types = [] def __init__(self, conf, publisher): super().__init__(conf, publisher) self.definitions = self._load_definitions() def _load_definitions(self): plugin_manager = extension.ExtensionManager( namespace='ceilometer.event.trait_plugin') definitions = {} mfs = [] for dir in self.conf.meter.meter_definitions_dirs: for filepath in sorted(glob.glob(os.path.join(dir, "*.yaml"))): if filepath is not None: mfs.append(filepath) for mf in mfs: meters_cfg = declarative.load_definitions( self.conf, {}, mf) for meter_cfg in reversed(meters_cfg['metric']): if meter_cfg.get('name') in definitions: # skip duplicate meters LOG.warning("Skipping duplicate meter definition %s", meter_cfg) continue try: md = MeterDefinition(meter_cfg, self.conf, plugin_manager) except declarative.DefinitionException as e: LOG.error("Error loading meter definition: %s", e) else: definitions[meter_cfg['name']] = md return definitions.values() def build_sample(self, notification): for d in self.definitions: if d.match_type(notification['event_type']): for s in d.to_samples(notification): yield sample_util.Sample.from_notification(**s) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1703568 ceilometer-27.0.0.0rc1/ceilometer/middleware.py0000664000175000017500000000245615250267452020154 0ustar00zuulzuul# # Copyright 2013 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.pipeline import sample as endpoint from ceilometer import sample class HTTPRequest(endpoint.SampleEndpoint): event_types = ['http.request'] def build_sample(self, message): yield sample.Sample.from_notification( name=message['event_type'], type=sample.TYPE_DELTA, volume=1, unit=message['event_type'].split('.')[1], user_id=message['payload']['request'].get('HTTP_X_USER_ID'), project_id=message['payload']['request'].get('HTTP_X_PROJECT_ID'), resource_id=message['payload']['request'].get( 'HTTP_X_SERVICE_NAME'), message=message) class HTTPResponse(HTTPRequest): event_types = ['http.response'] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2451277 ceilometer-27.0.0.0rc1/ceilometer/network/0000775000175000017500000000000015250267567017156 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1713567 ceilometer-27.0.0.0rc1/ceilometer/network/__init__.py0000664000175000017500000000000015250267452021246 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1713567 ceilometer-27.0.0.0rc1/ceilometer/network/floatingip.py0000664000175000017500000000405415250267452021660 0ustar00zuulzuul# Copyright 2016 Sungard Availability Services # Copyright 2016 Red Hat # Copyright 2012 eNovance # Copyright 2013 IBM Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.network.services import base from ceilometer import sample LOG = log.getLogger(__name__) class FloatingIPPollster(base.BaseServicesPollster): FIELDS = ['router_id', 'status', 'floating_network_id', 'fixed_ip_address', 'port_id', 'floating_ip_address', ] @property def default_discovery(self): return 'fip_services' def get_samples(self, manager, cache, resources): for fip in resources or []: LOG.debug("FLOATING IP : %s", fip) status = self.get_status_id(fip['status']) if status == -1: LOG.warning( "Unknown status %(status)s for floating IP address " "%(address)s (%(id)s), setting volume to -1", {"status": fip['status'], "address": fip['floating_ip_address'], "id": fip['id']}) yield sample.Sample( name='ip.floating', type=sample.TYPE_GAUGE, unit='ip', volume=status, user_id=fip.get('user_id'), project_id=fip['tenant_id'], resource_id=fip['id'], resource_metadata=self.extract_metadata(fip) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2461276 ceilometer-27.0.0.0rc1/ceilometer/network/services/0000775000175000017500000000000015250267567021001 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1713567 ceilometer-27.0.0.0rc1/ceilometer/network/services/__init__.py0000664000175000017500000000000015250267452023071 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1713567 ceilometer-27.0.0.0rc1/ceilometer/network/services/base.py0000664000175000017500000000223615250267452022261 0ustar00zuulzuul# # Copyright 2014 Cisco Systems,Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.polling import plugin_base # status map for converting metric status to volume int STATUS = { 'inactive': 0, 'active': 1, 'pending_create': 2, 'down': 3, 'created': 4, 'pending_update': 5, 'pending_delete': 6, 'error': 7, } class BaseServicesPollster(plugin_base.PollsterBase): FIELDS = [] def extract_metadata(self, metric): return {k: metric[k] for k in self.FIELDS} @staticmethod def get_status_id(value): if not value: return -1 status = value.lower() return STATUS.get(status, -1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1723568 ceilometer-27.0.0.0rc1/ceilometer/network/services/discovery.py0000664000175000017500000000371515250267452023361 0ustar00zuulzuul# # Copyright (c) 2014 Cisco Systems, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import neutron_client from ceilometer.polling import plugin_base class _BaseServicesDiscovery(plugin_base.DiscoveryBase): KEYSTONE_REQUIRED_FOR_SERVICE = 'neutron' def __init__(self, conf): super().__init__(conf) self.neutron_cli = neutron_client.Client(conf) class VPNServicesDiscovery(_BaseServicesDiscovery): def discover(self, manager, param=None): """Discover resources to monitor.""" return self.neutron_cli.vpn_get_all() class IPSecConnectionsDiscovery(_BaseServicesDiscovery): def discover(self, manager, param=None): """Discover resources to monitor.""" conns = self.neutron_cli.ipsec_site_connections_get_all() return conns class FirewallDiscovery(_BaseServicesDiscovery): def discover(self, manager, param=None): """Discover resources to monitor.""" fw = self.neutron_cli.firewall_get_all() return [i for i in fw if i.get('status', None) != 'error'] class FirewallPolicyDiscovery(_BaseServicesDiscovery): def discover(self, manager, param=None): """Discover resources to monitor.""" return self.neutron_cli.fw_policy_get_all() class FloatingIPDiscovery(_BaseServicesDiscovery): def discover(self, manager, param=None): """Discover floating IP resources to monitor.""" return self.neutron_cli.fip_get_all() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1733568 ceilometer-27.0.0.0rc1/ceilometer/network/services/fwaas.py0000664000175000017500000000566115250267452022455 0ustar00zuulzuul# # Copyright 2014 Cisco Systems,Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.network.services import base from ceilometer import sample LOG = log.getLogger(__name__) class FirewallPollster(base.BaseServicesPollster): """Pollster to capture firewall group status samples (FWaaS v2).""" FIELDS = ['admin_state_up', 'description', 'name', 'status', 'ingress_firewall_policy_id', 'egress_firewall_policy_id', ] @property def default_discovery(self): return 'fw_services' def get_samples(self, manager, cache, resources): resources = resources or [] for fw in resources: LOG.debug("Firewall : %s", fw) status = self.get_status_id(fw['status']) if status == -1: LOG.warning( "Unknown status %(status)s for firewall %(name)s " "(%(id)s), setting volume to -1", {"status": fw['status'], "name": fw['name'], "id": fw['id']}) yield sample.Sample( name='network.services.firewall', type=sample.TYPE_GAUGE, unit='firewall', volume=status, user_id=None, project_id=fw.get('project_id') or fw.get('tenant_id'), resource_id=fw['id'], resource_metadata=self.extract_metadata(fw) ) class FirewallPolicyPollster(base.BaseServicesPollster): """Pollster to capture firewall policy samples.""" FIELDS = ['name', 'description', 'name', 'firewall_rules', 'shared', 'audited', ] @property def default_discovery(self): return 'fw_policy' def get_samples(self, manager, cache, resources): resources = resources or [] for fw in resources: LOG.debug("Firewall Policy: %s", fw) yield sample.Sample( name='network.services.firewall.policy', type=sample.TYPE_GAUGE, unit='firewall_policy', volume=1, user_id=None, project_id=fw.get('project_id') or fw.get('tenant_id'), resource_id=fw['id'], resource_metadata=self.extract_metadata(fw) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1743567 ceilometer-27.0.0.0rc1/ceilometer/network/services/vpnaas.py0000664000175000017500000000641615250267452022643 0ustar00zuulzuul# # Copyright 2014 Cisco Systems,Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.network.services import base from ceilometer import sample LOG = log.getLogger(__name__) class VPNServicesPollster(base.BaseServicesPollster): """Pollster to capture VPN status samples.""" FIELDS = ['is_admin_state_up', 'description', 'name', 'status', 'subnet_id', 'router_id', 'flavor_id', 'external_v4_ip', 'external_v6_ip', ] @property def default_discovery(self): return 'vpn_services' def get_samples(self, manager, cache, resources): resources = resources or [] for vpn in resources: LOG.debug("VPN : %s", vpn) status = self.get_status_id(vpn['status']) if status == -1: LOG.warning( "Unknown status %(status)s for VPN %(name)s (%(id)s), " "setting volume to -1", {"status": vpn['status'], "name": vpn['name'], "id": vpn['id']}) yield sample.Sample( name='network.services.vpn', type=sample.TYPE_GAUGE, unit='vpnservice', volume=status, user_id=None, project_id=vpn.get('project_id') or vpn.get('tenant_id'), resource_id=vpn['id'], resource_metadata=self.extract_metadata(vpn) ) class IPSecConnectionsPollster(base.BaseServicesPollster): """Pollster to capture vpn ipsec connections status samples.""" FIELDS = ['name', 'description', 'peer_address', 'peer_id', 'peer_cidrs', 'peer_ep_group_id', 'psk', 'initiator', 'ikepolicy_id', 'ipsecpolicy_id', 'vpnservice_id', 'mtu', 'is_admin_state_up', 'status', 'project_id' ] @property def default_discovery(self): return 'ipsec_connections' def get_samples(self, manager, cache, resources): resources = resources or [] for conn in resources: LOG.debug("IPSec Connection Info: %s", conn) yield sample.Sample( name='network.services.vpn.connections', type=sample.TYPE_GAUGE, unit='ipsec_site_connection', volume=1, user_id=None, project_id=conn.get('project_id') or conn.get('tenant_id'), resource_id=conn['id'], resource_metadata=self.extract_metadata(conn) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1753566 ceilometer-27.0.0.0rc1/ceilometer/neutron_client.py0000664000175000017500000000556715250267452021075 0ustar00zuulzuul# Copyright (C) 2014 eNovance SAS # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools from openstack import connection from openstack import exceptions as os_exc from oslo_config import cfg from oslo_log import log from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('neutron', default='network', help='Neutron service type.'), ] LOG = log.getLogger(__name__) def logged(func): """Decorator to log exceptions from openstacksdk calls.""" @functools.wraps(func) def with_logging(*args, **kwargs): try: return func(*args, **kwargs) except os_exc.HttpException as e: if e.status_code == 404: LOG.warning("The resource could not be found.") else: LOG.warning(e) return [] except Exception as e: LOG.exception(e) raise return with_logging class Client: """A client which gets information via openstacksdk.""" def __init__(self, conf): """Initialize the Neutron client using openstacksdk. :param conf: Oslo config object with service credentials. """ creds = conf.service_credentials params = { 'session': keystone_client.get_session(conf), 'interface': creds.interface, 'region_name': creds.region_name, 'service_type': conf.service_types.neutron, } self.conn = connection.Connection(conf=conf, **params) @logged def vpn_get_all(self): """Get all VPN services.""" return [vpn.to_dict() for vpn in self.conn.network.vpn_services()] @logged def ipsec_site_connections_get_all(self): """Get all IPSec site connections.""" return [conn.to_dict() for conn in self.conn.network.vpn_ipsec_site_connections()] @logged def firewall_get_all(self): """Get all firewall groups (FWaaS v2).""" return [fw.to_dict() for fw in self.conn.network.firewall_groups()] @logged def fw_policy_get_all(self): """Get all firewall policies.""" return [policy.to_dict() for policy in self.conn.network.firewall_policies()] @logged def fip_get_all(self): """Get all floating IPs.""" return [fip.to_dict() for fip in self.conn.network.ips()] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1763568 ceilometer-27.0.0.0rc1/ceilometer/notification.py0000664000175000017500000001503615250267452020523 0ustar00zuulzuul# # Copyright 2017-2018 Red Hat, Inc. # Copyright 2012-2013 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import time import cotyledon from oslo_config import cfg from oslo_log import log import oslo_messaging from stevedore import named from ceilometer import messaging LOG = log.getLogger(__name__) OPTS = [ cfg.BoolOpt('ack_on_event_error', default=True, help='Acknowledge message when event persistence fails.'), cfg.MultiStrOpt('messaging_urls', default=[], secret=True, help="Messaging URLs to listen for notifications. " "Example: rabbit://user:pass@host1:port1" "[,user:pass@hostN:portN]/virtual_host " "(DEFAULT/transport_url is used if empty). This " "is useful when you have dedicate messaging nodes " "for each service, for example, all nova " "notifications go to rabbit-nova:5672, while all " "cinder notifications go to rabbit-cinder:5672."), cfg.IntOpt('batch_size', default=1, min=1, help='Number of notification messages to wait before ' 'publishing them.'), cfg.IntOpt('batch_timeout', help='Number of seconds to wait before dispatching samples ' 'when batch_size is not reached (None means indefinitely).' ), cfg.IntOpt('workers', default=1, min=1, deprecated_group='DEFAULT', deprecated_name='notification_workers', help='Number of workers for notification service.'), cfg.MultiStrOpt('pipelines', default=['meter', 'event'], help="Select which pipeline managers to enable to " " generate data"), ] EXCHANGES_OPTS = [ cfg.MultiStrOpt('notification_control_exchanges', default=['nova', 'glance', 'neutron', 'cinder', 'heat', 'keystone', 'trove', 'zaqar', 'swift', 'ceilometer', 'magnum', 'dns', 'ironic', 'aodh'], deprecated_group='DEFAULT', deprecated_name="http_control_exchanges", help="Exchanges name to listen for notifications."), ] class NotificationService(cotyledon.Service): """Notification service. When running multiple agents, additional queuing sequence is required for inter process communication. Each agent has two listeners: one to listen to the main OpenStack queue and another listener(and notifier) for IPC to divide pipeline sink endpoints. Coordination should be enabled to have proper active/active HA. """ NOTIFICATION_NAMESPACE = 'ceilometer.notification.v2' def __init__(self, worker_id, conf, coordination_id=None): super().__init__(worker_id) self.startup_delay = worker_id self.conf = conf self.listeners = [] def get_targets(self): """Return a sequence of oslo_messaging.Target This sequence is defining the exchange and topics to be connected. """ topics = (self.conf.notification_topics if 'notification_topics' in self.conf else self.conf.oslo_messaging_notifications.topics) return [oslo_messaging.Target(topic=topic, exchange=exchange) for topic in set(topics) for exchange in set(self.conf.notification.notification_control_exchanges)] @staticmethod def _log_missing_pipeline(names): LOG.error('Could not load the following pipelines: %s', names) def run(self): # Delay startup so workers are jittered time.sleep(self.startup_delay) super().run() self.managers = [ext.obj for ext in named.NamedExtensionManager( namespace='ceilometer.notification.pipeline', names=self.conf.notification.pipelines, invoke_on_load=True, on_missing_entrypoints_callback=self._log_missing_pipeline, invoke_args=(self.conf,))] # FIXME(sileht): endpoint uses the notification_topics option # and it should not because this is an oslo_messaging option # not a ceilometer. Until we have something to get the # notification_topics in another way, we must create a transport # to ensure the option has been registered by oslo_messaging. messaging.get_notifier(messaging.get_transport(self.conf), '') endpoints = [] for pipe_mgr in self.managers: LOG.debug("Loading manager endpoints for [%s].", pipe_mgr) endpoint = pipe_mgr.get_main_endpoints() LOG.debug("Loaded endpoints [%s] for manager [%s].", endpoint, pipe_mgr) endpoints.extend(endpoint) targets = self.get_targets() urls = self.conf.notification.messaging_urls or [None] for url in urls: transport = messaging.get_transport(self.conf, url) # NOTE(gordc): ignore batching as we want pull # to maintain sequencing as much as possible. listener = messaging.get_batch_notification_listener( transport, targets, endpoints, allow_requeue=True, batch_size=self.conf.notification.batch_size, batch_timeout=self.conf.notification.batch_timeout) listener.start( override_pool_size=self.conf.max_parallel_requests ) self.listeners.append(listener) @staticmethod def kill_listeners(listeners): # NOTE(gordc): correct usage of oslo.messaging listener is to stop(), # which stops new messages, and wait(), which processes remaining # messages and closes connection for listener in listeners: listener.stop() listener.wait() def terminate(self): self.kill_listeners(self.listeners) super().terminate() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1763568 ceilometer-27.0.0.0rc1/ceilometer/nova_client.py0000664000175000017500000001170115250267452020331 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import novaclient from novaclient import api_versions from novaclient import client as nova_client import openstack from oslo_config import cfg from oslo_log import log from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('nova', default='compute', help='Nova service type.'), ] LOG = log.getLogger(__name__) def logged(func): @functools.wraps(func) def with_logging(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: LOG.exception(e) raise return with_logging class Client: """A client which gets information via python-novaclient.""" def __init__(self, conf): """Initialize a nova client object.""" creds = conf.service_credentials ks_session = keystone_client.get_session(conf) self.nova_client = nova_client.Client( version=api_versions.APIVersion('2.1'), session=ks_session, # nova adapter options region_name=creds.region_name, endpoint_type=creds.interface, service_type=conf.service_types.nova) self.image_client = openstack.connection.Connection( image_api_version='2', session=ks_session, region_name=creds.region_name, image_interface=creds.interface) def _with_flavor_and_image(self, instances): flavor_cache = {} image_cache = {} for instance in instances: self._with_flavor(instance, flavor_cache) self._with_image(instance, image_cache) return instances def _with_flavor(self, instance, cache): fid = instance.flavor['id'] if fid in cache: flavor = cache.get(fid) else: try: flavor = self.nova_client.flavors.get(fid) except novaclient.exceptions.NotFound: flavor = None cache[fid] = flavor attr_defaults = [('name', 'unknown-id-%s' % fid), ('vcpus', 0), ('ram', 0), ('disk', 0), ('ephemeral', 0)] for attr, default in attr_defaults: if not flavor: instance.flavor[attr] = default continue instance.flavor[attr] = getattr(flavor, attr, default) def _with_image(self, instance, cache): try: iid = instance.image['id'] except TypeError: instance.image = None instance.kernel_id = None instance.ramdisk_id = None return if iid in cache: image = cache.get(iid) else: try: image = self.image_client.image.get_image(iid) except openstack.exceptions.NotFoundException: image = None cache[iid] = image attr_defaults = [('kernel_id', None), ('ramdisk_id', None)] instance.image['name'] = ( getattr(image, 'name') if image else 'unknown-id-%s' % iid) for attr, default in attr_defaults: ameta = getattr(image, attr, default) if image else default setattr(instance, attr, ameta) if image: image_meta = {"base_image_ref": iid} # Notifications and libvirt XML metadata return all # image_meta values as strings. Do the same here. # Use ignore_none=True to exclude unset attributes and avoid # polluting metadata with "None" strings. image_meta.update( (k, str(v)) for k, v in image.to_dict(ignore_none=True).items() if k not in ('id', 'name', 'metadata', 'kernel_id', 'ramdisk_id', 'tags')) else: image_meta = {} instance.image_meta = image_meta @logged def instance_get_all_by_host(self, hostname, since=None): """Returns list of instances on particular host. If since is supplied, it will return the instances changed since that datetime. since should be in ISO Format '%Y-%m-%dT%H:%M:%SZ' """ search_opts = {'host': hostname, 'all_tenants': True} if since: search_opts['changes-since'] = since return self._with_flavor_and_image(self.nova_client.servers.list( detailed=True, search_opts=search_opts)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2471275 ceilometer-27.0.0.0rc1/ceilometer/objectstore/0000775000175000017500000000000015250267567020010 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1763568 ceilometer-27.0.0.0rc1/ceilometer/objectstore/__init__.py0000664000175000017500000000000015250267452022100 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1773567 ceilometer-27.0.0.0rc1/ceilometer/objectstore/rgw.py0000664000175000017500000002413315250267452021155 0ustar00zuulzuul# # Copyright 2015 Reliance Jio Infocomm Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Common code for working with ceph object stores """ from keystoneauth1 import exceptions from oslo_config import cfg from oslo_log import log from urllib import parse as urlparse from ceilometer import keystone_client from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) TLS_VERSION_CHOICES = ['1.2', '1.3'] SERVICE_OPTS = [ cfg.StrOpt('radosgw', help='Radosgw service type.'), ] CREDENTIAL_OPTS = [ cfg.StrOpt('access_key', secret=True, help='Access key for Radosgw Admin.'), cfg.StrOpt('secret_key', secret=True, help='Secret key for Radosgw Admin.') ] CLIENT_OPTS = [ cfg.BoolOpt('implicit_tenants', default=False, help='Whether RGW uses implicit tenants or not.'), cfg.StrOpt('rgw_service_name', default=None, sample_default='ceph', help='Service name for object store endpoint. ' 'If left to None, will use [service_types]/radosgw.'), cfg.BoolOpt('insecure', default=True, help='Disable HTTPS certificate verification for RGW ' 'Admin API connections. insecure is currently set ' 'to True by default, but to encourage secure ' 'connections this will be changed to False in a ' 'future release.'), cfg.StrOpt('cafile', default=None, help='PEM encoded Certificate Authority to use when ' 'verifying HTTPS connections to the RGW Admin API. ' 'If not set, the system CA bundle is used.'), cfg.StrOpt('tls_min_version', default=None, choices=TLS_VERSION_CHOICES, help='Minimum TLS protocol version for RGW Admin API ' 'connections. If unset, awscurl default applies.'), cfg.StrOpt('tls_max_version', default=None, choices=TLS_VERSION_CHOICES, help='Maximum TLS protocol version for RGW Admin API ' 'connections. If unset, awscurl default applies.'), ] class _Base(plugin_base.PollsterBase): METHOD = 'bucket' _ENDPOINT = None def __init__(self, conf): super().__init__(conf) self.access_key = self.conf.rgw_admin_credentials.access_key self.secret = self.conf.rgw_admin_credentials.secret_key self.implicit_tenants = self.conf.rgw_client.implicit_tenants if self.conf.rgw_client.insecure: self.verify = False elif self.conf.rgw_client.cafile: self.verify = self.conf.rgw_client.cafile else: self.verify = True self.tls_min_version = self.conf.rgw_client.tls_min_version self.tls_max_version = self.conf.rgw_client.tls_max_version @property def default_discovery(self): return 'tenant' @property def CACHE_KEY_METHOD(self): return 'rgw.get_%s' % self.METHOD @staticmethod def _get_endpoint(conf, ksclient): # we store the endpoint as a base class attribute, so keystone is # only ever called once. if _Base._ENDPOINT is None: service_type = conf.service_types.radosgw service_name = conf.rgw_client.rgw_service_name if service_type is None and service_name is None: LOG.error( "Skipping radosgw polling, config issue: Required " "config not found, need either " "rgw_client.rgw_service_name or " "service_types.radosgw") return None try: # Use the service_name to target the endpoint. # There are cases where both 'radosgw' and 'swift' are used. rgw_url = keystone_client.url_for( ksclient, service_type=service_type, service_name=service_name, interface=conf.service_credentials.interface, region_name=conf.service_credentials.region_name) _Base._ENDPOINT = urlparse.urljoin(rgw_url, '/admin') except exceptions.EndpointNotFound: LOG.debug("Radosgw endpoint not found") LOG.debug("Using endpoint %s for radosgw connections", _Base._ENDPOINT) return _Base._ENDPOINT def _iter_accounts(self, ksclient, cache, tenants): if self.CACHE_KEY_METHOD not in cache: cache[self.CACHE_KEY_METHOD] = list(self._get_account_info( ksclient, tenants)) return iter(cache[self.CACHE_KEY_METHOD]) def _get_account_info(self, ksclient, tenants): endpoint = self._get_endpoint(self.conf, ksclient) if not endpoint: return try: from ceilometer.objectstore import rgw_client as c_rgw_client rgw_client = c_rgw_client.RGWAdminClient(endpoint, self.access_key, self.secret, self.implicit_tenants, self.verify, self.tls_min_version, self.tls_max_version) except ImportError: raise plugin_base.PollsterPermanentError(tenants) for t in tenants: api_method = 'get_%s' % self.METHOD yield t.id, getattr(rgw_client, api_method)(t.id) class ContainersObjectsPollster(_Base): """Get info about object counts in a container using RGW Admin APIs.""" def get_samples(self, manager, cache, resources): for tenant, bucket_info in self._iter_accounts(manager.keystone, cache, resources): for it in bucket_info['buckets']: yield sample.Sample( name='radosgw.containers.objects', type=sample.TYPE_GAUGE, volume=int(it.num_objects), unit='object', user_id=None, project_id=tenant, resource_id=tenant + '/' + it.name, resource_metadata=None, ) class ContainersSizePollster(_Base): """Get info about object sizes in a container using RGW Admin APIs.""" def get_samples(self, manager, cache, resources): for tenant, bucket_info in self._iter_accounts(manager.keystone, cache, resources): for it in bucket_info['buckets']: yield sample.Sample( name='radosgw.containers.objects.size', type=sample.TYPE_GAUGE, volume=int(it.size * 1024), unit='B', user_id=None, project_id=tenant, resource_id=tenant + '/' + it.name, resource_metadata=None, ) class ObjectsSizePollster(_Base): """Iterate over all accounts, using keystone.""" def get_samples(self, manager, cache, resources): for tenant, bucket_info in self._iter_accounts(manager.keystone, cache, resources): yield sample.Sample( name='radosgw.objects.size', type=sample.TYPE_GAUGE, volume=int(bucket_info['size'] * 1024), unit='B', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class ObjectsPollster(_Base): """Iterate over all accounts, using keystone.""" def get_samples(self, manager, cache, resources): for tenant, bucket_info in self._iter_accounts(manager.keystone, cache, resources): yield sample.Sample( name='radosgw.objects', type=sample.TYPE_GAUGE, volume=int(bucket_info['num_objects']), unit='object', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class ObjectsContainersPollster(_Base): def get_samples(self, manager, cache, resources): for tenant, bucket_info in self._iter_accounts(manager.keystone, cache, resources): yield sample.Sample( name='radosgw.objects.containers', type=sample.TYPE_GAUGE, volume=int(bucket_info['num_buckets']), unit='object', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class UsagePollster(_Base): METHOD = 'usage' def get_samples(self, manager, cache, resources): for tenant, usage in self._iter_accounts(manager.keystone, cache, resources): yield sample.Sample( name='radosgw.api.request', type=sample.TYPE_GAUGE, volume=int(usage), unit='request', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1773567 ceilometer-27.0.0.0rc1/ceilometer/objectstore/rgw_client.py0000664000175000017500000000703615250267452022516 0ustar00zuulzuul# # Copyright 2015 Reliance Jio Infocomm Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from collections import namedtuple from awscurl import awscurl from urllib import parse as urlparse from ceilometer.i18n import _ # Default region, in the case of radosgw either set # the value to your zonegroup name or keep the default region. # With a single Ceph zone-group, there's no need to customize # this value. _DEFAULT_REGION = "us-east-1" class RGWAdminAPIFailed(Exception): pass class RGWAdminClient: Bucket = namedtuple('Bucket', 'name, num_objects, size') def __init__(self, endpoint, access_key, secret_key, implicit_tenants, verify=True, tls_min_version=None, tls_max_version=None): self.access_key = access_key self.secret = secret_key self.endpoint = endpoint self.hostname = urlparse.urlparse(endpoint).netloc self.implicit_tenants = implicit_tenants self.verify = verify self.tls_min_version = tls_min_version self.tls_max_version = tls_max_version def _make_request(self, path, req_params): uri = f"{self.endpoint}/{path}" if req_params: uri = f"{uri}?{urlparse.urlencode(req_params)}" r = awscurl.make_request("GET", "s3", _DEFAULT_REGION, uri, {'Accept': 'application/json'}, "", self.access_key, self.secret, None, False, self.verify, tls_min=self.tls_min_version, tls_max=self.tls_max_version) if r.status_code != 200: raise RGWAdminAPIFailed( _('RGW AdminOps API returned %(status)s %(reason)s') % {'status': r.status_code, 'reason': r.reason}) if not r.text: return {} return r.json() def get_bucket(self, tenant_id): if self.implicit_tenants: rgw_uid = tenant_id + "$" + tenant_id else: rgw_uid = tenant_id path = "bucket" req_params = {"uid": rgw_uid, "stats": "true"} json_data = self._make_request(path, req_params) stats = {'num_buckets': 0, 'buckets': [], 'size': 0, 'num_objects': 0} stats['num_buckets'] = len(json_data) for it in json_data: for v in it["usage"].values(): stats['num_objects'] += v["num_objects"] stats['size'] += v["size_kb"] stats['buckets'].append(self.Bucket(it["bucket"], v["num_objects"], v["size_kb"])) return stats def get_usage(self, tenant_id): if self.implicit_tenants: rgw_uid = tenant_id + "$" + tenant_id else: rgw_uid = tenant_id path = "usage" req_params = {"uid": rgw_uid} json_data = self._make_request(path, req_params) usage_data = json_data["summary"] return sum(it["total"]["ops"] for it in usage_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1773567 ceilometer-27.0.0.0rc1/ceilometer/objectstore/swift.py0000664000175000017500000001742515250267452021520 0ustar00zuulzuul# # Copyright 2012 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Common code for working with object stores """ from keystoneauth1 import exceptions from oslo_config import cfg from oslo_log import log from swiftclient import client as swift from swiftclient.exceptions import ClientException from urllib import parse as urlparse from ceilometer import keystone_client from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) OPTS = [ cfg.StrOpt('reseller_prefix', default='AUTH_', help="Swift reseller prefix. Must be on par with " "reseller_prefix in proxy-server.conf."), ] SERVICE_OPTS = [ cfg.StrOpt('swift', default='object-store', help='Swift service type.'), ] class _Base(plugin_base.PollsterBase): METHOD = 'head' _ENDPOINT = None @property def default_discovery(self): return 'tenant' @property def CACHE_KEY_METHOD(self): return 'swift.%s_account' % self.METHOD @staticmethod def _get_endpoint(conf, ksclient): # we store the endpoint as a base class attribute, so keystone is # only ever called once if _Base._ENDPOINT is None: try: creds = conf.service_credentials _Base._ENDPOINT = keystone_client.url_for( ksclient, service_type=conf.service_types.swift, interface=creds.interface, region_name=creds.region_name) except exceptions.EndpointNotFound as e: LOG.info("Swift endpoint not found: %s", e) return _Base._ENDPOINT def _iter_accounts(self, ksclient, cache, tenants): if self.CACHE_KEY_METHOD not in cache: cache[self.CACHE_KEY_METHOD] = list(self._get_account_info( ksclient, tenants)) return iter(cache[self.CACHE_KEY_METHOD]) def _get_account_info(self, ksclient, tenants): endpoint = self._get_endpoint(self.conf, ksclient) if not endpoint: return swift_api_method = getattr(swift, '%s_account' % self.METHOD) for t in tenants: try: http_conn = swift.http_connection( self._neaten_url(endpoint, t.id, self.conf.reseller_prefix), cacert=self.conf.service_credentials.cafile) yield (t.id, swift_api_method( None, keystone_client.get_auth_token(ksclient), http_conn=http_conn)) except ClientException as e: if e.http_status == 404: LOG.warning("Swift tenant id %s not found.", t.id) elif e.http_status == 403: LOG.error("The credentials configured does not have " "correct roles to access Swift tenant id %s.", t.id) else: raise e @staticmethod def _neaten_url(endpoint, tenant_id, reseller_prefix): """Transform the registered url to standard and valid format.""" return urlparse.urljoin(endpoint.split('/v1')[0].rstrip('/') + '/', 'v1/' + reseller_prefix + tenant_id) class _ContainersBase(_Base): FIELDS = ("storage_policy",) def _get_resource_metadata(self, container): # NOTE(callumdickinson): Sets value to None if a field is not found. return {f: container.get(f) for f in self.FIELDS} class ObjectsPollster(_Base): """Collect the total objects count for each project""" def get_samples(self, manager, cache, resources): tenants = resources for tenant, account in self._iter_accounts(manager.keystone, cache, tenants): yield sample.Sample( name='storage.objects', type=sample.TYPE_GAUGE, volume=int(account['x-account-object-count']), unit='object', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class ObjectsSizePollster(_Base): """Collect the total objects size of each project""" def get_samples(self, manager, cache, resources): tenants = resources for tenant, account in self._iter_accounts(manager.keystone, cache, tenants): yield sample.Sample( name='storage.objects.size', type=sample.TYPE_GAUGE, volume=int(account['x-account-bytes-used']), unit='B', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class ObjectsContainersPollster(_Base): """Collect the container count for each project""" def get_samples(self, manager, cache, resources): tenants = resources for tenant, account in self._iter_accounts(manager.keystone, cache, tenants): yield sample.Sample( name='storage.objects.containers', type=sample.TYPE_GAUGE, volume=int(account['x-account-container-count']), unit='container', user_id=None, project_id=tenant, resource_id=tenant, resource_metadata=None, ) class ContainersObjectsPollster(_ContainersBase): """Collect the objects count per container for each project""" METHOD = 'get' def get_samples(self, manager, cache, resources): tenants = resources for tenant, account in self._iter_accounts(manager.keystone, cache, tenants): containers_info = account[1] for container in containers_info: yield sample.Sample( name='storage.containers.objects', type=sample.TYPE_GAUGE, volume=int(container['count']), unit='object', user_id=None, project_id=tenant, resource_id=tenant + '/' + container['name'], resource_metadata=self._get_resource_metadata(container), ) class ContainersSizePollster(_ContainersBase): """Collect the total objects size per container for each project""" METHOD = 'get' def get_samples(self, manager, cache, resources): tenants = resources for tenant, account in self._iter_accounts(manager.keystone, cache, tenants): containers_info = account[1] for container in containers_info: yield sample.Sample( name='storage.containers.objects.size', type=sample.TYPE_GAUGE, volume=int(container['bytes']), unit='B', user_id=None, project_id=tenant, resource_id=tenant + '/' + container['name'], resource_metadata=self._get_resource_metadata(container), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1783566 ceilometer-27.0.0.0rc1/ceilometer/octavia_client.py0000664000175000017500000000254715250267452021024 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack from oslo_config import cfg from ceilometer import keystone_client SERVICE_OPTS = [ cfg.StrOpt('octavia', default='load-balancer', help='Octavia service type.'), ] class Client: """A client which gets information via openstacksdk.""" def __init__(self, conf): """Initialize an Octavia client object.""" creds = conf.service_credentials self.conn = openstack.connection.Connection( session=keystone_client.get_session(conf), region_name=creds.region_name, load_balancer_interface=creds.interface, load_balancer_service_type=conf.service_types.octavia ) def loadbalancers_list(self): """Return a list of load balancers.""" return self.conn.load_balancer.load_balancers() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1783566 ceilometer-27.0.0.0rc1/ceilometer/opts.py0000664000175000017500000001154315250267452017021 0ustar00zuulzuul# Copyright 2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import itertools import socket from keystoneauth1 import loading from oslo_config import cfg import ceilometer.alarm.discovery import ceilometer.cmd.polling import ceilometer.compute.discovery import ceilometer.compute.virt.inspector import ceilometer.compute.virt.libvirt.utils import ceilometer.designate_client import ceilometer.event.converter import ceilometer.image.discovery import ceilometer.ipmi.pollsters import ceilometer.keystone_client import ceilometer.manila_client import ceilometer.meter.notifications import ceilometer.neutron_client import ceilometer.notification import ceilometer.nova_client import ceilometer.objectstore.rgw import ceilometer.objectstore.swift import ceilometer.octavia_client import ceilometer.pipeline.base import ceilometer.polling.manager import ceilometer.publisher.messaging import ceilometer.publisher.utils import ceilometer.sample import ceilometer.utils import ceilometer.volume.discovery OPTS = [ cfg.HostAddressOpt('host', default=socket.gethostname(), sample_default='', help='Hostname, FQDN or IP address of this host. ' 'Must be valid within AMQP key.'), cfg.IntOpt('max_parallel_requests', default=64, min=1, help='Maximum number of parallel requests for ' 'services to handle at the same time.'), ] def list_opts(): # FIXME(sileht): re-add pollster namespaces in the generated configfile # This have been removed due to a recursive import issue return [ ('DEFAULT', itertools.chain(ceilometer.cmd.polling.CLI_OPTS, ceilometer.compute.virt.inspector.OPTS, ceilometer.compute.virt.libvirt.utils.OPTS, ceilometer.objectstore.swift.OPTS, ceilometer.pipeline.base.OPTS, ceilometer.polling.manager.POLLING_OPTS, ceilometer.sample.OPTS, ceilometer.utils.OPTS, OPTS)), ('compute', ceilometer.compute.discovery.OPTS), ('coordination', [ cfg.StrOpt( 'backend_url', secret=True, help='The backend URL to use for distributed coordination. If ' 'left empty, per-deployment central agent and per-host ' 'compute agent won\'t do workload ' 'partitioning and will only function correctly if a ' 'single instance of that service is running.') ]), ('event', ceilometer.event.converter.OPTS), ('ipmi', ceilometer.ipmi.pollsters.OPTS), ('meter', ceilometer.meter.notifications.OPTS), ('notification', itertools.chain(ceilometer.notification.OPTS, ceilometer.notification.EXCHANGES_OPTS)), ('polling', ceilometer.polling.manager.POLLING_OPTS), ('publisher', ceilometer.publisher.utils.OPTS), ('publisher_notifier', ceilometer.publisher.messaging.NOTIFIER_OPTS), ('rgw_admin_credentials', ceilometer.objectstore.rgw.CREDENTIAL_OPTS), ('rgw_client', ceilometer.objectstore.rgw.CLIENT_OPTS), ('service_types', itertools.chain(ceilometer.alarm.discovery.SERVICE_OPTS, ceilometer.cinder_client.SERVICE_OPTS, ceilometer.designate_client.SERVICE_OPTS, ceilometer.manila_client.SERVICE_OPTS, ceilometer.neutron_client.SERVICE_OPTS, ceilometer.nova_client.SERVICE_OPTS, ceilometer.objectstore.rgw.SERVICE_OPTS, ceilometer.objectstore.swift.SERVICE_OPTS, ceilometer.octavia_client.SERVICE_OPTS,)) ] def list_keystoneauth_opts(): # NOTE(sileht): the configuration file contains only the options # for the password plugin that handles keystone v2 and v3 API # with discovery. But other options are possible. return [('service_credentials', itertools.chain( loading.get_session_conf_options(), loading.get_auth_common_conf_options(), loading.get_auth_plugin_conf_options('password'), ceilometer.keystone_client.CLI_OPTS ))] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2471275 ceilometer-27.0.0.0rc1/ceilometer/pipeline/0000775000175000017500000000000015250267567017272 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1793568 ceilometer-27.0.0.0rc1/ceilometer/pipeline/__init__.py0000664000175000017500000000000015250267452021362 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1793568 ceilometer-27.0.0.0rc1/ceilometer/pipeline/base.py0000664000175000017500000002510615250267452020553 0ustar00zuulzuul# # Copyright 2013 Intel Corp. # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc from oslo_config import cfg from oslo_log import log import oslo_messaging from ceilometer import agent from ceilometer import publisher OPTS = [ cfg.StrOpt('pipeline_cfg_file', default="pipeline.yaml", help="Configuration file for pipeline definition." ), cfg.StrOpt('event_pipeline_cfg_file', default="event_pipeline.yaml", help="Configuration file for event pipeline definition." ), ] LOG = log.getLogger(__name__) class PipelineException(agent.ConfigException): def __init__(self, message, cfg): super().__init__('Pipeline', message, cfg) class PublishContext: def __init__(self, pipelines): self.pipelines = pipelines or [] def __enter__(self): def p(data): for p in self.pipelines: p.publish_data(data) return p def __exit__(self, exc_type, exc_value, traceback): for p in self.pipelines: p.flush() class PipelineSource(agent.Source): """Represents a source of samples or events.""" def __init__(self, cfg): try: super().__init__(cfg) except agent.SourceException as err: raise PipelineException(err.msg, cfg) try: self.sinks = cfg['sinks'] except KeyError as err: raise PipelineException( "Required field %s not specified" % err.args[0], cfg) def check_sinks(self, sinks): if not self.sinks: raise PipelineException( "No sink defined in source %s" % self, self.cfg) for sink in self.sinks: if sink not in sinks: raise PipelineException( f"Dangling sink {sink} from source {self}", self.cfg) class Sink: """Represents a sink for the transformation and publication of data. Each sink config is concerned *only* with the transformation rules and publication conduits for data. In effect, a sink describes a chain of handlers. The chain ends with one or more publishers. At the end of the chain, publishers publish the data. The exact publishing method depends on publisher type, for example, pushing into data storage via the message bus providing guaranteed delivery, or for loss-tolerant data UDP may be used. """ def __init__(self, conf, cfg, publisher_manager): self.conf = conf self.cfg = cfg try: self.name = cfg['name'] except KeyError as err: raise PipelineException( "Required field %s not specified" % err.args[0], cfg) if not cfg.get('publishers'): raise PipelineException("No publisher specified", cfg) self.publishers = [] for p in cfg['publishers']: if '://' not in p: # Support old format without URL p = p + "://" try: self.publishers.append(publisher_manager.get(p)) except Exception: LOG.exception("Unable to load publisher %s", p) self.multi_publish = True if len(self.publishers) > 1 else False def __str__(self): return self.name @staticmethod def flush(): """Flush data after all events have been injected to pipeline.""" class Pipeline(metaclass=abc.ABCMeta): """Represents a coupling between a sink and a corresponding source.""" def __init__(self, conf, source, sink): self.conf = conf self.source = source self.sink = sink self.name = str(self) def __str__(self): return (self.source.name if self.source.name == self.sink.name else f'{self.source.name}:{self.sink.name}') def flush(self): self.sink.flush() @property def publishers(self): return self.sink.publishers @abc.abstractmethod def publish_data(self, data): """Publish data from pipeline.""" @abc.abstractmethod def supported(self, data): """Attribute to filter on. Pass if no partitioning.""" class PublisherManager: def __init__(self, conf, purpose): self._loaded_publishers = {} self._conf = conf self._purpose = purpose def get(self, url): if url not in self._loaded_publishers: p = publisher.get_publisher( self._conf, url, 'ceilometer.%s.publisher' % self._purpose) self._loaded_publishers[url] = p return self._loaded_publishers[url] class PipelineManager(agent.ConfigManagerBase): """Pipeline Manager Pipeline manager sets up pipelines according to config file """ def __init__(self, conf, cfg_file): """Setup the pipelines according to config. The configuration is supported as follows: Decoupled: the source and sink configuration are separately specified before being linked together. This allows source- specific configuration, such as meter handling, to be kept focused only on the fine-grained source while avoiding the necessity for wide duplication of sink-related config. The configuration is provided in the form of separate lists of dictionaries defining sources and sinks, for example: {"sources": [{"name": source_1, "meters" : ["meter_1", "meter_2"], "sinks" : ["sink_1", "sink_2"] }, {"name": source_2, "meters" : ["meter_3"], "sinks" : ["sink_2"] }, ], "sinks": [{"name": sink_1, "publishers": ["publisher_1", "publisher_2"] }, {"name": sink_2, "publishers": ["publisher_3"] }, ] } Valid meter format is '*', '!meter_name', or 'meter_name'. '*' is wildcard symbol means any meters; '!meter_name' means "meter_name" will be excluded; 'meter_name' means 'meter_name' will be included. Valid meters definition is all "included meter names", all "excluded meter names", wildcard and "excluded meter names", or only wildcard. Publisher's name is plugin name in setup.cfg """ super().__init__(conf) cfg = self.load_config(cfg_file) self.pipelines = [] if not ('sources' in cfg and 'sinks' in cfg): raise PipelineException("Both sources & sinks are required", cfg) publisher_manager = PublisherManager(self.conf, self.pm_type) unique_names = set() sources = [] for s in cfg.get('sources'): name = s.get('name') if name in unique_names: raise PipelineException("Duplicated source names: %s" % name, self) else: unique_names.add(name) sources.append(self.pm_source(s)) unique_names.clear() sinks = {} for s in cfg.get('sinks'): name = s.get('name') if name in unique_names: raise PipelineException("Duplicated sink names: %s" % name, self) else: unique_names.add(name) sinks[s['name']] = self.pm_sink(self.conf, s, publisher_manager) unique_names.clear() for source in sources: source.check_sinks(sinks) for target in source.sinks: pipe = self.pm_pipeline(self.conf, source, sinks[target]) if pipe.name in unique_names: raise PipelineException( "Duplicate pipeline name: %s. Ensure pipeline" " names are unique. (name is the source and sink" " names combined)" % pipe.name, cfg) else: unique_names.add(pipe.name) self.pipelines.append(pipe) unique_names.clear() @property @abc.abstractmethod def pm_type(self): """Pipeline manager type.""" @property @abc.abstractmethod def pm_pipeline(self): """Pipeline class""" @property @abc.abstractmethod def pm_source(self): """Pipeline source class""" @property @abc.abstractmethod def pm_sink(self): """Pipeline sink class""" def publisher(self): """Build publisher for pipeline publishing.""" return PublishContext(self.pipelines) def get_main_endpoints(self): """Return endpoints for main queue.""" pass class NotificationEndpoint: """Base Endpoint for plugins that support the notification API.""" event_types = [] """List of strings to filter messages on.""" def __init__(self, conf, publisher): super().__init__() # NOTE(gordc): this is filter rule used by oslo.messaging to dispatch # messages to an endpoint. if self.event_types: self.filter_rule = oslo_messaging.NotificationFilter( event_type='|'.join(self.event_types)) self.conf = conf self.publisher = publisher @abc.abstractmethod def process_notifications(self, priority, notifications): """Return a sequence of Counter instances for the given message. :param message: Message to process. """ @classmethod def _consume_and_drop(cls, notifications): """RPC endpoint for useless notification level""" # NOTE(sileht): nothing special todo here, but because we listen # for the generic notification exchange we have to consume all its # queues audit = _consume_and_drop critical = _consume_and_drop debug = _consume_and_drop error = _consume_and_drop info = _consume_and_drop sample = _consume_and_drop warn = _consume_and_drop ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2481277 ceilometer-27.0.0.0rc1/ceilometer/pipeline/data/0000775000175000017500000000000015250267567020203 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1793568 ceilometer-27.0.0.0rc1/ceilometer/pipeline/data/event_definitions.yaml0000664000175000017500000004435115250267452024603 0ustar00zuulzuul--- - event_type: 'compute.instance.*' traits: &instance_traits tenant_id: fields: payload.tenant_id user_id: fields: payload.user_id instance_id: fields: payload.instance_id display_name: fields: payload.display_name resource_id: fields: payload.instance_id cell_name: fields: payload.cell_name host: fields: publisher_id.`split(., 1, 1)` service: fields: publisher_id.`split(., 0, -1)` memory_mb: type: int fields: payload.memory_mb disk_gb: type: int fields: payload.disk_gb root_gb: type: int fields: payload.root_gb ephemeral_gb: type: int fields: payload.ephemeral_gb vcpus: type: int fields: payload.vcpus instance_type_id: fields: payload.instance_type_id instance_flavor_id: fields: payload.instance_flavor_id instance_type: fields: payload.instance_type state: fields: payload.state os_architecture: fields: payload.image_meta.'org.openstack__1__architecture' os_version: fields: payload.image_meta.'org.openstack__1__os_version' os_distro: fields: payload.image_meta.'org.openstack__1__os_distro' launched_at: type: datetime fields: payload.launched_at deleted_at: type: datetime fields: payload.deleted_at - event_type: compute.instance.create.end traits: <<: *instance_traits availability_zone: fields: payload.availability_zone - event_type: compute.instance.update traits: <<: *instance_traits old_state: fields: payload.old_state - event_type: compute.instance.exists traits: <<: *instance_traits audit_period_beginning: type: datetime fields: payload.audit_period_beginning audit_period_ending: type: datetime fields: payload.audit_period_ending - event_type: ['volume.exists', 'volume.retype', 'volume.create.*', 'volume.delete.*', 'volume.resize.*', 'volume.attach.*', 'volume.detach.*', 'volume.update.*', 'snapshot.exists', 'snapshot.create.*', 'snapshot.delete.*', 'snapshot.update.*', 'volume.transfer.accept.end', 'snapshot.transfer.accept.end'] traits: &cinder_traits user_id: fields: payload.user_id project_id: fields: payload.tenant_id availability_zone: fields: payload.availability_zone display_name: fields: payload.display_name replication_status: fields: payload.replication_status status: fields: payload.status created_at: type: datetime fields: payload.created_at image_id: fields: payload.glance_metadata[?key=image_id].value instance_id: fields: payload.volume_attachment[0].server_id - event_type: ['volume.transfer.*', 'volume.exists', 'volume.retype', 'volume.create.*', 'volume.delete.*', 'volume.resize.*', 'volume.attach.*', 'volume.detach.*', 'volume.update.*', 'snapshot.transfer.accept.end'] traits: <<: *cinder_traits resource_id: fields: payload.volume_id host: fields: payload.host size: type: int fields: payload.size type: fields: payload.volume_type replication_status: fields: payload.replication_status - event_type: ['snapshot.transfer.accept.end'] traits: <<: *cinder_traits resource_id: fields: payload.snapshot_id project_id: fields: payload.tenant_id - event_type: ['share.create.*', 'share.delete.*', 'share.extend.*', 'share.shrink.*'] traits: &share_traits share_id: fields: payload.share_id user_id: fields: payload.user_id project_id: fields: payload.tenant_id snapshot_id: fields: payload.snapshot_id availability_zone: fields: payload.availability_zone status: fields: payload.status created_at: type: datetime fields: payload.created_at share_group_id: fields: payload.share_group_id size: type: int fields: payload.size name: fields: payload.name proto: fields: payload.proto is_public: fields: payload.is_public description: fields: payload.description host: fields: payload.host - event_type: ['snapshot.exists', 'snapshot.create.*', 'snapshot.delete.*', 'snapshot.update.*'] traits: <<: *cinder_traits resource_id: fields: payload.snapshot_id volume_id: fields: payload.volume_id - event_type: ['image_volume_cache.*'] traits: image_id: fields: payload.image_id host: fields: payload.host - event_type: ['image.create', 'image.update', 'image.upload', 'image.delete'] traits: &glance_crud project_id: fields: payload.owner resource_id: fields: payload.id name: fields: payload.name status: fields: payload.status created_at: type: datetime fields: payload.created_at user_id: fields: payload.owner deleted_at: type: datetime fields: payload.deleted_at size: type: int fields: payload.size - event_type: image.send traits: &glance_send receiver_project: fields: payload.receiver_tenant_id receiver_user: fields: payload.receiver_user_id user_id: fields: payload.owner_id image_id: fields: payload.image_id destination_ip: fields: payload.destination_ip bytes_sent: type: int fields: payload.bytes_sent - event_type: orchestration.stack.* traits: &orchestration_crud project_id: fields: payload.tenant_id user_id: fields: ['ctxt.trustor_user_id', 'ctxt.user_id'] resource_id: fields: payload.stack_identity name: fields: payload.name - event_type: ['identity.user.*', 'identity.project.*', 'identity.group.*', 'identity.role.*', 'identity.OS-TRUST:trust.*', 'identity.region.*', 'identity.service.*', 'identity.endpoint.*', 'identity.policy.*'] traits: &identity_crud resource_id: fields: payload.resource_info initiator_id: fields: payload.initiator.id project_id: fields: payload.initiator.project_id domain_id: fields: payload.initiator.domain_id - event_type: identity.role_assignment.* traits: &identity_role_assignment role: fields: payload.role group: fields: payload.group domain: fields: payload.domain user: fields: payload.user project: fields: payload.project - event_type: identity.authenticate traits: &identity_authenticate typeURI: fields: payload.typeURI id: fields: payload.id action: fields: payload.action eventType: fields: payload.eventType eventTime: type: datetime fields: payload.eventTime outcome: fields: payload.outcome initiator_typeURI: fields: payload.initiator.typeURI initiator_id: fields: payload.initiator.id initiator_name: fields: payload.initiator.name initiator_host_agent: fields: payload.initiator.host.agent initiator_host_addr: fields: payload.initiator.host.address target_typeURI: fields: payload.target.typeURI target_id: fields: payload.target.id observer_typeURI: fields: payload.observer.typeURI observer_id: fields: payload.observer.id - event_type: objectstore.http.request traits: &objectstore_request typeURI: fields: payload.typeURI id: fields: payload.id action: fields: payload.action eventType: fields: payload.eventType eventTime: type: datetime fields: payload.eventTime outcome: fields: payload.outcome initiator_typeURI: fields: payload.initiator.typeURI initiator_id: fields: payload.initiator.id initiator_project_id: fields: payload.initiator.project_id target_typeURI: fields: payload.target.typeURI target_id: fields: payload.target.id target_action: fields: payload.target.action target_metadata_path: fields: payload.target.metadata.path target_metadata_version: fields: payload.target.metadata.version target_metadata_container: fields: payload.target.metadata.container target_metadata_object: fields: payload.target.metadata.object observer_id: fields: payload.observer.id - event_type: ['network.*', 'subnet.*', 'port.*', 'router.*', 'floatingip.*', 'firewall.*', 'firewall_policy.*', 'firewall_rule.*', 'vpnservice.*', 'ipsecpolicy.*', 'ikepolicy.*', 'ipsec_site_connection.*'] traits: &network_traits user_id: fields: ctxt.user_id project_id: fields: ctxt.tenant_id - event_type: network.* traits: <<: *network_traits name: fields: payload.network.name resource_id: fields: ['payload.network.id', 'payload.id'] - event_type: subnet.* traits: <<: *network_traits name: fields: payload.subnet.name resource_id: fields: ['payload.subnet.id', 'payload.id'] - event_type: port.* traits: <<: *network_traits name: fields: payload.port.name resource_id: fields: ['payload.port.id', 'payload.id'] - event_type: router.* traits: <<: *network_traits name: fields: payload.router.name resource_id: fields: ['payload.router.id', 'payload.id'] - event_type: floatingip.* traits: <<: *network_traits resource_id: fields: ['payload.floatingip.id', 'payload.id'] - event_type: firewall.* traits: <<: *network_traits name: fields: payload.firewall.name resource_id: fields: ['payload.firewall.id', 'payload.id'] - event_type: firewall_policy.* traits: <<: *network_traits name: fields: payload.firewall_policy.name resource_id: fields: ['payload.firewall_policy.id', 'payload.id'] - event_type: firewall_rule.* traits: <<: *network_traits name: fields: payload.firewall_rule.name resource_id: fields: ['payload.firewall_rule.id', 'payload.id'] - event_type: vpnservice.* traits: <<: *network_traits name: fields: payload.vpnservice.name resource_id: fields: ['payload.vpnservice.id', 'payload.id'] - event_type: ipsecpolicy.* traits: <<: *network_traits name: fields: payload.ipsecpolicy.name resource_id: fields: ['payload.ipsecpolicy.id', 'payload.id'] - event_type: ikepolicy.* traits: <<: *network_traits name: fields: payload.ikepolicy.name resource_id: fields: ['payload.ikepolicy.id', 'payload.id'] - event_type: ipsec_site_connection.* traits: <<: *network_traits resource_id: fields: ['payload.ipsec_site_connection.id', 'payload.id'] - event_type: '*http.*' traits: &http_audit project_id: fields: payload.initiator.project_id user_id: fields: payload.initiator.id typeURI: fields: payload.typeURI eventType: fields: payload.eventType action: fields: payload.action outcome: fields: payload.outcome id: fields: payload.id eventTime: type: datetime fields: payload.eventTime requestPath: fields: payload.requestPath observer_id: fields: payload.observer.id target_id: fields: payload.target.id target_typeURI: fields: payload.target.typeURI target_name: fields: payload.target.name initiator_typeURI: fields: payload.initiator.typeURI initiator_id: fields: payload.initiator.id initiator_name: fields: payload.initiator.name initiator_host_address: fields: payload.initiator.host.address - event_type: '*http.response' traits: <<: *http_audit reason_code: fields: payload.reason.reasonCode - event_type: ['dns.domain.create', 'dns.domain.update', 'dns.domain.delete'] traits: &dns_domain_traits status: fields: payload.status retry: fields: payload.retry description: fields: payload.description expire: fields: payload.expire email: fields: payload.email ttl: fields: payload.ttl action: fields: payload.action name: fields: payload.name resource_id: fields: payload.id created_at: type: datetime fields: payload.created_at updated_at: type: datetime fields: payload.updated_at version: fields: payload.version parent_domain_id: fields: parent_domain_id serial: fields: payload.serial - event_type: dns.domain.exists traits: <<: *dns_domain_traits audit_period_beginning: type: datetime fields: payload.audit_period_beginning audit_period_ending: type: datetime fields: payload.audit_period_ending - event_type: ['dns.zone.create', 'dns.zone.update', 'dns.zone.delete'] traits: &dns_zone_traits resource_id: fields: payload.id project_id: fields: payload.tenant_id name: fields: payload.name description: fields: payload.description action: fields: payload.action created_at: type: datetime fields: payload.created_at email: fields: payload.email expire: fields: payload.expire pool_id: fields: payload.pool_id refresh: fields: payload.refresh retry: fields: payload.retry serial: fields: payload.serial status: fields: payload.status ttl: fields: payload.ttl type: fields: payload.type updated_at: type: datetime fields: payload.updated_at version: fields: payload.version - event_type: dns.zone.exists traits: <<: *dns_zone_traits audit_period_beginning: type: datetime fields: payload.audit_period_beginning audit_period_ending: type: datetime fields: payload.audit_period_ending - event_type: trove.* traits: &trove_base_traits instance_type: fields: payload.instance_type user_id: fields: payload.user_id resource_id: fields: payload.instance_id instance_type_id: fields: payload.instance_type_id launched_at: type: datetime fields: payload.launched_at instance_name: fields: payload.instance_name state: fields: payload.state nova_instance_id: fields: payload.nova_instance_id service_id: fields: payload.service_id created_at: type: datetime fields: payload.created_at region: fields: payload.region - event_type: ['trove.instance.create', 'trove.instance.modify_volume', 'trove.instance.modify_flavor', 'trove.instance.delete'] traits: &trove_common_traits name: fields: payload.name availability_zone: fields: payload.availability_zone instance_size: type: int fields: payload.instance_size volume_size: type: int fields: payload.volume_size nova_volume_id: fields: payload.nova_volume_id - event_type: trove.instance.create traits: <<: [*trove_base_traits, *trove_common_traits] - event_type: trove.instance.modify_volume traits: <<: [*trove_base_traits, *trove_common_traits] old_volume_size: type: int fields: payload.old_volume_size modify_at: type: datetime fields: payload.modify_at - event_type: trove.instance.modify_flavor traits: <<: [*trove_base_traits, *trove_common_traits] old_instance_size: type: int fields: payload.old_instance_size modify_at: type: datetime fields: payload.modify_at - event_type: trove.instance.delete traits: <<: [*trove_base_traits, *trove_common_traits] deleted_at: type: datetime fields: payload.deleted_at - event_type: trove.instance.exists traits: <<: *trove_base_traits display_name: fields: payload.display_name audit_period_beginning: type: datetime fields: payload.audit_period_beginning audit_period_ending: type: datetime fields: payload.audit_period_ending - event_type: profiler.* traits: project: fields: payload.project service: fields: payload.service name: fields: payload.name base_id: fields: payload.base_id trace_id: fields: payload.trace_id parent_id: fields: payload.parent_id timestamp: type: datetime fields: payload.timestamp host: fields: payload.info.host path: fields: payload.info.request.path query: fields: payload.info.request.query method: fields: payload.info.request.method scheme: fields: payload.info.request.scheme db.statement: fields: payload.info.db.statement db.params: fields: payload.info.db.params - event_type: 'magnum.cluster.*' traits: &magnum_cluster_crud id: fields: payload.id typeURI: fields: payload.typeURI eventType: fields: payload.eventType eventTime: type: datetime fields: payload.eventTime action: fields: payload.action outcome: fields: payload.outcome initiator_id: fields: payload.initiator.id initiator_typeURI: fields: payload.initiator.typeURI initiator_name: fields: payload.initiator.name initiator_host_agent: fields: payload.initiator.host.agent initiator_host_address: fields: payload.initiator.host.address target_id: fields: payload.target.id target_typeURI: fields: payload.target.typeURI observer_id: fields: payload.observer.id observer_typeURI: fields: payload.observer.typeURI - event_type: 'octavia.loadbalancer.*' traits: resource_id: fields: payload.loadbalancer_id project_id: fields: payload.project_id name: fields: payload.name description: fields: payload.description admin_state_up: fields: payload.admin_state_up availability_zone: fields: payload.availability_zone vip_address: fields: payload.vip_address vip_network_id: fields: payload.vip_network_id vip_port_id: fields: payload.vip_port_id vip_qos_policy_id: fields: payload.vip_qos_policy_id vip_subnet_id: fields: payload.vip_subnet_id - event_type: 'alarm.*' traits: id: fields: payload.alarm_id user_id: fields: payload.user_id project_id: fields: payload.project_id on_behalf_of: fields: payload.on_behalf_of severity: fields: payload.severity detail: fields: payload.detail type: fields: payload.type ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1803567 ceilometer-27.0.0.0rc1/ceilometer/pipeline/data/event_pipeline.yaml0000664000175000017500000000026015250267452024064 0ustar00zuulzuul--- sources: - name: event_source events: - "*" sinks: - event_sink sinks: - name: event_sink publishers: - notifier:// ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1813567 ceilometer-27.0.0.0rc1/ceilometer/pipeline/data/pipeline.yaml0000664000175000017500000000025715250267452022671 0ustar00zuulzuul--- sources: - name: meter_source meters: - "*" sinks: - meter_sink sinks: - name: meter_sink publishers: - gnocchi:// ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1813567 ceilometer-27.0.0.0rc1/ceilometer/pipeline/event.py0000664000175000017500000001046715250267452020766 0ustar00zuulzuul# Copyright 2012-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log import oslo_messaging from stevedore import extension from ceilometer import agent from ceilometer.event import converter from ceilometer.pipeline import base LOG = log.getLogger(__name__) class EventEndpoint(base.NotificationEndpoint): event_types = [] def __init__(self, conf, publisher): super().__init__(conf, publisher) LOG.debug('Loading event definitions') self.event_converter = converter.setup_events( conf, extension.ExtensionManager( namespace='ceilometer.event.trait_plugin')) def info(self, notifications): """Convert message at info level to Ceilometer Event. :param notifications: list of notifications """ return self.process_notifications('info', notifications) def error(self, notifications): """Convert message at error level to Ceilometer Event. :param notifications: list of notifications """ return self.process_notifications('error', notifications) def process_notifications(self, priority, notifications): for message in notifications: try: event = self.event_converter.to_event(priority, message) if event is not None: with self.publisher as p: p(event) except Exception: if not self.conf.notification.ack_on_event_error: return oslo_messaging.NotificationResult.REQUEUE LOG.exception('Fail to process a notification') return oslo_messaging.NotificationResult.HANDLED class EventSource(base.PipelineSource): """Represents a source of events. In effect it is a set of notification handlers capturing events for a set of matching notifications. """ def __init__(self, cfg): super().__init__(cfg) self.events = cfg.get('events') try: self.check_source_filtering(self.events, 'events') except agent.SourceException as err: raise base.PipelineException(err.msg, cfg) def support_event(self, event_name): return self.is_supported(self.events, event_name) class EventSink(base.Sink): def publish_events(self, events): if events: for p in self.publishers: try: p.publish_events(events) except Exception: LOG.exception("Pipeline %(pipeline)s: %(status)s " "after error from publisher %(pub)s", {'pipeline': self, 'status': 'Continue' if self.multi_publish else 'Exit', 'pub': p}) if not self.multi_publish: raise class EventPipeline(base.Pipeline): """Represents a pipeline for Events.""" def __str__(self): # NOTE(gordc): prepend a namespace so we ensure event and sample # pipelines do not have the same name. return 'event:%s' % super().__str__() def publish_data(self, events): if not isinstance(events, list): events = [events] supported = [e for e in events if self.supported(e)] self.sink.publish_events(supported) def supported(self, event): return self.source.support_event(event.event_type) class EventPipelineManager(base.PipelineManager): pm_type = 'event' pm_pipeline = EventPipeline pm_source = EventSource pm_sink = EventSink def __init__(self, conf): super().__init__( conf, conf.event_pipeline_cfg_file) def get_main_endpoints(self): return [EventEndpoint(self.conf, self.publisher())] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1813567 ceilometer-27.0.0.0rc1/ceilometer/pipeline/sample.py0000664000175000017500000001253315250267452021122 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from stevedore import extension from ceilometer import agent from ceilometer.pipeline import base LOG = log.getLogger(__name__) class SampleEndpoint(base.NotificationEndpoint): def info(self, notifications): """Convert message at info level to Ceilometer sample. :param notifications: list of notifications """ return self.process_notifications('info', notifications) def sample(self, notifications): """Convert message at sample level to Ceilometer Event. :param notifications: list of notifications """ return self.process_notifications('sample', notifications) def process_notifications(self, priority, notifications): for message in notifications: try: LOG.debug("Processing sample notification [%s] for publisher " "[%s] with priority [%s] using the agent [%s].", message, self.publisher, priority, self) with self.publisher as p: p(list(self.build_sample(message))) except Exception: LOG.exception('Fail to process notification message [%s]', message) raise def build_sample(notification): """Build sample from provided notification.""" pass class SampleSource(base.PipelineSource): """Represents a source of samples. In effect it is a set of notification handlers processing samples for a set of matching meters. Each source encapsulates meter name matching and mapping to one or more sinks for publication. """ def __init__(self, cfg): super().__init__(cfg) try: self.meters = cfg['meters'] except KeyError: raise base.PipelineException("Missing meters value", cfg) try: self.check_source_filtering(self.meters, 'meters') except agent.SourceException as err: raise base.PipelineException(err.msg, cfg) def support_meter(self, meter_name): return self.is_supported(self.meters, meter_name) class SampleSink(base.Sink): def publish_samples(self, samples): """Push samples into pipeline for publishing. :param samples: Sample list. """ if samples: for p in self.publishers: try: p.publish_samples(samples) except Exception: LOG.exception("Pipeline %(pipeline)s: Continue after " "error from publisher %(pub)s", {'pipeline': self, 'pub': p}) @staticmethod def flush(): pass class SamplePipeline(base.Pipeline): """Represents a pipeline for Samples.""" def _validate_volume(self, s): volume = s.volume if volume is None: LOG.warning( 'metering data %(counter_name)s for %(resource_id)s ' '@ %(timestamp)s has no volume (volume: None), the sample will' ' be dropped', {'counter_name': s.name, 'resource_id': s.resource_id, 'timestamp': s.timestamp if s.timestamp else 'NO TIMESTAMP'} ) return False if not isinstance(volume, (int, float)): try: volume = float(volume) except ValueError: LOG.warning( 'metering data %(counter_name)s for %(resource_id)s ' '@ %(timestamp)s has volume which is not a number ' '(volume: %(counter_volume)s), the sample will be dropped', {'counter_name': s.name, 'resource_id': s.resource_id, 'timestamp': ( s.timestamp if s.timestamp else 'NO TIMESTAMP'), 'counter_volume': volume} ) return False return True def publish_data(self, samples): if not isinstance(samples, list): samples = [samples] supported = [s for s in samples if self.supported(s) and self._validate_volume(s)] self.sink.publish_samples(supported) def supported(self, sample): return self.source.support_meter(sample.name) class SamplePipelineManager(base.PipelineManager): pm_type = 'sample' pm_pipeline = SamplePipeline pm_source = SampleSource pm_sink = SampleSink def __init__(self, conf): super().__init__( conf, conf.pipeline_cfg_file) def get_main_endpoints(self): exts = extension.ExtensionManager( namespace='ceilometer.sample.endpoint', invoke_on_load=True, invoke_args=(self.conf, self.publisher())) return [ext.obj for ext in exts] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2491276 ceilometer-27.0.0.0rc1/ceilometer/polling/0000775000175000017500000000000015250267567017131 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1813567 ceilometer-27.0.0.0rc1/ceilometer/polling/__init__.py0000664000175000017500000000000015250267452021221 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2501276 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/0000775000175000017500000000000015250267567021140 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1813567 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/__init__.py0000664000175000017500000000000015250267452023230 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1823568 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/endpoint.py0000664000175000017500000000271315250267452023326 0ustar00zuulzuul# Copyright 2014-2015 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer import keystone_client from ceilometer.polling import plugin_base as plugin LOG = log.getLogger(__name__) class EndpointDiscovery(plugin.DiscoveryBase): """Discovery that supplies service endpoints. This discovery should be used when the relevant APIs are not well suited to dividing the pollster's work into smaller pieces than a whole service at once. """ def discover(self, manager, param=None): endpoints = keystone_client.get_urls( manager.keystone, service_type=param, interface=self.conf.service_credentials.interface, region_name=self.conf.service_credentials.region_name) if not endpoints: LOG.warning('No endpoints found for service %s', "" if param is None else param) return [] return endpoints ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1833568 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/localnode.py0000664000175000017500000000155515250267452023451 0ustar00zuulzuul# Copyright 2015 Intel # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.polling import plugin_base class LocalNodeDiscovery(plugin_base.DiscoveryBase): def discover(self, manager, param=None): """Return local node as resource.""" return [self.conf.host] @property def group_id(self): return "LocalNode-%s" % self.conf.host ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1833568 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/non_openstack_credentials_discovery.py0000664000175000017500000000403715250267452031014 0ustar00zuulzuul# Copyright 2014-2015 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.polling.discovery.endpoint import EndpointDiscovery from urllib import parse as urlparse import requests LOG = log.getLogger(__name__) class NonOpenStackCredentialsDiscovery(EndpointDiscovery): """Barbican secrets discovery Discovery that supplies non-OpenStack credentials for the dynamic pollster sub-system. This solution uses the EndpointDiscovery to find the Barbican URL where we can retrieve the credentials. """ BARBICAN_URL_GET_PAYLOAD_PATTERN = "/v1/secrets/%s/payload" def discover(self, manager, param=None): if not param: return [] barbican_endpoints = super().discover(manager, "key-manager") if not barbican_endpoints: LOG.warning("No Barbican endpoints found to execute the" " credentials discovery process to [%s].", param) return [] else: LOG.debug("Barbican endpoint found [%s].", barbican_endpoints) barbican_server = next(iter(barbican_endpoints)) barbican_endpoint = self.BARBICAN_URL_GET_PAYLOAD_PATTERN % param babrican_url = urlparse.urljoin(barbican_server, barbican_endpoint) LOG.debug("Retrieving secrets from: %s.", babrican_url) resp = manager._keystone.session.get(babrican_url, authenticated=True) if resp.status_code != requests.codes.ok: resp.raise_for_status() return [resp._content] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1833568 ceilometer-27.0.0.0rc1/ceilometer/polling/discovery/tenant.py0000664000175000017500000000405415250267452022777 0ustar00zuulzuul# Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.polling import plugin_base as plugin LOG = log.getLogger(__name__) class TenantDiscovery(plugin.DiscoveryBase): """Discovery that supplies keystone tenants. This discovery should be used when the pollster's work can't be divided into smaller pieces than per-tenants. Example of this is the Swift pollster, which polls account details and does so per-project. """ def discover(self, manager, param=None): filters = {} # When ignore_disabled_projects is enabled, add a filter # to the Keystone API queries so that only enabled projects, # and projects in enabled domains, are returned. if self.conf.polling.ignore_disabled_projects: filters["enabled"] = True domains = manager.keystone.list_domains(**filters) LOG.debug( "Found %i %sKeystone domains", len(domains), "enabled " if self.conf.polling.ignore_disabled_projects else "") tenants = [] projects_log_message = ( "Found %i enabled projects in domain %s" if self.conf.polling.ignore_disabled_projects else "Found %i projects in domain %s") for domain in domains: domain_projects = manager.keystone.list_projects( domain, **filters) LOG.debug(projects_log_message, len(domain_projects), domain.name) tenants.extend(domain_projects) return tenants ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1843567 ceilometer-27.0.0.0rc1/ceilometer/polling/dynamic_pollster.py0000664000175000017500000013574115250267452023057 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Dynamic pollster component This component enables operators to create new pollsters on the fly via configuration. The configuration files are read from '/etc/ceilometer/pollsters.d/'. The pollster are defined in YAML files similar to the idea used for handling notifications. """ import copy import json import re import subprocess import time import xmltodict from oslo_log import log from requests import RequestException from ceilometer import declarative from ceilometer.polling import plugin_base from ceilometer import sample as ceilometer_sample from ceilometer import utils as ceilometer_utils from functools import reduce import operator import requests from urllib import parse as urlparse LOG = log.getLogger(__name__) def validate_sample_type(sample_type): if sample_type not in ceilometer_sample.TYPES: raise declarative.DynamicPollsterDefinitionException( "Invalid sample type [%s]. Valid ones are [%s]." % (sample_type, ceilometer_sample.TYPES)) class XMLResponseHandler: """This response handler converts an XML in string format to a dict""" @staticmethod def handle(response): return xmltodict.parse(response) class JsonResponseHandler: """This response handler converts a JSON in string format to a dict""" @staticmethod def handle(response): return json.loads(response) class PlainTextResponseHandler: """Response handler converts string to a list of dict [{'out'=}]""" @staticmethod def handle(response): return [{'out': str(response)}] VALID_HANDLERS = { 'json': JsonResponseHandler, 'xml': XMLResponseHandler, 'text': PlainTextResponseHandler } def validate_response_handler(val): if not isinstance(val, list): raise declarative.DynamicPollsterDefinitionException( "Invalid response_handlers configuration. It must be a list. " "Provided value type: %s" % type(val).__name__) for value in val: if value not in VALID_HANDLERS: raise declarative.DynamicPollsterDefinitionException( "Invalid response_handler value [%s]. Accepted values " "are [%s]" % (value, ', '.join(list(VALID_HANDLERS)))) def validate_extra_metadata_skip_samples(val): if not isinstance(val, list) or next( filter(lambda v: not isinstance(v, dict), val), None): raise declarative.DynamicPollsterDefinitionException( "Invalid extra_metadata_fields_skip configuration." " It must be a list of maps. Provided value: %s," " value type: %s." % (val, type(val).__name__)) class ResponseHandlerChain: """Tries to convert a string to a dict using the response handlers""" def __init__(self, response_handlers, **meta): if not isinstance(response_handlers, list): response_handlers = list(response_handlers) self.response_handlers = response_handlers self.meta = meta def handle(self, response): failed_handlers = [] for handler in self.response_handlers: try: return handler.handle(response) except Exception as e: handler_name = handler.__name__ failed_handlers.append(handler_name) LOG.debug( "Error handling response [%s] with handler [%s]: %s. " "We will try the next one, if multiple handlers were " "configured.", response, handler_name, e) handlers_str = ', '.join(failed_handlers) raise declarative.InvalidResponseTypeException( "No remaining handlers to handle the response [%s], " "used handlers [%s]. [%s]." % (response, handlers_str, self.meta)) class PollsterDefinitionBuilder: def __init__(self, definitions): self.definitions = definitions def build_definitions(self, configurations): supported_definitions = [] for definition in self.definitions: if definition.is_field_applicable_to_definition(configurations): supported_definitions.append(definition) if not supported_definitions: raise declarative.DynamicPollsterDefinitionException( "Your configurations do not fit any type of DynamicPollsters, " "please recheck them. Used configurations are [%s]." % configurations) definition_name = self.join_supported_definitions_names( supported_definitions) definition_parents = tuple(supported_definitions) definition_attribs = {'extra_definitions': reduce( lambda d1, d2: d1 + d2, map(lambda df: df.extra_definitions, supported_definitions))} definition_type = type(definition_name, definition_parents, definition_attribs) return definition_type(configurations) @staticmethod def join_supported_definitions_names(supported_definitions): return ''.join(map(lambda df: df.__name__, supported_definitions)) class PollsterSampleExtractor: def __init__(self, definitions): self.definitions = definitions def generate_new_metadata_fields(self, metadata=None, pollster_definitions=None): pollster_definitions =\ pollster_definitions or self.definitions.configurations metadata_mapping = pollster_definitions['metadata_mapping'] if not metadata_mapping or not metadata: return metadata_keys = list(metadata.keys()) for k in metadata_keys: if k not in metadata_mapping: continue new_key = metadata_mapping[k] metadata[new_key] = metadata[k] LOG.debug("Generating new key [%s] with content [%s] of key [%s]", new_key, metadata[k], k) if pollster_definitions['preserve_mapped_metadata']: continue k_value = metadata.pop(k) LOG.debug("Removed key [%s] with value [%s] from " "metadata set that is sent to Gnocchi.", k, k_value) def generate_sample( self, pollster_sample, pollster_definitions=None, **kwargs): pollster_definitions =\ pollster_definitions or self.definitions.configurations metadata = dict() if 'metadata_fields' in pollster_definitions: for k in pollster_definitions['metadata_fields']: val = self.retrieve_attribute_nested_value( pollster_sample, value_attribute=k, definitions=self.definitions.configurations) LOG.debug("Assigning value [%s] to metadata key [%s].", val, k) metadata[k] = val self.generate_new_metadata_fields( metadata=metadata, pollster_definitions=pollster_definitions) pollster_sample['metadata'] = metadata extra_metadata = self.definitions.retrieve_extra_metadata( kwargs['manager'], pollster_sample, kwargs['conf']) LOG.debug("Extra metadata [%s] collected for sample [%s].", extra_metadata, pollster_sample) for key in extra_metadata.keys(): if key in metadata.keys(): LOG.warning("The extra metadata key [%s] already exist in " "pollster current metadata set [%s]. Therefore, " "we will ignore it with its value [%s].", key, metadata, extra_metadata[key]) continue metadata[key] = extra_metadata[key] return ceilometer_sample.Sample( timestamp=ceilometer_utils.isotime(), name=pollster_definitions['name'], type=pollster_definitions['sample_type'], unit=pollster_definitions['unit'], volume=pollster_sample['value'], user_id=pollster_sample.get("user_id"), project_id=pollster_sample.get("project_id"), resource_id=pollster_sample.get("id"), resource_metadata=metadata) def retrieve_attribute_nested_value(self, json_object, value_attribute=None, definitions=None, **kwargs): if not definitions: definitions = self.definitions.configurations attribute_key = value_attribute if not attribute_key: attribute_key = self.definitions.extract_attribute_key() LOG.debug( "Retrieving the nested keys [%s] from [%s] or pollster [""%s].", attribute_key, json_object, definitions["name"]) keys_and_operations = attribute_key.split("|") attribute_key = keys_and_operations[0].strip() if attribute_key == ".": value = json_object else: nested_keys = attribute_key.split(".") value = reduce(operator.getitem, nested_keys, json_object) return self.operate_value(keys_and_operations, value, definitions) def operate_value(self, keys_and_operations, value, definitions): # We do not have operations to be executed against the value extracted if len(keys_and_operations) < 2: return value for operation in keys_and_operations[1::]: # The operation must be performed onto the 'value' variable if 'value' not in operation: raise declarative.DynamicPollsterDefinitionException( "The attribute field operation [%s] must use the [" "value] variable." % operation, definitions) LOG.debug("Executing operation [%s] against value[%s] for " "pollster [%s].", operation, value, definitions["name"]) value = eval(operation.strip()) # noqa: S307 LOG.debug("Result [%s] of operation [%s] for pollster [%s].", value, operation, definitions["name"]) return value class SimplePollsterSampleExtractor(PollsterSampleExtractor): def generate_single_sample(self, pollster_sample, **kwargs): value = self.retrieve_attribute_nested_value( pollster_sample) value = self.definitions.value_mapper.map_or_skip_value( value, pollster_sample) if isinstance(value, SkippedSample): return value pollster_sample['value'] = value return self.generate_sample(pollster_sample, **kwargs) def extract_sample(self, pollster_sample, **kwargs): sample = self.generate_single_sample(pollster_sample, **kwargs) if isinstance(sample, SkippedSample): return sample yield sample class MultiMetricPollsterSampleExtractor(PollsterSampleExtractor): def extract_sample(self, pollster_sample, **kwargs): pollster_definitions = self.definitions.configurations value = self.retrieve_attribute_nested_value( pollster_sample, definitions=pollster_definitions) LOG.debug("We are dealing with a multi metric pollster. The " "value we are processing is the following: [%s].", value) self.validate_sample_is_list(value) sub_metric_placeholder, pollster_name, sub_metric_attribute_name = \ self.extract_names_attrs() value_attribute = \ self.extract_field_name_from_value_attribute_configuration() LOG.debug("Using attribute [%s] to look for values in the " "multi metric pollster [%s] with sample [%s]", value_attribute, pollster_definitions, value) pollster_definitions = copy.deepcopy(pollster_definitions) yield from self.extract_sub_samples(value, sub_metric_attribute_name, pollster_name, value_attribute, sub_metric_placeholder, pollster_definitions, pollster_sample, **kwargs) def extract_sub_samples(self, value, sub_metric_attribute_name, pollster_name, value_attribute, sub_metric_placeholder, pollster_definitions, pollster_sample, **kwargs): for sub_sample in value: sub_metric_name = sub_sample[sub_metric_attribute_name] new_metric_name = pollster_name.replace( sub_metric_placeholder, sub_metric_name) pollster_definitions['name'] = new_metric_name actual_value = self.retrieve_attribute_nested_value( sub_sample, value_attribute, definitions=pollster_definitions) pollster_sample['value'] = actual_value if self.should_skip_generate_sample(actual_value, sub_sample, sub_metric_name): continue yield self.generate_sample( pollster_sample, pollster_definitions, **kwargs) def extract_field_name_from_value_attribute_configuration(self): value_attribute = self.definitions.configurations['value_attribute'] return self.definitions.pattern_pollster_value_attribute.match( value_attribute).group(3)[1::] def extract_names_attrs(self): pollster_name = self.definitions.configurations['name'] sub_metric_placeholder = pollster_name.split(".").pop() return (sub_metric_placeholder, pollster_name, self.definitions.pattern_pollster_name.match( "." + sub_metric_placeholder).group(2)) def validate_sample_is_list(self, value): pollster_definitions = self.definitions.configurations if not isinstance(value, list): raise declarative.DynamicPollsterException( "Multi metric pollster defined, but the value [%s]" " obtained with [%s] attribute is not a list" " of objects." % (value, pollster_definitions['value_attribute']), pollster_definitions) def should_skip_generate_sample(self, actual_value, sub_sample, sub_metric_name): skip_sample_values = \ self.definitions.configurations['skip_sample_values'] if actual_value in skip_sample_values: LOG.debug( "Skipping multi metric sample [%s] because " "value [%s] is configured to be skipped in " "skip list [%s].", sub_sample, actual_value, skip_sample_values) return True if sub_metric_name in skip_sample_values: LOG.debug( "Skipping sample [%s] because its sub-metric " "name [%s] is configured to be skipped in " "skip list [%s].", sub_sample, sub_metric_name, skip_sample_values) return True return False class PollsterValueMapper: def __init__(self, definitions): self.definitions = definitions def map_or_skip_value(self, value, pollster_sample): skip_sample_values = \ self.definitions.configurations['skip_sample_values'] if value in skip_sample_values: LOG.debug("Skipping sample [%s] because value [%s] " "is configured to be skipped in skip list [%s].", pollster_sample, value, skip_sample_values) return SkippedSample() return self.execute_value_mapping(value) def execute_value_mapping(self, value): value_mapping = self.definitions.configurations['value_mapping'] if not value_mapping: return value if value in value_mapping: old_value = value value = value_mapping[value] LOG.debug("Value mapped from [%s] to [%s]", old_value, value) else: default_value = \ self.definitions.configurations['default_value'] LOG.warning( "Value [%s] was not found in value_mapping [%s]; " "therefore, we will use the default [%s].", value, value_mapping, default_value) value = default_value return value class PollsterDefinition: """Represents a dynamic pollster configuration/parameter It abstract the job of developers when creating or extending parameters, such as validating parameters name, values and so on. """ def __init__(self, name, required=False, on_missing=lambda df: df.default, default=None, validation_regex=None, creatable=True, validator=None): """Create a dynamic pollster configuration/parameter :param name: the name of the pollster parameter/configuration. :param required: indicates if the configuration/parameter is optional or not. :param on_missing: function that is executed when the parameter/configuration is missing. :param default: the default value to be used. :param validation_regex: the regular expression used to validate the name of the configuration/parameter. :param creatable: it is an override mechanism to avoid creating a configuration/parameter with the default value. The default is ``True``; therefore, we always use the default value. However, we can disable the use of the default value by setting ``False``. When we set this configuration to ``False``, the parameter is not added to the definition dictionary if not defined by the operator in the pollster YAML configuration file. :param validator: function used to validate the value of the parameter/configuration when it is given by the user. This function signature should receive a value that is the value of the parameter to be validate. """ self.name = name self.required = required self.on_missing = on_missing self.validation_regex = validation_regex self.creatable = creatable self.default = default if self.validation_regex: self.validation_pattern = re.compile(self.validation_regex) self.validator = validator def validate(self, val): if val is None: return self.on_missing(self) if self.validation_regex and not self.validation_pattern.match(val): raise declarative.DynamicPollsterDefinitionException( "Pollster %s [%s] does not match [%s]." % (self.name, val, self.validation_regex)) if self.validator: self.validator(val) return val class PollsterDefinitions: POLLSTER_VALID_NAMES_REGEXP = r"^([\w-]+)(\.[\w-]+)*(\.{[\w-]+})?$" EXTERNAL_ENDPOINT_TYPE = "external" standard_definitions = [ PollsterDefinition(name='name', required=True, validation_regex=POLLSTER_VALID_NAMES_REGEXP), PollsterDefinition(name='sample_type', required=True, validator=validate_sample_type), PollsterDefinition(name='unit', required=True), PollsterDefinition(name='endpoint_type', required=True), PollsterDefinition(name='url_path', required=True), PollsterDefinition(name='metadata_fields', creatable=False), PollsterDefinition(name='skip_sample_values', default=[]), PollsterDefinition(name='value_mapping', default={}), PollsterDefinition(name='default_value', default=-1), PollsterDefinition(name='metadata_mapping', default={}), PollsterDefinition(name='preserve_mapped_metadata', default=True), PollsterDefinition(name='response_entries_key'), PollsterDefinition(name='next_sample_url_attribute'), PollsterDefinition(name='user_id_attribute', default="user_id"), PollsterDefinition(name='resource_id_attribute', default="id"), PollsterDefinition(name='project_id_attribute', default="project_id"), PollsterDefinition(name='headers'), PollsterDefinition(name='timeout', default=30), PollsterDefinition(name='extra_metadata_fields_cache_seconds', default=3600), PollsterDefinition(name='extra_metadata_fields'), PollsterDefinition(name='extra_metadata_fields_skip', default=[{}], validator=validate_extra_metadata_skip_samples), PollsterDefinition(name='response_handlers', default=['json'], validator=validate_response_handler), PollsterDefinition(name='base_metadata', default={}) ] extra_definitions = [] def __init__(self, configurations): self.configurations = configurations self.value_mapper = PollsterValueMapper(self) self.definitions = self.map_definitions() self.validate_configurations(configurations) self.validate_missing() self.sample_gatherer = PollsterSampleGatherer(self) self.sample_extractor = SimplePollsterSampleExtractor(self) self.response_cache = {} def validate_configurations(self, configurations): for k, v in self.definitions.items(): if configurations.get(k) is not None: self.configurations[k] = self.definitions[k].validate( self.configurations[k]) elif self.definitions[k].creatable: self.configurations[k] = self.definitions[k].default @staticmethod def is_field_applicable_to_definition(configurations): return True def map_definitions(self): definitions = dict( map(lambda df: (df.name, df), self.standard_definitions)) extra_definitions = dict( map(lambda df: (df.name, df), self.extra_definitions)) definitions.update(extra_definitions) return definitions def extract_attribute_key(self): pass def validate_missing(self): required_configurations = map(lambda fdf: fdf.name, filter(lambda df: df.required, self.definitions.values())) missing = list(filter( lambda rf: rf not in map(lambda f: f[0], filter(lambda f: f[1], self.configurations.items())), required_configurations)) if missing: raise declarative.DynamicPollsterDefinitionException( "Required fields %s not specified." % missing, self.configurations) def should_skip_extra_metadata(self, skip, sample): match_msg = "Sample [%s] %smatches with configured" \ " extra_metadata_fields_skip [%s]." if skip == sample: LOG.debug(match_msg, sample, "", skip) return True if not isinstance(skip, dict) or not isinstance(sample, dict): LOG.debug(match_msg, sample, "not ", skip) return False for key in skip: if key not in sample: LOG.debug(match_msg, sample, "not ", skip) return False if not self.should_skip_extra_metadata(skip[key], sample[key]): LOG.debug(match_msg, sample, "not ", skip) return False LOG.debug(match_msg, sample, "", skip) return True def skip_sample(self, request_sample, skips): for skip in skips: if not skip: continue if self.should_skip_extra_metadata(skip, request_sample): LOG.debug("Skipping extra_metadata_field gathering for " "sample [%s] as defined in the " "extra_metadata_fields_skip [%s]", request_sample, skip) return True return False def retrieve_extra_metadata(self, manager, request_sample, pollster_conf): extra_metadata_fields = self.configurations['extra_metadata_fields'] if extra_metadata_fields: extra_metadata_samples = {} extra_metadata_by_name = {} if not isinstance(extra_metadata_fields, (list, tuple)): extra_metadata_fields = [extra_metadata_fields] for ext_metadata in extra_metadata_fields: ext_metadata.setdefault( 'extra_metadata_fields_skip', self.configurations['extra_metadata_fields_skip']) ext_metadata.setdefault( 'sample_type', self.configurations['sample_type']) ext_metadata.setdefault('unit', self.configurations['unit']) ext_metadata.setdefault( 'value_attribute', ext_metadata.get( 'value', self.configurations['value_attribute'])) ext_metadata['base_metadata'] = { 'extra_metadata_captured': extra_metadata_samples, 'extra_metadata_by_name': extra_metadata_by_name, 'sample': request_sample } parent_cache_ttl = self.configurations[ 'extra_metadata_fields_cache_seconds'] cache_ttl = ext_metadata.get( 'extra_metadata_fields_cache_seconds', parent_cache_ttl ) response_cache = self.response_cache extra_metadata_pollster = DynamicPollster( ext_metadata, conf=pollster_conf, cache_ttl=cache_ttl, extra_metadata_responses_cache=response_cache, ) skips = ext_metadata['extra_metadata_fields_skip'] if self.skip_sample(request_sample, skips): continue resources = [None] if ext_metadata.get('endpoint_type'): resources = manager.discover([ extra_metadata_pollster.default_discovery], {}) samples = extra_metadata_pollster.get_samples( manager, None, resources) for sample in samples: self.fill_extra_metadata_samples( extra_metadata_by_name, extra_metadata_samples, sample) return extra_metadata_samples LOG.debug("No extra metadata to be captured for pollsters [%s] and " "request sample [%s].", self.definitions, request_sample) return {} def fill_extra_metadata_samples(self, extra_metadata_by_name, extra_metadata_samples, sample): extra_metadata_samples[sample.name] = sample.volume LOG.debug("Merging the sample metadata [%s] of the " "extra_metadata_field [%s], with the " "extra_metadata_samples [%s].", sample.resource_metadata, sample.name, extra_metadata_samples) for key, value in sample.resource_metadata.items(): if value is None and key in extra_metadata_samples: LOG.debug("Metadata [%s] for extra_metadata_field [%s] " "is None, skipping metadata override by None " "value", key, sample.name) continue extra_metadata_samples[key] = value extra_metadata_by_name[sample.name] = { 'value': sample.volume, 'metadata': sample.resource_metadata } LOG.debug("extra_metadata_samples after merging: [%s].", extra_metadata_samples) class MultiMetricPollsterDefinitions(PollsterDefinitions): MULTI_METRIC_POLLSTER_NAME_REGEXP = r".*(\.{(\w+)})$" pattern_pollster_name = re.compile( MULTI_METRIC_POLLSTER_NAME_REGEXP) MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP = r"^(\[(\w+)\])((\.\w+)+)$" pattern_pollster_value_attribute = re.compile( MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP) extra_definitions = [ PollsterDefinition( name='value_attribute', required=True, validation_regex=MULTI_METRIC_POLLSTER_VALUE_ATTRIBUTE_REGEXP), ] def __init__(self, configurations): super().__init__(configurations) self.sample_extractor = MultiMetricPollsterSampleExtractor(self) @staticmethod def is_field_applicable_to_definition(configurations): return configurations.get( 'name') and MultiMetricPollsterDefinitions.\ pattern_pollster_name.match(configurations['name']) def extract_attribute_key(self): return self.pattern_pollster_value_attribute.match( self.configurations['value_attribute']).group(2) class SingleMetricPollsterDefinitions(PollsterDefinitions): extra_definitions = [ PollsterDefinition(name='value_attribute', required=True)] def __init__(self, configurations): super().__init__(configurations) def extract_attribute_key(self): return self.configurations['value_attribute'] @staticmethod def is_field_applicable_to_definition(configurations): return not MultiMetricPollsterDefinitions. \ is_field_applicable_to_definition(configurations) class PollsterSampleGatherer: def __init__(self, definitions): self.definitions = definitions self.response_handler_chain = ResponseHandlerChain( map(VALID_HANDLERS.get, self.definitions.configurations['response_handlers']), url_path=definitions.configurations['url_path'] ) def get_cache_key(self, definitions, **kwargs): return self.get_request_linked_samples_url(kwargs, definitions) def get_cached_response(self, definitions, **kwargs): if self.definitions.cache_ttl == 0: return cache_key = self.get_cache_key(definitions, **kwargs) response_cache = self.definitions.response_cache cached_response, max_ttl_for_cache = response_cache.get( cache_key, (None, None)) current_time = time.time() if cached_response and max_ttl_for_cache >= current_time: LOG.debug("Returning response [%s] for request [%s] as the TTL " "[max=%s, current_time=%s] has not expired yet.", cached_response, definitions, max_ttl_for_cache, current_time) return cached_response if cached_response and max_ttl_for_cache < current_time: LOG.debug("Cleaning cached response [%s] for request [%s] " "as the TTL [max=%s, current_time=%s] has expired.", cached_response, definitions, max_ttl_for_cache, current_time) response_cache.pop(cache_key, None) def store_cached_response(self, definitions, resp, **kwargs): if self.definitions.cache_ttl == 0: return cache_key = self.get_cache_key(definitions, **kwargs) extra_metadata_fields_cache_seconds = self.definitions.cache_ttl max_ttl_for_cache = time.time() + extra_metadata_fields_cache_seconds cache_tuple = (resp, max_ttl_for_cache) self.definitions.response_cache[cache_key] = cache_tuple @property def default_discovery(self): return 'endpoint:' + self.definitions.configurations['endpoint_type'] def execute_request_get_samples(self, **kwargs): return self.execute_request_for_definitions( self.definitions.configurations, **kwargs) def execute_request_for_definitions(self, definitions, **kwargs): if response_dict := self.get_cached_response(definitions, **kwargs): url = 'cached' else: resp, url = self._internal_execute_request_get_samples( definitions=definitions, **kwargs) response_dict = self.response_handler_chain.handle(resp.text) self.store_cached_response(definitions, response_dict, **kwargs) entry_size = len(response_dict) LOG.debug("Entries [%s] in the DICT for request [%s] " "for dynamic pollster [%s].", response_dict, url, definitions['name']) if entry_size > 0: samples = self.retrieve_entries_from_response( response_dict, definitions) url_to_next_sample = self.get_url_to_next_sample( response_dict, definitions) self.prepare_samples(definitions, samples, **kwargs) if url_to_next_sample: kwargs['next_sample_url'] = url_to_next_sample samples += self.execute_request_for_definitions( definitions=definitions, **kwargs) return samples return [] def prepare_samples( self, definitions, samples, execute_id_overrides=True, **kwargs): if samples and execute_id_overrides: for request_sample in samples: user_id_attribute = definitions.get( 'user_id_attribute', 'user_id') project_id_attribute = definitions.get( 'project_id_attribute', 'project_id') resource_id_attribute = definitions.get( 'resource_id_attribute', 'id') self.generate_new_attributes_in_sample( request_sample, user_id_attribute, 'user_id') self.generate_new_attributes_in_sample( request_sample, project_id_attribute, 'project_id') self.generate_new_attributes_in_sample( request_sample, resource_id_attribute, 'id') def generate_new_attributes_in_sample( self, sample, attribute_key, new_attribute_key): if attribute_key == new_attribute_key: LOG.debug("We do not need to generate new attribute as the " "attribute_key[%s] and the new_attribute_key[%s] " "configurations are the same.", attribute_key, new_attribute_key) return if attribute_key: attribute_value = self.definitions.sample_extractor.\ retrieve_attribute_nested_value(sample, attribute_key) LOG.debug("Mapped attribute [%s] to value [%s] in sample [%s].", attribute_key, attribute_value, sample) sample[new_attribute_key] = attribute_value def get_url_to_next_sample(self, resp, definitions): linked_sample_extractor = definitions.get('next_sample_url_attribute') if not linked_sample_extractor: return None try: return self.definitions.sample_extractor.\ retrieve_attribute_nested_value(resp, linked_sample_extractor) except KeyError: LOG.debug("There is no next sample url for the sample [%s] using " "the configuration [%s]", resp, linked_sample_extractor) return None def _internal_execute_request_get_samples(self, definitions=None, keystone_client=None, **kwargs): if not definitions: definitions = self.definitions.configurations url = self.get_request_linked_samples_url(kwargs, definitions) request_arguments = self.create_request_arguments(definitions) LOG.debug("Executing request against [url=%s] with parameters [" "%s] for pollsters [%s]", url, request_arguments, definitions["name"]) resp = keystone_client.session.get(url, **request_arguments) if resp.status_code != requests.codes.ok: resp.raise_for_status() return resp, url def create_request_arguments(self, definitions): request_args = { "authenticated": True } request_headers = definitions.get('headers', []) if request_headers: request_args['headers'] = request_headers request_args['timeout'] = definitions.get('timeout', 300) return request_args def get_request_linked_samples_url(self, kwargs, definitions): next_sample_url = kwargs.get('next_sample_url') if next_sample_url: return self.get_next_page_url(kwargs, next_sample_url) LOG.debug("Generating url with [%s] and path [%s].", kwargs, definitions['url_path']) return self.get_request_url( kwargs, definitions['url_path']) def get_next_page_url(self, kwargs, next_sample_url): parse_result = urlparse.urlparse(next_sample_url) if parse_result.netloc: return next_sample_url return self.get_request_url(kwargs, next_sample_url) def get_request_url(self, kwargs, url_path): endpoint = kwargs['resource'] params = copy.deepcopy( self.definitions.configurations.get( 'base_metadata', {})) try: url_path = eval(url_path, params) # noqa: S307 except Exception: LOG.debug("Cannot eval path [%s] with params [%s]," " using [%s] instead.", url_path, params, url_path) return urlparse.urljoin((endpoint if endpoint.endswith("/") else (endpoint + "/")), url_path) def retrieve_entries_from_response(self, response_json, definitions): if isinstance(response_json, list): return response_json first_entry_name = definitions.get('response_entries_key') if not first_entry_name: try: first_entry_name = next(iter(response_json)) except RuntimeError as e: LOG.debug("Generator threw a StopIteration " "and we need to catch it [%s].", e) return self.definitions.sample_extractor.\ retrieve_attribute_nested_value(response_json, first_entry_name) class NonOpenStackApisPollsterDefinition(PollsterDefinitions): extra_definitions = [ PollsterDefinition(name='value_attribute', required=True), PollsterDefinition(name='module', required=True), PollsterDefinition(name='authentication_object', required=True), PollsterDefinition(name='barbican_secret_id', default=""), PollsterDefinition(name='authentication_parameters', default=""), PollsterDefinition(name='endpoint_type')] def __init__(self, configurations): super().__init__( configurations) self.sample_gatherer = NonOpenStackApisSamplesGatherer(self) @staticmethod def is_field_applicable_to_definition(configurations): return configurations.get('module') class HostCommandPollsterDefinition(PollsterDefinitions): extra_definitions = [ PollsterDefinition(name='endpoint_type', required=False), PollsterDefinition(name='url_path', required=False), PollsterDefinition(name='host_command', required=True)] def __init__(self, configurations): super().__init__( configurations) self.sample_gatherer = HostCommandSamplesGatherer(self) @staticmethod def is_field_applicable_to_definition(configurations): return configurations.get('host_command') class HostCommandSamplesGatherer(PollsterSampleGatherer): class Response: def __init__(self, text): self.text = text def get_cache_key(self, definitions, **kwargs): return self.get_command(definitions) def _internal_execute_request_get_samples(self, definitions, **kwargs): command = self.get_command(definitions, **kwargs) LOG.debug('Running Host command: [%s]', command) result = subprocess.getoutput(command) # noqa: S605 LOG.debug('Host command [%s] result: [%s]', command, result) return self.Response(result), command def get_command(self, definitions, next_sample_url=None, **kwargs): command = next_sample_url or definitions['host_command'] params = copy.deepcopy( self.definitions.configurations.get( 'base_metadata', {})) try: command = eval(command, params) # noqa: S307 except Exception: LOG.debug("Cannot eval command [%s] with params [%s]," " using [%s] instead.", command, params, command) return command @property def default_discovery(self): return 'local_node' class NonOpenStackApisSamplesGatherer(PollsterSampleGatherer): @property def default_discovery(self): return 'barbican:' + \ self.definitions.configurations['barbican_secret_id'] def _internal_execute_request_get_samples(self, definitions, **kwargs): credentials = kwargs['resource'] override_credentials = definitions['authentication_parameters'] if override_credentials: credentials = override_credentials if not isinstance(credentials, str): credentials = self.normalize_credentials_to_string(credentials) url = self.get_request_linked_samples_url(kwargs, definitions) authenticator_module_name = definitions['module'] authenticator_class_name = definitions['authentication_object'] imported_module = __import__(authenticator_module_name) authenticator_class = getattr(imported_module, authenticator_class_name) authenticator_arguments = list(map(str.strip, credentials.split(","))) authenticator_instance = authenticator_class(*authenticator_arguments) request_arguments = self.create_request_arguments(definitions) request_arguments["auth"] = authenticator_instance LOG.debug("Executing request against [url=%s] with parameters [" "%s] for pollsters [%s]", url, request_arguments, definitions["name"]) resp = requests.get(url, **request_arguments) # noqa: S113 if resp.status_code != requests.codes.ok: raise declarative.NonOpenStackApisDynamicPollsterException( "Error while executing request[%s]." " Status[%s] and reason [%s]." % (url, resp.status_code, resp.reason)) return resp, url @staticmethod def normalize_credentials_to_string(credentials): if isinstance(credentials, bytes): credentials = credentials.decode('utf-8') else: credentials = str(credentials) LOG.debug("Credentials [%s] were not defined as a string. " "Therefore, we converted it to a string like object.", credentials) return credentials def create_request_arguments(self, definitions): request_arguments = super().create_request_arguments( definitions) request_arguments.pop("authenticated") return request_arguments def get_request_url(self, kwargs, url_path): endpoint = self.definitions.configurations['url_path'] if endpoint == url_path: return url_path return urlparse.urljoin((endpoint if endpoint.endswith("/") else (endpoint + "/")), url_path) def generate_new_attributes_in_sample( self, sample, attribute_key, new_attribute_key): if attribute_key: attribute_value = self.definitions.sample_extractor. \ retrieve_attribute_nested_value(sample, attribute_key) LOG.debug("Mapped attribute [%s] to value [%s] in sample [%s].", attribute_key, attribute_value, sample) sample[new_attribute_key] = attribute_value class SkippedSample: pass class DynamicPollster(plugin_base.PollsterBase): # Mandatory name field name = "" def __init__(self, pollster_definitions={}, conf=None, cache_ttl=0, extra_metadata_responses_cache=None, supported_definitions=[HostCommandPollsterDefinition, NonOpenStackApisPollsterDefinition, MultiMetricPollsterDefinitions, SingleMetricPollsterDefinitions]): super().__init__(conf) self.supported_definitions = supported_definitions LOG.debug("%s instantiated with [%s]", __name__, pollster_definitions) self.definitions = PollsterDefinitionBuilder( self.supported_definitions).build_definitions(pollster_definitions) self.definitions.cache_ttl = cache_ttl self.definitions.response_cache = extra_metadata_responses_cache if extra_metadata_responses_cache is None: self.definitions.response_cache = {} self.pollster_definitions = self.definitions.configurations if 'metadata_fields' in self.pollster_definitions: LOG.debug("Metadata fields configured to [%s].", self.pollster_definitions['metadata_fields']) self.name = self.pollster_definitions['name'] self.obj = self @property def default_discovery(self): return self.definitions.sample_gatherer.default_discovery def load_samples(self, resource, manager): try: return self.definitions.sample_gatherer.\ execute_request_get_samples(manager=manager, resource=resource, keystone_client=manager._keystone) except RequestException as e: LOG.warning("Error [%s] while loading samples for [%s] " "for dynamic pollster [%s].", e, resource, self.name) return list([]) def get_samples(self, manager, cache, resources): if not resources: LOG.debug("No resources received for processing.") yield None for r in resources: LOG.debug("Executing get sample for resource [%s].", r) samples = self.load_samples(r, manager) if not isinstance(samples, (list, tuple)): samples = [samples] for pollster_sample in samples: sample = self.extract_sample( pollster_sample, manager=manager, resource=r, conf=self.conf) if isinstance(sample, SkippedSample): continue yield from sample def extract_sample(self, pollster_sample, **kwargs): return self.definitions.sample_extractor.extract_sample( pollster_sample, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1853566 ceilometer-27.0.0.0rc1/ceilometer/polling/manager.py0000664000175000017500000012770715250267452021124 0ustar00zuulzuul# # Copyright 2013 Julien Danjou # Copyright 2014-2017 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import glob import itertools import logging import os import queue import random import socket import threading import uuid from concurrent import futures import cotyledon from futurist import periodics from keystoneauth1 import exceptions as ka_exceptions from oslo_config import cfg from oslo_log import log import oslo_messaging from oslo_utils import netutils from oslo_utils import timeutils from stevedore import extension from tooz import coordination from urllib import parse as urlparse from ceilometer import agent from ceilometer import cache_utils from ceilometer import declarative from ceilometer import keystone_client from ceilometer import messaging from ceilometer.polling import dynamic_pollster from ceilometer.polling import plugin_base from ceilometer.polling import prom_exporter from ceilometer.publisher import utils as publisher_utils from ceilometer import utils LOG = log.getLogger(__name__) POLLING_OPTS = [ cfg.StrOpt('cfg_file', default="polling.yaml", help="Configuration file for polling definition." ), cfg.StrOpt('heartbeat_socket_dir', default=None, help="Path to directory where socket file for polling " "heartbeat will be created."), cfg.StrOpt('partitioning_group_prefix', deprecated_group='central', help='Work-load partitioning group prefix. Use only if you ' 'want to run multiple polling agents with different ' 'config files. For each sub-group of the agent ' 'pool with the same partitioning_group_prefix a disjoint ' 'subset of pollsters should be loaded.'), cfg.IntOpt('batch_size', default=50, help='Batch size of samples to send to notification agent, ' 'Set to 0 to disable. When prometheus exporter feature ' 'is used, this should be largered than maximum number of ' 'samples per metric.'), cfg.MultiStrOpt('pollsters_definitions_dirs', default=["/etc/ceilometer/pollsters.d"], help="List of directories with YAML files used " "to created pollsters."), cfg.BoolOpt('identity_name_discovery', deprecated_name='tenant_name_discovery', default=False, help='Identify project and user names from polled samples. ' 'By default, collecting these values is disabled due ' 'to the fact that it could overwhelm keystone service ' 'with lots of continuous requests depending upon the ' 'number of projects, users and samples polled from ' 'the environment. While using this feature, it is ' 'recommended that ceilometer be configured with a ' 'caching backend to reduce the number of calls ' 'made to keystone.'), cfg.BoolOpt('enable_notifications', default=True, help='Whether the polling service should be sending ' 'notifications after polling cycles.'), cfg.BoolOpt('enable_prometheus_exporter', default=False, help='Allow this ceilometer polling instance to ' 'expose directly the retrieved metrics in Prometheus ' 'format.'), cfg.ListOpt('prometheus_listen_addresses', default=["127.0.0.1:9101"], help='A list of ipaddr:port combinations on which ' 'the exported metrics will be exposed.'), cfg.BoolOpt('ignore_disabled_projects', default=False, help='Whether the polling service should ignore ' 'disabled projects or not.'), cfg.BoolOpt('prometheus_tls_enable', default=False, help='Whether it will expose tls metrics or not'), cfg.StrOpt('prometheus_tls_certfile', default=None, help='The certificate file to allow this ceilometer to ' 'expose tls scrape endpoints'), cfg.StrOpt('prometheus_tls_keyfile', default=None, help='The private key to allow this ceilometer to ' 'expose tls scrape endpoints'), cfg.IntOpt('threads_to_process_pollsters', default=1, min=0, help='The number of threads used to process the pollsters.' 'The value one (1) means that the processing is in a' 'serial fashion (not ordered!). The value zero (0) means ' 'that the we will use as much threads as the number of ' 'pollsters configured in the polling task. Any other' 'positive integer can be used to fix an upper bound limit' 'to the number of threads used for processing pollsters in' 'parallel. One must bear in mind that, using more than one' 'thread might not take full advantage of the discovery ' 'cache and pollsters cache processes; it is possible ' 'though to improve/use pollsters that synchronize ' 'themselves in the cache objects.'), ] def hash_of_set(s): return str(hash(frozenset(s))) class PollingException(agent.ConfigException): def __init__(self, message, cfg): super().__init__('Polling', message, cfg) class HeartBeatException(agent.ConfigException): def __init__(self, message, cfg): super().__init__('Polling', message, cfg) class Resources: def __init__(self, agent_manager): self.agent_manager = agent_manager self._resources = [] self._discovery = [] self.blacklist = [] def setup(self, source): self._resources = source.resources self._discovery = source.discovery def get(self, discovery_cache=None): source_discovery = (self.agent_manager.discover(self._discovery, discovery_cache) if self._discovery else []) if self._resources: static_resources_group = self.agent_manager.construct_group_id( hash_of_set(self._resources)) return [v for v in self._resources if not self.agent_manager.partition_coordinator or self.agent_manager.hashrings[ static_resources_group].belongs_to_self( str(v))] + source_discovery return source_discovery @staticmethod def key(source_name, pollster): return f'{source_name}-{pollster.name}' def iter_random(iterable): """Iter over iterable in a random fashion.""" lst = list(iterable) random.shuffle(lst) return iter(lst) class PollingTask: """Polling task for polling samples and notifying. A polling task can be invoked periodically or only once. """ def __init__(self, agent_manager): self.manager = agent_manager # elements of the Cartesian product of sources X pollsters # with a common interval self.pollster_matches = collections.defaultdict(set) # we relate the static resources and per-source discovery to # each combination of pollster and matching source def resource_factory(): return Resources(agent_manager) self.resources = collections.defaultdict(resource_factory) conf = self.manager.conf self._batch_size = conf.polling.batch_size self._telemetry_secret = conf.publisher.telemetry_secret self.ks_client = self.manager.keystone self._name_discovery = conf.polling.identity_name_discovery self._cache = cache_utils.get_client(conf) # element that provides a map between source names and source object self.sources_map = dict() def add(self, pollster, source): self.sources_map[source.name] = source self.pollster_matches[source.name].add(pollster) key = Resources.key(source.name, pollster) self.resources[key].setup(source) def poll_and_notify(self): """Polling sample and notify.""" cache = {} discovery_cache = {} poll_history = {} for source_name, pollsters in iter_random( self.pollster_matches.items()): self.execute_polling_task_processing(cache, discovery_cache, poll_history, pollsters, source_name) def execute_polling_task_processing(self, cache, discovery_cache, poll_history, pollsters, source_name): all_pollsters = list(pollsters) number_workers_for_pollsters =\ self.manager.conf.polling.threads_to_process_pollsters if number_workers_for_pollsters < 0: raise RuntimeError("The configuration " "'threads_to_process_pollsters' has a negative " "value [%s], which should not be allowed.", number_workers_for_pollsters) if number_workers_for_pollsters == 0: number_workers_for_pollsters = len(all_pollsters) if number_workers_for_pollsters < len(all_pollsters): LOG.debug("The number of pollsters in source [%s] is bigger " "than the number of worker threads to execute them. " "Therefore, one can expect the process to be longer " "than the expected.", source_name) all_pollster_scheduled = [] with futures.ThreadPoolExecutor( thread_name_prefix="Pollster-executor", max_workers=number_workers_for_pollsters) as executor: LOG.debug("Processing pollsters for [%s] with [%s] threads.", source_name, number_workers_for_pollsters) for pollster in all_pollsters: all_pollster_scheduled.append( self.register_pollster_execution( cache, discovery_cache, executor, poll_history, pollster, source_name)) for s in all_pollster_scheduled: LOG.debug(s.result()) def register_pollster_execution(self, cache, discovery_cache, executor, poll_history, pollster, source_name): LOG.debug("Registering pollster [%s] from source [%s] to be executed " "via executor [%s] with cache [%s], pollster history [%s], " "and discovery cache [%s].", pollster, source_name, executor, cache, poll_history, discovery_cache) def _internal_function(): self._internal_pollster_run(cache, discovery_cache, poll_history, pollster, source_name) return "Finished processing pollster [%s]." % pollster.name return executor.submit(_internal_function) def _internal_pollster_run(self, cache, discovery_cache, poll_history, pollster, source_name): key = Resources.key(source_name, pollster) candidate_res = list( self.resources[key].get(discovery_cache)) if not candidate_res and pollster.obj.default_discovery: LOG.debug("Executing discovery process for pollsters [%s] " "and discovery method [%s] via process [%s].", pollster.obj, pollster.obj.default_discovery, self.manager.discover) candidate_res = self.manager.discover( [pollster.obj.default_discovery], discovery_cache) # Remove duplicated resources and black resources. Using # set() requires well defined __hash__ for each resource. # Since __eq__ is defined, 'not in' is safe here. polling_resources = [] black_res = self.resources[key].blacklist history = poll_history.get(pollster.name, []) for x in candidate_res: if x not in history: history.append(x) if x not in black_res: polling_resources.append(x) poll_history[pollster.name] = history if self.manager.conf.polling.enable_prometheus_exporter: prom_exporter.purge_stale_metrics(pollster.name) # If no resources, skip for this pollster if not polling_resources: p_context = 'new' if history else '' LOG.debug("Skip pollster %(name)s, no %(p_context)s " "resources found this cycle", {'name': pollster.name, 'p_context': p_context}) return LOG.info("Polling pollster %(poll)s in the context of " "%(src)s", dict(poll=pollster.name, src=source_name)) try: source_obj = self.sources_map[source_name] coordination_group_name = source_obj.group_for_coordination LOG.debug("Checking if we need coordination for pollster " "[%s] with coordination group name [%s].", pollster, coordination_group_name) if self.manager.hashrings and self.manager.hashrings.get( coordination_group_name): LOG.debug("The pollster [%s] is configured in a " "source for polling that requires " "coordination under name [%s].", pollster, coordination_group_name) group_coordination = self.manager.hashrings[ coordination_group_name].belongs_to_self( str(pollster.name)) LOG.debug("Pollster [%s] is configured with " "coordination [%s] under name [%s].", pollster.name, group_coordination, coordination_group_name) if not group_coordination: LOG.info("The pollster [%s] should be processed " "by other node.", pollster.name) return else: LOG.debug("The pollster [%s] is not configured in a " "source for polling that requires " "coordination. The current hashrings are " "the following [%s].", pollster, self.manager.hashrings) polling_timestamp = timeutils.utcnow().isoformat() samples = pollster.obj.get_samples( manager=self.manager, cache=cache, resources=polling_resources ) sample_batch = [] self.manager.heartbeat(pollster.name, polling_timestamp) for sample in samples: # Note(yuywz): Unify the timestamp of polled samples sample.set_timestamp(polling_timestamp) if self._name_discovery and self._cache: # Try to resolve project UUIDs from cache first, # and then keystone LOG.debug("Ceilometer is configured to resolve " "project IDs to name; loading the " "project name for project ID [%s] in " "sample [%s].", sample.project_id, sample) if sample.project_id: sample.project_name = \ self._cache.resolve_uuid_from_cache( "projects", sample.project_id ) # Try to resolve user UUIDs from cache first, # and then keystone LOG.debug("Ceilometer is configured to resolve " "user IDs to name; loading the " "user name for user ID [%s] in " "sample [%s].", sample.user_id, sample) if sample.user_id: sample.user_name = \ self._cache.resolve_uuid_from_cache( "users", sample.user_id ) LOG.debug("Final sample generated after loading " "the project and user names bases on " "the IDs [%s].", sample) sample_dict = ( publisher_utils.meter_message_from_counter( sample, self._telemetry_secret )) if self._batch_size: if len(sample_batch) >= self._batch_size: self._send_notification(sample_batch) sample_batch = [] sample_batch.append(sample_dict) else: self._send_notification([sample_dict]) if sample_batch: self._send_notification(sample_batch) LOG.info("Finished polling pollster %(poll)s in the " "context of %(src)s", dict(poll=pollster.name, src=source_name)) except plugin_base.PollsterPermanentError as err: LOG.error( 'Prevent pollster %(name)s from ' 'polling %(res_list)s on source %(source)s anymore!', dict(name=pollster.name, res_list=str(err.fail_res_list), source=source_name)) self.resources[key].blacklist.extend(err.fail_res_list) except Exception as err: LOG.exception( 'Continue after error from %(name)s: %(error)s', {'name': pollster.name, 'error': err}) def _send_notification(self, samples): if self.manager.conf.polling.enable_notifications: self.manager.notifier.sample( {}, 'telemetry.polling', {'samples': samples} ) if self.manager.conf.polling.enable_prometheus_exporter: prom_exporter.collect_metrics(samples) class AgentHeartBeatManager(cotyledon.Service): def __init__(self, worker_id, conf, namespaces=None, queue=None): super().__init__(worker_id) self.conf = conf if conf.polling.heartbeat_socket_dir is None: raise HeartBeatException("path to a directory containing " "heart beat sockets is required", conf) if type(namespaces) is not list: if namespaces is None: namespaces = "" namespaces = [namespaces] self._lock = threading.Lock() self._queue = queue self._status = dict() self._sock_pth = os.path.join( conf.polling.heartbeat_socket_dir, f"ceilometer-{'-'.join(sorted(namespaces))}.socket" ) self._delete_socket() self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: self._sock.bind(self._sock_pth) self._sock.listen(1) except OSError as err: raise HeartBeatException("Failed to open socket file " f"({self._sock_pth}): {err}", conf) LOG.info("Starting heartbeat child service. Listening on %s", self._sock_pth) def _delete_socket(self): try: os.remove(self._sock_pth) except OSError: pass def terminate(self): self._tpe.shutdown(wait=False, cancel_futures=True) self._sock.close() self._delete_socket() def _update_status(self): hb = self._queue.get() with self._lock: self._status[hb['pollster']] = hb['timestamp'] LOG.debug("Updated heartbeat for %s %s", hb['pollster'], hb['timestamp']) def _send_heartbeat(self): s, addr = self._sock.accept() LOG.debug("Heartbeat status report requested at %s", self._sock_pth) with self._lock: out = '\n'.join([f"{k} {v}" for k, v in self._status.items()]) s.sendall(out.encode('utf-8')) s.close() LOG.debug("Reported heartbeat status:\n%s", out) def run(self): super().run() LOG.debug("Started heartbeat child process.") def _read_queue(): LOG.debug("Started heartbeat update thread") while True: self._update_status() def _report_status(): LOG.debug("Started heartbeat reporting thread") while True: self._send_heartbeat() with futures.ThreadPoolExecutor(max_workers=2) as executor: self._tpe = executor executor.submit(_read_queue) executor.submit(_report_status) class AgentManager(cotyledon.Service): def __init__(self, worker_id, conf, namespaces=None, queue=None): namespaces = namespaces or ['compute', 'central'] group_prefix = conf.polling.partitioning_group_prefix super().__init__(worker_id) self.conf = conf self._queue = queue if type(namespaces) is not list: namespaces = [namespaces] # we'll have default ['compute', 'central'] here if no namespaces will # be passed extensions = (self._extensions('poll', namespace, self.conf).extensions for namespace in namespaces) extensions = list(itertools.chain(*list(extensions))) # get the extensions from pollster builder extensions_fb = (self._extensions_from_builder('poll', namespace) for namespace in namespaces) extensions_fb = list(itertools.chain(*list(extensions_fb))) # NOTE(tkajinam): Remove this after 2026.1 release if extensions_fb: LOG.warning('Support for pollster build has been deprecated') # Create dynamic pollsters extensions_dynamic_pollsters = self.create_dynamic_pollsters( namespaces) extensions_dynamic_pollsters = list(extensions_dynamic_pollsters) self.extensions = ( extensions + extensions_fb + extensions_dynamic_pollsters) if not self.extensions: LOG.warning('No valid pollsters can be loaded from %s ' 'namespaces', namespaces) discoveries = (self._extensions('discover', namespace, self.conf).extensions for namespace in namespaces) self.discoveries = list(itertools.chain(*list(discoveries))) self.polling_periodics = None self.hashrings = None self.partition_coordinator = None if self.conf.coordination.backend_url: # XXX uuid4().bytes ought to work, but it requires ascii for now coordination_id = str(uuid.uuid4()).encode('ascii') self.partition_coordinator = coordination.get_coordinator( self.conf.coordination.backend_url, coordination_id) # Compose coordination group prefix. # We'll use namespaces as the basement for this partitioning. namespace_prefix = '-'.join(sorted(namespaces)) self.group_prefix = (f'{namespace_prefix}-{group_prefix}' if group_prefix else namespace_prefix) if self.conf.polling.enable_notifications: self.notifier = oslo_messaging.Notifier( messaging.get_transport(self.conf), driver=self.conf.publisher_notifier.telemetry_driver, publisher_id="ceilometer.polling") if self.conf.polling.enable_prometheus_exporter: for addr in self.conf.polling.prometheus_listen_addresses: address = netutils.parse_host_port(addr) if address[0] is None or address[1] is None: LOG.warning('Ignoring invalid address: %s', addr) certfile = self.conf.polling.prometheus_tls_certfile keyfile = self.conf.polling.prometheus_tls_keyfile if self.conf.polling.prometheus_tls_enable: if not certfile or not keyfile: raise ValueError( "Certfile and keyfile must be provided." ) else: certfile = keyfile = None prom_exporter.export( address[0], address[1], certfile, keyfile) self._keystone = None self._keystone_last_exception = None def heartbeat(self, name, timestamp): """Send heartbeat data if the agent is configured to do so.""" if self._queue is not None: try: hb = { 'timestamp': timestamp, 'pollster': name } self._queue.put_nowait(hb) LOG.debug("Polster heartbeat update: %s", name) except queue.Full: LOG.warning("Heartbeat queue full. Update failed: %s", hb) def create_dynamic_pollsters(self, namespaces): """Creates dynamic pollsters This method Creates dynamic pollsters based on configurations placed on 'pollsters_definitions_dirs' :param namespaces: The namespaces we are running on to validate if the pollster should be instantiated or not. :return: a list with the dynamic pollsters defined by the operator. """ namespaces_set = set(namespaces) pollsters_definitions_dirs = self.conf.pollsters_definitions_dirs if not pollsters_definitions_dirs: LOG.info("Variable 'pollsters_definitions_dirs' not defined.") return [] LOG.info("Looking for dynamic pollsters configurations at [%s].", pollsters_definitions_dirs) pollsters_definitions_files = [] for directory in pollsters_definitions_dirs: files = glob.glob(os.path.join(directory, "*.yaml")) if not files: LOG.info("No dynamic pollsters found in folder [%s].", directory) continue for filepath in sorted(files): if filepath is not None: pollsters_definitions_files.append(filepath) if not pollsters_definitions_files: LOG.info("No dynamic pollsters file found in dirs [%s].", pollsters_definitions_dirs) return [] pollsters_definitions = {} for pollsters_definitions_file in pollsters_definitions_files: pollsters_cfg = declarative.load_definitions( self.conf, {}, pollsters_definitions_file) LOG.info("File [%s] has [%s] dynamic pollster configurations.", pollsters_definitions_file, len(pollsters_cfg)) for pollster_cfg in pollsters_cfg: pollster_name = pollster_cfg['name'] pollster_namespaces = pollster_cfg.get( 'namespaces', ['central']) if isinstance(pollster_namespaces, list): pollster_namespaces = set(pollster_namespaces) else: pollster_namespaces = {pollster_namespaces} if not bool(namespaces_set & pollster_namespaces): LOG.info("The pollster [%s] is not configured to run in " "these namespaces %s, the configured namespaces " "for this pollster are %s. Therefore, we are " "skipping it.", pollster_name, namespaces_set, pollster_namespaces) continue if pollster_name not in pollsters_definitions: LOG.info("Loading dynamic pollster [%s] from file [%s].", pollster_name, pollsters_definitions_file) try: pollsters_definitions[pollster_name] =\ dynamic_pollster.DynamicPollster( pollster_cfg, self.conf) except Exception as e: LOG.error( "Error [%s] while loading dynamic pollster [%s].", e, pollster_name) else: LOG.info( "Dynamic pollster [%s] is already defined." "Therefore, we are skipping it.", pollster_name) LOG.debug("Total of dynamic pollsters [%s] loaded.", len(pollsters_definitions)) return pollsters_definitions.values() @staticmethod def _get_ext_mgr(namespace, *args, **kwargs): def _catch_extension_load_error(mgr, ep, exc): # Extension raising ExtensionLoadError can be ignored, # and ignore anything we can't import as a safety measure. if isinstance(exc, plugin_base.ExtensionLoadError): LOG.debug("Skip loading extension for %s: %s", ep.name, exc.msg) return show_exception = (LOG.isEnabledFor(logging.DEBUG) and isinstance(exc, ImportError)) LOG.error("Failed to import extension for %(name)r: " "%(error)s", {'name': ep.name, 'error': exc}, exc_info=show_exception) if isinstance(exc, ImportError): return raise exc return extension.ExtensionManager( namespace=namespace, invoke_on_load=True, invoke_args=args, invoke_kwds=kwargs, on_load_failure_callback=_catch_extension_load_error, ) def _extensions(self, category, agent_ns=None, *args, **kwargs): namespace = (f'ceilometer.{category}.{agent_ns}' if agent_ns else 'ceilometer.%s' % category) return self._get_ext_mgr(namespace, *args, **kwargs) def _extensions_from_builder(self, category, agent_ns=None): ns = (f'ceilometer.builder.{category}.{agent_ns}' if agent_ns else 'ceilometer.builder.%s' % category) mgr = self._get_ext_mgr(ns, self.conf) def _build(ext): return ext.plugin.get_pollsters_extensions(self.conf) # NOTE: this seems a stevedore bug. if no extensions are found, # map will raise runtimeError which is not documented. if mgr.names(): return list(itertools.chain(*mgr.map(_build))) else: return [] def join_partitioning_groups(self): groups = set() for d in self.discoveries: generated_group_id = self.construct_group_id(d.obj.group_id) LOG.debug("Adding discovery [%s] with group ID [%s] to build the " "coordination partitioning via constructed group ID " "[%s].", d.__dict__, d.obj.group_id, generated_group_id) groups.add(generated_group_id) # let each set of statically-defined resources have its own group static_resource_groups = set() for p in self.polling_manager.sources: if p.resources: generated_group_id = self.construct_group_id( hash_of_set(p.resources)) LOG.debug("Adding pollster group [%s] with resources [%s] to " "build the coordination partitioning via " "constructed group ID [%s].", p, p.resources, generated_group_id) static_resource_groups.add(generated_group_id) else: LOG.debug("Pollster group [%s] does not have resources defined" "to build the group ID for coordination.", p) groups.update(static_resource_groups) # (rafaelweingartner) here we will configure the dynamic # coordination process. It is useful to sync pollster that do not rely # on discovery process, such as the dynamic pollster on compute nodes. dynamic_pollster_groups_for_coordination = set() for p in self.polling_manager.sources: if p.group_id_coordination_expression: if p.resources: LOG.warning("The pollster group [%s] has resources to " "execute coordination. Therefore, we do not " "add it via the dynamic coordination process.", p.name) continue group_prefix = p.name generated_group_id = eval( # noqa: S307 p.group_id_coordination_expression) group_for_coordination = "{}-{}".format( group_prefix, generated_group_id) dynamic_pollster_groups_for_coordination.add( group_for_coordination) p.group_for_coordination = group_for_coordination LOG.debug("Adding pollster group [%s] with dynamic " "coordination to build the coordination " "partitioning via constructed group ID [%s].", p, dynamic_pollster_groups_for_coordination) else: LOG.debug("Pollster group [%s] does not have an expression to " "dynamically use in the coordination process.", p) groups.update(dynamic_pollster_groups_for_coordination) self.hashrings = { group: self.partition_coordinator.join_partitioned_group(group) for group in groups} LOG.debug("Hashrings [%s] created for pollsters definition.", self.hashrings) def setup_polling_tasks(self): polling_tasks = {} for source in self.polling_manager.sources: for pollster in self.extensions: if source.support_meter(pollster.name): polling_task = polling_tasks.get(source.get_interval()) if not polling_task: polling_task = PollingTask(self) polling_tasks[source.get_interval()] = polling_task polling_task.add(pollster, source) return polling_tasks def construct_group_id(self, discovery_group_id): return f'{self.group_prefix}-{discovery_group_id}' def start_polling_tasks(self): data = self.setup_polling_tasks() # Don't start useless threads if no task will run if not data or len(data) == 0: return # One thread per polling tasks is enough self.polling_periodics = periodics.PeriodicWorker.create( [], executor_factory=lambda: futures.ThreadPoolExecutor(max_workers=len(data))) for interval, polling_task in data.items(): @periodics.periodic(spacing=interval, run_immediately=True) def task(running_task): self.interval_task(running_task) self.polling_periodics.add(task, polling_task) utils.spawn_thread(self.polling_periodics.start, allow_empty=True) def run(self): super().run() self.polling_manager = PollingManager(self.conf) if self.partition_coordinator: self.partition_coordinator.start(start_heart=True) self.join_partitioning_groups() self.start_polling_tasks() def terminate(self): self.stop_pollsters_tasks() if self.partition_coordinator: self.partition_coordinator.stop() super().terminate() def interval_task(self, task): # NOTE(sileht): remove the previous keystone client # and exception to get a new one in this polling cycle. self._keystone = None self._keystone_last_exception = None # Note(leehom): if coordinator enabled call run_watchers to # update group member info before collecting if self.partition_coordinator: self.partition_coordinator.run_watchers() task.poll_and_notify() @property def keystone(self): # FIXME(sileht): This lazy loading of keystone client doesn't # look concurrently safe, we never see issue because once we have # connected to keystone everything is fine, and because all pollsters # are delayed during startup. But each polling task creates a new # client and overrides it which has been created by other polling # tasks. During this short time bad thing can occur. # # I think we must not reset keystone client before # running a polling task, but refresh it periodically instead. # NOTE(sileht): we do lazy loading of the keystone client # for multiple reasons: # * don't use it if no plugin need it # * use only one client for all plugins per polling cycle if self._keystone is None and self._keystone_last_exception is None: try: self._keystone = keystone_client.get_client(self.conf) self._keystone_last_exception = None except ka_exceptions.ClientException as e: self._keystone = None self._keystone_last_exception = e if self._keystone is not None: return self._keystone else: raise self._keystone_last_exception @staticmethod def _parse_discoverer(url): s = urlparse.urlparse(url) return (s.scheme or s.path), (s.netloc + s.path if s.scheme else None) def _discoverer(self, name): for d in self.discoveries: if d.name == name: return d.obj return None def discover(self, discovery=None, discovery_cache=None): resources = [] discovery = discovery or [] for url in discovery: if discovery_cache is not None and url in discovery_cache: resources.extend(discovery_cache[url]) continue name, param = self._parse_discoverer(url) discoverer = self._discoverer(name) if discoverer: try: if discoverer.KEYSTONE_REQUIRED_FOR_SERVICE: service_type = getattr( self.conf.service_types, discoverer.KEYSTONE_REQUIRED_FOR_SERVICE) if not keystone_client.url_for( self.keystone, service_type=service_type): LOG.warning( 'Skipping %(name)s, %(service_type)s service ' 'is not registered in keystone', {'name': name, 'service_type': service_type}) continue discovered = discoverer.discover(self, param) if self.partition_coordinator: discovered = [ v for v in discovered if self.hashrings[ self.construct_group_id(discoverer.group_id) ].belongs_to_self(str(v))] resources.extend(discovered) if discovery_cache is not None: discovery_cache[url] = discovered except ka_exceptions.ClientException as e: LOG.error('Skipping %(name)s, keystone issue: ' '%(exc)s', {'name': name, 'exc': e}) except Exception as err: LOG.exception('Unable to discover resources: %s', err) else: LOG.warning('Unknown discovery extension: %s', name) return resources def stop_pollsters_tasks(self): if self.polling_periodics: self.polling_periodics.stop() self.polling_periodics.wait() self.polling_periodics = None class PollingManager(agent.ConfigManagerBase): """Polling Manager to handle polling definition""" def __init__(self, conf): """Setup the polling according to config. The configuration is supported as follows: {"sources": [{"name": source_1, "interval": interval_time, "meters" : ["meter_1", "meter_2"], "resources": ["resource_uri1", "resource_uri2"], }, {"name": source_2, "interval": interval_time, "meters" : ["meter_3"], }, ]} } The interval determines the cadence of sample polling Valid meter format is '*', '!meter_name', or 'meter_name'. '*' is wildcard symbol means any meters; '!meter_name' means "meter_name" will be excluded; 'meter_name' means 'meter_name' will be included. Valid meters definition is all "included meter names", all "excluded meter names", wildcard and "excluded meter names", or only wildcard. The resources is list of URI indicating the resources from where the meters should be polled. It's optional and it's up to the specific pollster to decide how to use it. """ super().__init__(conf) cfg = self.load_config(conf.polling.cfg_file) self.sources = [] if 'sources' not in cfg: raise PollingException("sources required", cfg) for s in cfg.get('sources'): self.sources.append(PollingSource(s)) class PollingSource(agent.Source): """Represents a source of pollsters In effect it is a set of pollsters emitting samples for a set of matching meters. Each source encapsulates meter name matching, polling interval determination, optional resource enumeration or discovery. """ def __init__(self, cfg): try: super().__init__(cfg) except agent.SourceException as err: raise PollingException(err.msg, cfg) try: self.meters = cfg['meters'] except KeyError: raise PollingException("Missing meters value", cfg) try: self.interval = int(cfg['interval']) except ValueError: raise PollingException("Invalid interval value", cfg) except KeyError: raise PollingException("Missing interval value", cfg) if self.interval <= 0: raise PollingException("Interval value should > 0", cfg) self.resources = cfg.get('resources') or [] if not isinstance(self.resources, list): raise PollingException("Resources should be a list", cfg) self.discovery = cfg.get('discovery') or [] if not isinstance(self.discovery, list): raise PollingException("Discovery should be a list", cfg) try: self.check_source_filtering(self.meters, 'meters') except agent.SourceException as err: raise PollingException(err.msg, cfg) self.group_id_coordination_expression = cfg.get( 'group_id_coordination_expression') # This value is configured when coordination is enabled. self.group_for_coordination = None def get_interval(self): return self.interval def support_meter(self, meter_name): return self.is_supported(self.meters, meter_name) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1853566 ceilometer-27.0.0.0rc1/ceilometer/polling/plugin_base.py0000664000175000017500000001370415250267452021771 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Base class for plugins. """ import abc from stevedore import extension class ExtensionLoadError(Exception): """Error of loading pollster plugin. PollsterBase provides a hook, setup_environment, called in pollster loading to setup required HW/SW dependency. Any exception from it would be propagated as ExtensionLoadError, then skip loading this pollster. """ def __init__(self, msg=None): self.msg = msg class PollsterPermanentError(Exception): """Permanent error when polling. When unrecoverable error happened in polling, pollster can raise this exception with failed resource to prevent itself from polling any more. Resource is one of parameter resources from get_samples that cause polling error. """ def __init__(self, resources): self.fail_res_list = resources class PollsterBase(metaclass=abc.ABCMeta): """Base class for plugins that support the polling API.""" def setup_environment(self): """Setup required environment for pollster. Each subclass could overwrite it for specific usage. Any exception raised in this function would prevent pollster being loaded. """ pass def __init__(self, conf): super().__init__() self.conf = conf try: self.setup_environment() except Exception as err: raise ExtensionLoadError(err) @property @abc.abstractmethod def default_discovery(self): """Default discovery to use for this pollster. There are three ways a pollster can get a list of resources to poll, listed here in ascending order of precedence: 1. from the per-agent discovery, 2. from the per-pollster discovery (defined here) 3. from the per-pipeline configured discovery and/or per-pipeline configured static resources. If a pollster should only get resources from #1 or #3, this property should be set to None. """ @abc.abstractmethod def get_samples(self, manager, cache, resources): """Return a sequence of Counter instances from polling the resources. :param manager: The service manager class invoking the plugin. :param cache: A dictionary to allow pollsters to pass data between themselves when recomputing it would be expensive (e.g., asking another service for a list of objects). :param resources: A list of resources the pollster will get data from. It's up to the specific pollster to decide how to use it. It is usually supplied by a discovery, see ``default_discovery`` for more information. """ @classmethod def build_pollsters(cls, conf): """Return a list of tuple (name, pollster). The name is the meter name which the pollster would return, the pollster is a pollster object instance. The pollster which implements this method should be registered in the namespace of ceilometer.builder.xxx instead of ceilometer.poll.xxx. """ return [] @classmethod def get_pollsters_extensions(cls, conf): """Return a list of stevedore extensions. The returned stevedore extensions wrap the pollster object instances returned by build_pollsters. """ extensions = [] try: for name, pollster in cls.build_pollsters(conf): ext = extension.Extension(name, None, cls, pollster) extensions.append(ext) except Exception as err: raise ExtensionLoadError(err) return extensions class DiscoveryBase(metaclass=abc.ABCMeta): KEYSTONE_REQUIRED_FOR_SERVICE = None """Service type required in keystone catalog to works""" def __init__(self, conf): self.conf = conf @abc.abstractmethod def discover(self, manager, param=None): """Discover resources to monitor. The most fine-grained discovery should be preferred, so the work is the most evenly distributed among multiple agents (if they exist). For example: if the pollster can separately poll individual resources, it should have its own discovery implementation to discover those resources. If it can only poll per-tenant, then the `TenantDiscovery` should be used. If even that is not possible, use `EndpointDiscovery` (see their respective docstrings). :param manager: The service manager class invoking the plugin. :param param: an optional parameter to guide the discovery """ @property def group_id(self): """Return group id of this discovery. All running discoveries with the same group_id should return the same set of resources at a given point in time. By default, a discovery is put into a global group, meaning that all discoveries of its type running anywhere in the cloud, return the same set of resources. This property can be overridden to provide correct grouping of localized discoveries. For example, compute discovery is localized to a host, which is reflected in its group_id. A None value signifies that this discovery does not want to be part of workload partitioning at all. """ return 'global' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1853566 ceilometer-27.0.0.0rc1/ceilometer/polling/prom_exporter.py0000664000175000017500000001467715250267452022420 0ustar00zuulzuul# # Copyright 2024 Juan Larriba # Copyright 2024 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log import prometheus_client as prom LOG = log.getLogger(__name__) CEILOMETER_REGISTRY = prom.CollectorRegistry() def export(prom_iface, prom_port, tls_cert=None, tls_key=None): prom.start_http_server(port=prom_port, addr=prom_iface, registry=CEILOMETER_REGISTRY, certfile=tls_cert, keyfile=tls_key) def collect_metrics(samples): for sample in samples: name = "ceilometer_" + sample['counter_name'].replace('.', '_') if sample['counter_volume'] is None: LOG.warning("Skipping sample for metric %s with no " "counter_volume (resource: %s).", name, sample.get('resource_id')) continue labels = _gen_labels(sample) metric = CEILOMETER_REGISTRY._names_to_collectors.get(name, None) if metric is None: metric = prom.Gauge(name=name, documentation="", labelnames=labels['keys'], registry=CEILOMETER_REGISTRY) metric.labels(*labels['values']).set(sample['counter_volume']) def purge_stale_metrics(pollster): metric_cleared = False metric_name = "ceilometer_" + pollster.replace('.', '_') metric = CEILOMETER_REGISTRY._names_to_collectors.get(metric_name, None) if not metric_cleared: if metric: CEILOMETER_REGISTRY.unregister(metric) metric = None metric_cleared = True def _gen_labels(sample): labels = dict(keys=[], values=[]) cNameShards = sample['counter_name'].split(".") ctype = '' plugin = cNameShards[0] pluginVal = sample['resource_id'] if len(cNameShards) > 2: pluginVal = cNameShards[2] if len(cNameShards) > 1: ctype = cNameShards[1] else: ctype = cNameShards[0] labels['keys'].append(plugin) labels['values'].append(pluginVal) labels['keys'].append("publisher") labels['values'].append("ceilometer") labels['keys'].append("type") labels['values'].append(ctype) if sample.get('counter_name'): labels['keys'].append("counter") labels['values'].append(sample['counter_name']) if sample.get('project_id'): labels['keys'].append("project") labels['values'].append(sample['project_id']) if sample.get('project_name'): labels['keys'].append("project_name") labels['values'].append(sample['project_name']) if sample.get('user_id'): labels['keys'].append("user") labels['values'].append(sample['user_id']) if sample.get('user_name'): labels['keys'].append("user_name") labels['values'].append(sample['user_name']) if sample.get('counter_unit'): labels['keys'].append("unit") labels['values'].append(sample['counter_unit']) if sample.get('resource_id'): labels['keys'].append("resource") labels['values'].append(sample['resource_id']) if sample.get('resource_metadata'): resource_metadata = sample['resource_metadata'] if resource_metadata.get('host'): labels['keys'].append("vm_instance") labels['values'].append(resource_metadata['host']) if resource_metadata.get('display_name'): value = resource_metadata['display_name'] if resource_metadata.get('name'): value = ':'.join([value, resource_metadata['name']]) labels['keys'].append("resource_name") labels['values'].append(value) elif resource_metadata.get('name'): labels['keys'].append("resource_name") labels['values'].append(resource_metadata['name']) # NOTE(jwysogla): The prometheus_client library doesn't support # variable count of labels for the same metric. That's why the # prometheus exporter cannot support custom metric labels added # with the --property metering.= when # creating a server. This still works with publishers though. # The "server_group" label is used for autoscaling and so it's # the only one getting parsed. To always have the same number # of labels, it's added to all metrics and where there isn't a # value defined, it's substituted with "none". user_metadata = resource_metadata.get('user_metadata', {}) if user_metadata.get('server_group'): labels['keys'].append('server_group') labels['values'].append(user_metadata['server_group']) else: labels['keys'].append('server_group') labels['values'].append('none') if resource_metadata.get('alarm_state', '') != '': labels['keys'].append('state') labels['values'].append(resource_metadata['alarm_state']) # Add availability_zone for loadbalancer metrics if sample.get('counter_name', '').startswith('loadbalancer'): labels['keys'].append('availability_zone') az = resource_metadata.get('availability_zone') labels['values'].append(az if az else '') # Add binary, zone, status for volume service health metrics if sample.get('counter_name', '') == 'volume.service.health': for label in ('binary', 'zone', 'status'): value = resource_metadata.get(label, '') labels['keys'].append(label) labels['values'].append(value if value else '') if resource_metadata.get('flavor'): flavor = resource_metadata.get('flavor') if flavor.get('id'): labels['keys'].append("flavor_id") labels['values'].append(flavor['id']) if flavor.get('name'): labels['keys'].append("flavor_name") labels['values'].append(flavor['name']) return labels ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2511275 ceilometer-27.0.0.0rc1/ceilometer/privsep/0000775000175000017500000000000015250267567017155 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1863568 ceilometer-27.0.0.0rc1/ceilometer/privsep/__init__.py0000664000175000017500000000212515250267452021257 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Setup privsep decorator.""" from oslo_privsep import capabilities from oslo_privsep import priv_context sys_admin_pctxt = priv_context.PrivContext( 'ceilometer', cfg_section='ceilometer_sys_admin', pypath=__name__ + '.sys_admin_pctxt', capabilities=[capabilities.CAP_CHOWN, capabilities.CAP_DAC_OVERRIDE, capabilities.CAP_DAC_READ_SEARCH, capabilities.CAP_FOWNER, capabilities.CAP_NET_ADMIN, capabilities.CAP_SYS_ADMIN], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1863568 ceilometer-27.0.0.0rc1/ceilometer/privsep/ipmitool.py0000664000175000017500000000142715250267452021360 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Helpers for impi related routines. """ from oslo_concurrency import processutils import ceilometer.privsep @ceilometer.privsep.sys_admin_pctxt.entrypoint def ipmi(*cmd): return processutils.execute(*cmd) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2531276 ceilometer-27.0.0.0rc1/ceilometer/publisher/0000775000175000017500000000000015250267567017462 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1873567 ceilometer-27.0.0.0rc1/ceilometer/publisher/__init__.py0000664000175000017500000000273115250267452021567 0ustar00zuulzuul# # Copyright 2013 Intel Corp. # Copyright 2013-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc from oslo_log import log from oslo_utils import netutils from stevedore import driver LOG = log.getLogger(__name__) def get_publisher(conf, url, namespace): """Get publisher driver and load it. :param url: URL for the publisher :param namespace: Namespace to use to look for drivers. """ parse_result = netutils.urlsplit(url) loaded_driver = driver.DriverManager(namespace, parse_result.scheme) return loaded_driver.driver(conf, parse_result) class ConfigPublisherBase(metaclass=abc.ABCMeta): """Base class for plugins that publish data.""" def __init__(self, conf, parsed_url): self.conf = conf @abc.abstractmethod def publish_samples(self, samples): """Publish samples into final conduit.""" @abc.abstractmethod def publish_events(self, events): """Publish events into final conduit.""" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2531276 ceilometer-27.0.0.0rc1/ceilometer/publisher/data/0000775000175000017500000000000015250267567020373 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1873567 ceilometer-27.0.0.0rc1/ceilometer/publisher/data/gnocchi_resources.yaml0000664000175000017500000002331315250267452024756 0ustar00zuulzuul--- archive_policy_default: ceilometer-low archive_policies: # NOTE(sileht): We keep "mean" for now to not break all gating that # use the current tempest scenario. - name: ceilometer-low aggregation_methods: - mean back_window: 0 definition: - granularity: 5 minutes timespan: 30 days - name: ceilometer-low-rate aggregation_methods: - mean - rate:mean back_window: 0 definition: - granularity: 5 minutes timespan: 30 days - name: ceilometer-high aggregation_methods: - mean back_window: 0 definition: - granularity: 1 second timespan: 1 hour - granularity: 1 minute timespan: 1 day - granularity: 1 hour timespan: 365 days - name: ceilometer-high-rate aggregation_methods: - mean - rate:mean back_window: 0 definition: - granularity: 1 second timespan: 1 hour - granularity: 1 minute timespan: 1 day - granularity: 1 hour timespan: 365 days resources: - resource_type: identity metrics: identity.authenticate.success: identity.authenticate.pending: identity.authenticate.failure: identity.user.created: identity.user.deleted: identity.user.updated: identity.group.created: identity.group.deleted: identity.group.updated: identity.role.created: identity.role.deleted: identity.role.updated: identity.project.created: identity.project.deleted: identity.project.updated: identity.trust.created: identity.trust.deleted: identity.role_assignment.created: identity.role_assignment.deleted: - resource_type: ceph_account metrics: radosgw.objects: radosgw.objects.size: radosgw.objects.containers: radosgw.api.request: radosgw.containers.objects: radosgw.containers.objects.size: - resource_type: instance metrics: memory: memory.available: memory.usage: memory.resident: memory.swap.in: memory.swap.out: vcpus: power.state: cpu: archive_policy_name: ceilometer-low-rate disk.root.size: disk.ephemeral.size: disk.latency: disk.iops: disk.capacity: disk.allocation: disk.usage: compute.instance.booting.time: perf.cpu.cycles: perf.instructions: perf.cache.references: perf.cache.misses: attributes: host: resource_metadata.(instance_host|host) image_ref: resource_metadata.image_ref launched_at: resource_metadata.launched_at created_at: resource_metadata.created_at deleted_at: resource_metadata.deleted_at display_name: resource_metadata.display_name flavor_id: resource_metadata.(instance_flavor_id|(flavor.id)|flavor_id) flavor_name: resource_metadata.(instance_type|(flavor.name)|flavor_name) server_group: resource_metadata.user_metadata.server_group event_delete: compute.instance.delete.start event_create: compute.instance.create.end event_attributes: id: instance_id display_name: display_name host: host availability_zone: availability_zone flavor_id: instance_flavor_id flavor_name: instance_type user_id: user_id project_id: project_id event_associated_resources: instance_network_interface: '{"=": {"instance_id": "%s"}}' instance_disk: '{"=": {"instance_id": "%s"}}' - resource_type: instance_network_interface metrics: network.outgoing.packets: archive_policy_name: ceilometer-low-rate network.incoming.packets: archive_policy_name: ceilometer-low-rate network.outgoing.packets.drop: archive_policy_name: ceilometer-low-rate network.incoming.packets.drop: archive_policy_name: ceilometer-low-rate network.outgoing.packets.error: archive_policy_name: ceilometer-low-rate network.incoming.packets.error: archive_policy_name: ceilometer-low-rate network.outgoing.bytes: archive_policy_name: ceilometer-low-rate network.incoming.bytes: archive_policy_name: ceilometer-low-rate attributes: name: resource_metadata.vnic_name instance_id: resource_metadata.instance_id - resource_type: instance_disk metrics: disk.device.read.requests: archive_policy_name: ceilometer-low-rate disk.device.write.requests: archive_policy_name: ceilometer-low-rate disk.device.read.bytes: archive_policy_name: ceilometer-low-rate disk.device.write.bytes: archive_policy_name: ceilometer-low-rate disk.device.read.latency: disk.device.write.latency: disk.device.capacity: disk.device.allocation: disk.device.usage: attributes: name: resource_metadata.disk_name instance_id: resource_metadata.instance_id - resource_type: image metrics: image.size: image.download: image.serve: attributes: name: resource_metadata.name container_format: resource_metadata.container_format disk_format: resource_metadata.disk_format event_delete: image.delete event_attributes: id: resource_id - resource_type: ipmi metrics: hardware.ipmi.node.power: hardware.ipmi.node.temperature: hardware.ipmi.node.inlet_temperature: hardware.ipmi.node.outlet_temperature: hardware.ipmi.node.fan: hardware.ipmi.node.current: hardware.ipmi.node.voltage: hardware.ipmi.node.airflow: hardware.ipmi.node.cups: hardware.ipmi.node.cpu_util: hardware.ipmi.node.mem_util: hardware.ipmi.node.io_util: - resource_type: ipmi_sensor metrics: - 'hardware.ipmi.power' - 'hardware.ipmi.temperature' - 'hardware.ipmi.current' - 'hardware.ipmi.voltage' - 'hardware.ipmi.fan' attributes: node: resource_metadata.node - resource_type: network metrics: bandwidth: ip.floating: event_delete: floatingip.delete.end event_attributes: id: resource_id - resource_type: stack metrics: stack.create: stack.update: stack.delete: stack.resume: stack.suspend: - resource_type: swift_account metrics: storage.objects.incoming.bytes: storage.objects.outgoing.bytes: storage.objects.size: storage.objects: storage.objects.containers: storage.containers.objects: storage.containers.objects.size: attributes: storage_policy: resource_metadata.storage_policy - resource_type: volume metrics: volume: volume.size: snapshot.size: volume.snapshot.size: volume.backup.size: backup.size: volume.manage_existing.start: volume.manage_existing.end: volume.manage_existing_snapshot.start: volume.manage_existing_snapshot.end: attributes: display_name: resource_metadata.(display_name|name) volume_type: resource_metadata.volume_type volume_type_id: resource_metadata.volume_type_id image_id: resource_metadata.image_id instance_id: resource_metadata.instance_id event_delete: - volume.delete.end - snapshot.delete.end event_update: - volume.transfer.accept.end - snapshot.transfer.accept.end event_attributes: id: resource_id project_id: project_id - resource_type: volume_provider metrics: volume.provider.capacity.total: volume.provider.capacity.free: volume.provider.capacity.allocated: volume.provider.capacity.provisioned: volume.provider.capacity.virtual_free: - resource_type: volume_provider_pool metrics: volume.provider.pool.capacity.total: volume.provider.pool.capacity.free: volume.provider.pool.capacity.allocated: volume.provider.pool.capacity.provisioned: volume.provider.pool.capacity.virtual_free: attributes: provider: resource_metadata.provider - resource_type: nova_compute metrics: compute.node.cpu.frequency: compute.node.cpu.idle.percent: compute.node.cpu.idle.time: compute.node.cpu.iowait.percent: compute.node.cpu.iowait.time: compute.node.cpu.kernel.percent: compute.node.cpu.kernel.time: compute.node.cpu.percent: compute.node.cpu.user.percent: compute.node.cpu.user.time: attributes: host_name: resource_metadata.host - resource_type: manila_share metrics: manila.share.size: manila.share.status: attributes: name: resource_metadata.name host: resource_metadata.host status: resource_metadata.status availability_zone: resource_metadata.availability_zone protocol: resource_metadata.protocol - resource_type: loadbalancer metrics: loadbalancer.operating: loadbalancer.provisioning: attributes: name: resource_metadata.name availability_zone: resource_metadata.availability_zone vip_address: resource_metadata.vip_address provider: resource_metadata.provider event_delete: octavia.loadbalancer.delete.end event_attributes: id: resource_id - resource_type: volume_service metrics: volume.service.health: attributes: host: resource_metadata.host binary: resource_metadata.binary zone: resource_metadata.zone status: resource_metadata.status - resource_type: dns_zone metrics: dns.zone.status: dns.zone.recordsets: dns.zone.ttl: dns.zone.serial: attributes: zone_name: resource_metadata.name email: resource_metadata.email zone_type: resource_metadata.type pool_id: resource_metadata.pool_id event_delete: dns.zone.delete event_attributes: id: resource_id ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1883566 ceilometer-27.0.0.0rc1/ceilometer/publisher/file.py0000664000175000017500000001050115250267452020741 0ustar00zuulzuul# # Copyright 2013 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import logging import logging.handlers from oslo_log import log from urllib import parse as urlparse from ceilometer import publisher LOG = log.getLogger(__name__) class FilePublisher(publisher.ConfigPublisherBase): """Publisher metering data to file. The file publisher pushes metering data into a file. The file name and location should be configured in ceilometer pipeline configuration file. If a file name and location is not specified, this File Publisher will not log any meters other than log a warning in Ceilometer log file. To enable this publisher, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing pipeline:: - name: meter_file meters: - "*" publishers: - file:///var/test?max_bytes=10000000&backup_count=5&json File path is required for this publisher to work properly. If max_bytes or backup_count is missing, FileHandler will be used to save the metering data. If max_bytes and backup_count are present, RotatingFileHandler will be used to save the metering data. The json argument is used to explicitly ask ceilometer to write json into the file. """ def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) self.publisher_logger = None path = parsed_url.path if not path: LOG.error('The path for the file publisher is required') return rfh = None max_bytes = 0 backup_count = 0 self.output_json = None # Handling other configuration options in the query string if parsed_url.query: params = urlparse.parse_qs(parsed_url.query, keep_blank_values=True) if "json" in params: self.output_json = True if params.get('max_bytes') and params.get('backup_count'): try: max_bytes = int(params.get('max_bytes')[0]) backup_count = int(params.get('backup_count')[0]) except ValueError: LOG.error('max_bytes and backup_count should be ' 'numbers.') return # create rotating file handler rfh = logging.handlers.RotatingFileHandler( path, encoding='utf8', maxBytes=max_bytes, backupCount=backup_count) # (tkajinam) We need a independent logger, so explicitly create # a new instance, rather than using getLogger. self.publisher_logger = logging.Logger('publisher.file') # noqa: LOG001 self.publisher_logger.propagate = False self.publisher_logger.setLevel(logging.INFO) rfh.setLevel(logging.INFO) self.publisher_logger.addHandler(rfh) def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ if self.publisher_logger: for sample in samples: if self.output_json: self.publisher_logger.info(json.dumps(sample.as_dict())) else: self.publisher_logger.info(sample.as_dict()) def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline after transformation """ if self.publisher_logger: for event in events: if self.output_json: self.publisher_logger.info(json.dumps(event.as_dict(), default=str)) else: self.publisher_logger.info(event.as_dict()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1883566 ceilometer-27.0.0.0rc1/ceilometer/publisher/gnocchi.py0000664000175000017500000006617215250267452021453 0ustar00zuulzuul# # Copyright 2014-2015 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from collections import defaultdict import fnmatch import hashlib import itertools import json import operator import os import threading from gnocchiclient import exceptions as gnocchi_exc from keystoneauth1 import exceptions as ka_exceptions from oslo_log import log from oslo_utils import strutils from oslo_utils import timeutils from stevedore import extension import tenacity from urllib import parse as urlparse from ceilometer import cache_utils from ceilometer import declarative from ceilometer import exceptions as ceilo_exc from ceilometer import gnocchi_client from ceilometer.i18n import _ from ceilometer import keystone_client from ceilometer import publisher LOG = log.getLogger(__name__) EVENT_CREATE, EVENT_UPDATE, EVENT_DELETE = ("create", "update", "delete") class ResourcesDefinition: MANDATORY_FIELDS = {'resource_type': str, 'metrics': (dict, list)} MANDATORY_EVENT_FIELDS = {'id': str} def __init__(self, definition_cfg, archive_policy_default, archive_policy_override, plugin_manager): self.cfg = definition_cfg self._check_required_and_types(self.MANDATORY_FIELDS, self.cfg) if self.support_events(): self._check_required_and_types(self.MANDATORY_EVENT_FIELDS, self.cfg['event_attributes']) self._attributes = {} for name, attr_cfg in self.cfg.get('attributes', {}).items(): self._attributes[name] = declarative.Definition(name, attr_cfg, plugin_manager) self._event_attributes = {} for name, attr_cfg in self.cfg.get('event_attributes', {}).items(): self._event_attributes[name] = declarative.Definition( name, attr_cfg, plugin_manager) self.metrics = {} # NOTE(sileht): Convert old list to new dict format if isinstance(self.cfg['metrics'], list): values = [None] * len(self.cfg['metrics']) self.cfg['metrics'] = dict(zip(self.cfg['metrics'], values)) for m, extra in self.cfg['metrics'].items(): if not extra: extra = {} if not extra.get("archive_policy_name"): extra["archive_policy_name"] = archive_policy_default if archive_policy_override: extra["archive_policy_name"] = archive_policy_override # NOTE(sileht): For backward compat, this is after the override to # preserve the weird previous behavior. We don't really care as we # deprecate it. if 'archive_policy' in self.cfg: LOG.warning("archive_policy '%s' for a resource-type (%s) is " "deprecated, set it for each metric instead.", self.cfg["archive_policy"], self.cfg["resource_type"]) extra["archive_policy_name"] = self.cfg['archive_policy'] self.metrics[m] = extra @staticmethod def _check_required_and_types(expected, definition): for field, field_types in expected.items(): if field not in definition: raise declarative.ResourceDefinitionException( _("Required field %s not specified") % field, definition) if not isinstance(definition[field], field_types): raise declarative.ResourceDefinitionException( _("Required field %(field)s should be a %(type)s") % {'field': field, 'type': field_types}, definition) @staticmethod def _ensure_list(value): if isinstance(value, list): return value return [value] def support_events(self): for e in ["event_create", "event_delete", "event_update"]: if e in self.cfg: return True return False def event_match(self, event_type): for e in self._ensure_list(self.cfg.get('event_create', [])): if fnmatch.fnmatch(event_type, e): return EVENT_CREATE for e in self._ensure_list(self.cfg.get('event_delete', [])): if fnmatch.fnmatch(event_type, e): return EVENT_DELETE for e in self._ensure_list(self.cfg.get('event_update', [])): if fnmatch.fnmatch(event_type, e): return EVENT_UPDATE def sample_attributes(self, sample): attrs = {} sample_dict = sample.as_dict() for name, definition in self._attributes.items(): value = definition.parse(sample_dict) if value is not None: attrs[name] = value return attrs def event_attributes(self, event): attrs = {'type': self.cfg['resource_type']} traits = {trait.name: trait.value for trait in event.traits} for attr, field in self.cfg.get('event_attributes', {}).items(): value = traits.get(field) if value is not None: attrs[attr] = value return attrs class LockedDefaultDict(defaultdict): """defaultdict with lock to handle threading Dictionary only deletes if nothing is accessing dict and nothing is holding lock to be deleted. If both cases are not true, it will skip delete. """ def __init__(self, *args, **kwargs): self.lock = threading.Lock() super().__init__(*args, **kwargs) def __getitem__(self, key): with self.lock: return super().__getitem__(key) def pop(self, key, *args): with self.lock: key_lock = super().__getitem__(key) if key_lock.acquire(False): try: super().pop(key, *args) finally: key_lock.release() class GnocchiPublisher(publisher.ConfigPublisherBase): """Publisher class for recording metering data into the Gnocchi service. The publisher class records each meter into the gnocchi service configured in Ceilometer pipeline file. An example target may look like the following: gnocchi://?archive_policy=low&filter_project=gnocchi To disable filtering the Gnocchi project, use the following option: gnocchi://?enable_filter_project=false """ def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) # TODO(jd) allow to override Gnocchi endpoint via the host in the URL options = urlparse.parse_qs(parsed_url.query) self.enable_filter_project = strutils.bool_from_string( options.get('enable_filter_project', ['true'])[-1]) if self.enable_filter_project: self.filter_project = options.get('filter_project', ['service'])[-1] self.filter_domain = options.get('filter_domain', ['Default'])[-1] else: LOG.debug( "Filtering Gnocchi project is disabled, " "ignoring filter_project option") self.filter_project = None self.filter_domain = None resources_definition_file = options.get( 'resources_definition_file', ['gnocchi_resources.yaml'])[-1] archive_policy_override = options.get('archive_policy', [None])[-1] self.resources_definition, self.archive_policies_definition = ( self._load_definitions(conf, archive_policy_override, resources_definition_file)) self.metric_map = {metric: rd for rd in self.resources_definition for metric in rd.metrics} timeout = options.get('timeout', [6.05])[-1] self._ks_client = keystone_client.get_client(conf) # NOTE(cdent): The default cache backend is a real but # noop backend. We don't want to use that here because # we want to avoid the cache pathways entirely if the # cache has not been configured explicitly. self.cache = cache_utils.get_client(conf) self._gnocchi_project_id = None self._gnocchi_project_id_lock = threading.Lock() self._gnocchi_resource_lock = LockedDefaultDict(threading.Lock) try: self._gnocchi = self._get_gnocchi_client(conf, timeout) except tenacity.RetryError as e: raise e.last_attempt._exception from None self._already_logged_event_types = set() self._already_logged_metric_names = set() self._already_configured_archive_policies = False @tenacity.retry( stop=tenacity.stop_after_attempt(10), wait=tenacity.wait_fixed(5), retry=( tenacity.retry_if_exception_type(ka_exceptions.ServiceUnavailable) | tenacity.retry_if_exception_type(ka_exceptions.DiscoveryFailure) | tenacity.retry_if_exception_type(ka_exceptions.ConnectTimeout) ), reraise=False) def _get_gnocchi_client(self, conf, timeout): return gnocchi_client.get_gnocchiclient(conf, request_timeout=timeout) @staticmethod def _load_definitions(conf, archive_policy_override, resources_definition_file): plugin_manager = extension.ExtensionManager( namespace='ceilometer.event.trait_plugin') data = declarative.load_definitions( conf, {}, resources_definition_file, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'gnocchi_resources.yaml')) archive_policy_default = data.get("archive_policy_default", "ceilometer-low") resource_defs = [] for resource in data.get('resources', []): try: resource_defs.append(ResourcesDefinition( resource, archive_policy_default, archive_policy_override, plugin_manager)) except Exception: LOG.exception("Failed to load resource due to error") return resource_defs, data.get("archive_policies", []) def ensures_archives_policies(self): if not self._already_configured_archive_policies: for ap in self.archive_policies_definition: try: self._gnocchi.archive_policy.create(ap) except gnocchi_exc.ArchivePolicyAlreadyExists: # created in the meantime by another worker pass self._already_configured_archive_policies = True @property def gnocchi_project_id(self): if not self.enable_filter_project: return None if self._gnocchi_project_id is not None: return self._gnocchi_project_id with self._gnocchi_project_id_lock: if self._gnocchi_project_id is None: if not self.filter_project: LOG.debug( "Multiple executions were locked on " "self._gnocchi_project_id_lock`. This execution " "should no call `_internal_gnocchi_project_discovery` " "as `self.filter_project` is None.") return None try: domain = self._ks_client.find_domain( name=self.filter_domain) project = self._ks_client.find_project( name=self.filter_project, domain_id=domain.id) except ceilo_exc.NotFound: LOG.warning('Filtered project [%s] not found in keystone, ' 'ignoring the filter_project option', self.filter_project) self.filter_project = None return None except Exception: LOG.exception('Failed to retrieve filtered project [%s].', self.filter_project) raise self._gnocchi_project_id = project.id LOG.debug("Filtered project [%s] found with ID [%s].", self.filter_project, self._gnocchi_project_id) return self._gnocchi_project_id def _is_swift_account_sample(self, sample): try: return (self.metric_map[sample.name].cfg['resource_type'] == 'swift_account') except KeyError: return False def _is_gnocchi_activity(self, sample): return (self.filter_project and self.gnocchi_project_id and ( # avoid anything from the user used by gnocchi sample.project_id == self.gnocchi_project_id or # avoid anything in the swift account used by gnocchi (sample.resource_id == self.gnocchi_project_id and self._is_swift_account_sample(sample)) )) def _get_resource_definition_from_event(self, event_type): for rd in self.resources_definition: operation = rd.event_match(event_type) if operation: return rd, operation def filter_gnocchi_activity_openstack(self, samples): """Skip sample generated by gnocchi itself This method will filter out the samples that are generated by Gnocchi itself. """ if not self.enable_filter_project: LOG.debug("Filtering Gnocchi activities is disabled, " "samples [%s] will not be filtered.", samples) return samples filtered_samples = [] for sample in samples: if not self._is_gnocchi_activity(sample): filtered_samples.append(sample) LOG.debug("Sample [%s] is not a Gnocchi activity; therefore, " "we do not filter it out and push it to Gnocchi.", sample) else: LOG.debug("Sample [%s] is a Gnocchi activity; therefore, " "we filter it out and do not push it to Gnocchi.", sample) return filtered_samples def publish_samples(self, data): self.ensures_archives_policies() data = self.filter_gnocchi_activity_openstack(data) def value_to_sort(object_to_sort): value = object_to_sort.resource_id if not value: LOG.debug("Resource ID was not defined for sample data [%s]. " "Therefore, we will use an empty string as the " "resource ID.", object_to_sort) value = '' return value data.sort(key=value_to_sort) resource_grouped_samples = itertools.groupby( data, key=operator.attrgetter('resource_id')) gnocchi_data = {} measures = {} for resource_id, samples_of_resource in resource_grouped_samples: for sample in samples_of_resource: metric_name = sample.name LOG.debug("Processing sample [%s] for resource ID [%s].", sample, resource_id) rd = self.metric_map.get(metric_name) if rd is None: if metric_name not in self._already_logged_metric_names: LOG.warning("metric %s is not handled by Gnocchi", metric_name) self._already_logged_metric_names.add(metric_name) continue # NOTE(sileht): / is forbidden by Gnocchi resource_id = resource_id.replace('/', '_') if resource_id not in gnocchi_data: gnocchi_data[resource_id] = { 'resource_type': rd.cfg['resource_type'], 'resource': {"id": resource_id, "user_id": sample.user_id}} # NOTE(callumdickinson): project_id is added to resource_extra # because the project that owns the resource can change (e.g. # transferred volumes), and we want this reflected in Gnocchi. # user_id is not treated this way because it is not always # available on all samples for a given metric, and because in # some cases updating it would cause resource updates to occur # every time a new user performs a request against a resource. gnocchi_data[resource_id].setdefault( "resource_extra", {}).update({ "project_id": sample.project_id, **rd.sample_attributes(sample)}) measures.setdefault(resource_id, {}).setdefault( metric_name, {"measures": [], "archive_policy_name": rd.metrics[metric_name]["archive_policy_name"], "unit": sample.unit} )["measures"].append( {'timestamp': sample.timestamp, 'value': sample.volume} ) try: self.batch_measures(measures, gnocchi_data) except gnocchi_exc.ClientException as e: LOG.error("Gnocchi client exception while pushing measures [%s] " "for gnocchi data [%s]: [%s].", measures, gnocchi_data, str(e)) except Exception as e: LOG.exception( "Unexpected exception while pushing measures [%s] for " "gnocchi data [%s]: [%s].", measures, gnocchi_data, str(e)) for info in gnocchi_data.values(): resource = info["resource"] resource_type = info["resource_type"] resource_extra = info["resource_extra"] if not resource_extra: continue try: self._if_not_cached(resource_type, resource['id'], resource_extra) except gnocchi_exc.ClientException as e: LOG.error("Gnocchi client exception updating resource type " "[%s] with ID [%s] for resource data [%s]: [%s].", resource_type, resource.get('id'), resource_extra, str(e)) except Exception as e: LOG.exception( "Unexpected exception updating resource type [%s] " "with ID [%s] for resource data [%s]: [%s].", resource_type, resource.get('id'), resource_extra, str(e)) @staticmethod def _extract_resources_from_error(e, resource_infos): resource_ids = {r['original_resource_id'] for r in e.message['detail']} return [(resource_infos[rid]['resource_type'], resource_infos[rid]['resource'], resource_infos[rid]['resource_extra']) for rid in resource_ids] def batch_measures(self, measures, resource_infos): # NOTE(sileht): We don't care about error here, we want # resources metadata always been updated try: LOG.debug("Executing batch resource metrics measures for resource " "[%s] and measures [%s].", resource_infos, measures) self._gnocchi.metric.batch_resources_metrics_measures( measures, create_metrics=True) except gnocchi_exc.BadRequest as e: if not isinstance(e.message, dict): raise if e.message.get('cause') != 'Unknown resources': raise resources = self._extract_resources_from_error(e, resource_infos) for resource_type, resource, resource_extra in resources: try: resource.update(resource_extra) self._create_resource(resource_type, resource) except gnocchi_exc.ResourceAlreadyExists: # NOTE(sileht): resource created in the meantime pass except gnocchi_exc.ClientException as e: LOG.error('Error creating resource %(id)s: %(err)s', {'id': resource['id'], 'err': str(e)}) # We cannot post measures for this resource # and we can't patch it later del measures[resource['id']] del resource_infos[resource['id']] else: if self.cache and resource_extra: self.cache.set(resource['id'], self._hash_resource(resource_extra)) # NOTE(sileht): we have created missing resources/metrics, # now retry to post measures self._gnocchi.metric.batch_resources_metrics_measures( measures, create_metrics=True) LOG.debug( "%d measures posted against %d metrics through %d resources", sum(len(m["measures"]) for rid in measures for m in measures[rid].values()), sum(len(m) for m in measures.values()), len(resource_infos)) def _create_resource(self, resource_type, resource): self._gnocchi.resource.create(resource_type, resource) LOG.debug('Resource %s created', resource["id"]) def _update_resource(self, resource_type, res_id, resource_extra): self._gnocchi.resource.update(resource_type, res_id, resource_extra) LOG.debug('Resource %s updated', res_id) def _if_not_cached(self, resource_type, res_id, resource_extra): if self.cache: attribute_hash = self._hash_resource(resource_extra) if self._resource_cache_diff(res_id, attribute_hash): with self._gnocchi_resource_lock[res_id]: # NOTE(luogangyi): there is a possibility that the # resource was already built in cache by another # ceilometer-notification-agent when we get the lock here. if self._resource_cache_diff(res_id, attribute_hash): self._update_resource(resource_type, res_id, resource_extra) self.cache.set(res_id, attribute_hash) else: LOG.debug('Resource cache hit for %s', res_id) self._gnocchi_resource_lock.pop(res_id, None) else: LOG.debug('Resource cache hit for %s', res_id) else: self._update_resource(resource_type, res_id, resource_extra) @staticmethod def _hash_resource(resource): data = {k: v for k, v in resource.items() if k != 'metrics'} payload = json.dumps(data, sort_keys=True, separators=(',', ':')) return hashlib.blake2b( payload.encode(), usedforsecurity=False ).hexdigest() def _resource_cache_diff(self, key, attribute_hash): cached_hash = self.cache.get(key) return not cached_hash or cached_hash != attribute_hash def publish_events(self, events): for event in events: rd = self._get_resource_definition_from_event(event.event_type) if not rd: if event.event_type not in self._already_logged_event_types: LOG.debug("No gnocchi definition for event type: %s", event.event_type) self._already_logged_event_types.add(event.event_type) continue rd, operation = rd if operation == EVENT_DELETE: self._delete_event(rd, event) if operation == EVENT_CREATE: self._create_event(rd, event) if operation == EVENT_UPDATE: self._update_event(rd, event) def _update_event(self, rd, event): resource = rd.event_attributes(event) associated_resources = rd.cfg.get('event_associated_resources', {}) if associated_resources: to_update = itertools.chain([resource], *[ self._search_resource(resource_type, query % resource['id']) for resource_type, query in associated_resources.items() ]) else: to_update = [resource] for resource in to_update: self._set_update_attributes(resource) def _delete_event(self, rd, event): ended_at = timeutils.utcnow().isoformat() resource = rd.event_attributes(event) associated_resources = rd.cfg.get('event_associated_resources', {}) if associated_resources: to_end = itertools.chain([resource], *[ self._search_resource(resource_type, query % resource['id']) for resource_type, query in associated_resources.items() ]) else: to_end = [resource] for resource in to_end: self._set_ended_at(resource, ended_at) def _create_event(self, rd, event): resource = rd.event_attributes(event) resource_type = resource.pop('type') try: self._create_resource(resource_type, resource) except gnocchi_exc.ResourceAlreadyExists: LOG.debug("Create event received on existing resource (%s), " "ignore it.", resource['id']) except Exception: LOG.exception("Failed to create resource %s", resource) def _search_resource(self, resource_type, query): try: return self._gnocchi.resource.search( resource_type, json.loads(query)) except Exception: LOG.exception("Fail to search resource type %(resource_type)s " "with '%(query)s'", {'resource_type': resource_type, 'query': query}) return [] def _set_update_attributes(self, resource): resource_id = resource.pop('id') resource_type = resource.pop('type') try: self._if_not_cached(resource_type, resource_id, resource) except gnocchi_exc.ResourceNotFound: LOG.debug("Update event received on unexisting resource (%s), " "ignore it.", resource_id) except Exception: LOG.exception("Fail to update the resource %s", resource) def _set_ended_at(self, resource, ended_at): try: self._gnocchi.resource.update(resource['type'], resource['id'], {'ended_at': ended_at}) except gnocchi_exc.ResourceNotFound: LOG.debug("Delete event received on unexisting resource (%s), " "ignore it.", resource['id']) except Exception: LOG.exception("Fail to update the resource %s", resource) LOG.debug('Resource %(resource_id)s ended at %(ended_at)s', {'resource_id': resource["id"], 'ended_at': ended_at}) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1893568 ceilometer-27.0.0.0rc1/ceilometer/publisher/http.py0000664000175000017500000001731315250267452021011 0ustar00zuulzuul# # Copyright 2016 IBM # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from oslo_log import log from oslo_utils import strutils import requests from requests import adapters from urllib import parse as urlparse from ceilometer import publisher LOG = log.getLogger(__name__) class HttpPublisher(publisher.ConfigPublisherBase): """Publish metering data to a http endpoint This publisher pushes metering data to a specified http endpoint. The endpoint should be configured in ceilometer pipeline configuration file. If the `timeout` and/or `max_retries` are not specified, the default `timeout` and `max_retries` will be set to 5 and 2 respectively. Additional parameters are: - ssl certificate verification can be disabled by setting `verify_ssl` to False - batching can be configured by `batch` - Basic authentication can be configured using the URL authentication scheme: http://username:password@example.com - For certificate authentication, `clientcert` and `clientkey` are the paths to the certificate and key files respectively. `clientkey` is only required if the clientcert file doesn't already contain the key. All of the parameters mentioned above get removed during processing, with the remaining portion of the URL being used as the actual endpoint. e.g. https://username:password@example.com/path?verify_ssl=False&q=foo will result in a call to https://example.com/path?q=foo To use this publisher for samples, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing pipeline:: - name: meter_file meters: - "*" publishers: - http://host:80/path?timeout=1&max_retries=2&batch=False In the event_pipeline.yaml file, you can use the publisher in one of the sinks like the following: - name: event_sink publishers: - http://host:80/path?timeout=1&max_retries=2 """ HEADERS = {'Content-type': 'application/json'} def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) if not parsed_url.hostname: raise ValueError('The hostname of an endpoint for ' 'HttpPublisher is required') # non-numeric port from the url string will cause a ValueError # exception when the port is read. Do a read to make sure the port # is valid, if not, ValueError will be thrown. parsed_url.port # Handling other configuration options in the query string params = urlparse.parse_qs(parsed_url.query) self.timeout = self._get_param(params, 'timeout', 5, int) self.max_retries = self._get_param(params, 'max_retries', 2, int) self.poster = ( self._batch_post if strutils.bool_from_string(self._get_param( params, 'batch', True)) else self._individual_post) verify_ssl = self._get_param(params, 'verify_ssl', True) try: self.verify_ssl = strutils.bool_from_string(verify_ssl, strict=True) except ValueError: self.verify_ssl = (verify_ssl or True) username = parsed_url.username password = parsed_url.password if username: self.client_auth = (username, password) netloc = parsed_url.netloc.replace(username + ':' + password + '@', '') else: self.client_auth = None netloc = parsed_url.netloc clientcert = self._get_param(params, 'clientcert', None) clientkey = self._get_param(params, 'clientkey', None) if clientcert: if clientkey: self.client_cert = (clientcert, clientkey) else: self.client_cert = clientcert else: self.client_cert = None self.raw_only = strutils.bool_from_string( self._get_param(params, 'raw_only', False)) kwargs = {'max_retries': self.max_retries, 'pool_connections': conf.max_parallel_requests, 'pool_maxsize': conf.max_parallel_requests} self.session = requests.Session() if parsed_url.scheme in ["http", "https"]: scheme = parsed_url.scheme else: ssl = self._get_param(params, 'ssl', False) try: ssl = strutils.bool_from_string(ssl, strict=True) except ValueError: ssl = (ssl or False) scheme = "https" if ssl else "http" # authentication & config params have been removed, so use URL with # updated query string self.target = urlparse.urlunsplit([ scheme, netloc, parsed_url.path, urlparse.urlencode(params, doseq=True), parsed_url.fragment]) self.session.mount(self.target, adapters.HTTPAdapter(**kwargs)) LOG.debug('HttpPublisher for endpoint %s is initialized!', self.target) @staticmethod def _get_param(params, name, default_value, cast=None): try: return cast(params.pop(name)[-1]) if cast else params.pop(name)[-1] except (ValueError, TypeError, KeyError): LOG.debug('Default value %(value)s is used for %(name)s', {'value': default_value, 'name': name}) return default_value def _individual_post(self, data): for d in data: self._do_post(json.dumps(d)) def _batch_post(self, data): if not data: LOG.debug('Data set is empty!') return self._do_post(json.dumps(data)) def _do_post(self, data, sub_path=None): target = self.target + sub_path if sub_path else self.target LOG.trace('Message: %s', data) try: res = self.session.post(target, data=data, headers=self.HEADERS, timeout=self.timeout, auth=self.client_auth, cert=self.client_cert, verify=self.verify_ssl) res.raise_for_status() LOG.debug('Message posting to %s: status code %d.', target, res.status_code) except requests.exceptions.HTTPError: LOG.exception('Status Code: %(code)s. ' 'Failed to dispatch message: %(data)s', {'code': res.status_code, 'data': data}) def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ self.poster([sample.as_dict() for sample in samples]) def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline after transformation """ if self.raw_only: data = [evt.as_dict()['raw']['payload'] for evt in events if evt.as_dict().get('raw', {}).get('payload')] else: data = [event.serialize() for event in events] self.poster(data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1893568 ceilometer-27.0.0.0rc1/ceilometer/publisher/messaging.py0000664000175000017500000002220715250267452022005 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Publish a sample using the preferred RPC mechanism. """ import abc import itertools import operator import threading from oslo_config import cfg from oslo_log import log import oslo_messaging from oslo_utils import excutils from urllib import parse as urlparse from ceilometer import messaging from ceilometer import publisher from ceilometer.publisher import utils LOG = log.getLogger(__name__) NOTIFIER_OPTS = [ cfg.StrOpt('metering_topic', default='metering', help='The topic that ceilometer uses for metering ' 'notifications.', deprecated_for_removal=True, ), cfg.StrOpt('event_topic', default='event', help='The topic that ceilometer uses for event ' 'notifications.', deprecated_for_removal=True, ), cfg.StrOpt('telemetry_driver', default='messagingv2', help='The driver that ceilometer uses for metering ' 'notifications.', deprecated_name='metering_driver', ) ] class DeliveryFailure(Exception): def __init__(self, message=None, cause=None): super().__init__(message) self.cause = cause def raise_delivery_failure(exc): excutils.raise_with_cause(DeliveryFailure, str(exc), cause=exc) class MessagingPublisher(publisher.ConfigPublisherBase, metaclass=abc.ABCMeta): def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) options = urlparse.parse_qs(parsed_url.query) # the value of options is a list of url param values # only take care of the latest one if the option # is provided more than once self.per_meter_topic = bool(int( options.get('per_meter_topic', [0])[-1])) self.policy = options.get('policy', ['default'])[-1] self.max_queue_length = int(options.get( 'max_queue_length', [1024])[-1]) self.max_retry = 0 self.queue_lock = threading.Lock() self.local_queue = [] if self.policy in ['default', 'queue', 'drop']: LOG.info('Publishing policy set to %s', self.policy) else: LOG.warning('Publishing policy is unknown (%s) force to default', self.policy) self.policy = 'default' self.retry = 1 if self.policy in ['queue', 'drop'] else None def publish_samples(self, samples): """Publish samples on RPC. :param samples: Samples from pipeline. """ meters = [ utils.meter_message_from_counter( sample, self.conf.publisher.telemetry_secret) for sample in samples ] topic = self.conf.publisher_notifier.metering_topic with self.queue_lock: self.local_queue.append((topic, meters)) if self.per_meter_topic: queue_per_meter_topic = [] for meter_name, meter_list in itertools.groupby( sorted(meters, key=operator.itemgetter('counter_name')), operator.itemgetter('counter_name')): meter_list = list(meter_list) topic_name = topic + '.' + meter_name LOG.debug('Publishing %(m)d samples on %(n)s', {'m': len(meter_list), 'n': topic_name}) queue_per_meter_topic.append((topic_name, meter_list)) with self.queue_lock: self.local_queue.extend(queue_per_meter_topic) self.flush() def flush(self): with self.queue_lock: queue = self.local_queue self.local_queue = [] queue = self._process_queue(queue, self.policy) with self.queue_lock: self.local_queue = (queue + self.local_queue) if self.policy == 'queue': self._check_queue_length() def _check_queue_length(self): queue_length = len(self.local_queue) if queue_length > self.max_queue_length > 0: count = queue_length - self.max_queue_length self.local_queue = self.local_queue[count:] LOG.warning("Publisher max local_queue length is exceeded, " "dropping %d oldest samples", count) def _process_queue(self, queue, policy): current_retry = 0 while queue: topic, data = queue[0] try: self._send(topic, data) except DeliveryFailure: data = sum([len(m) for __, m in queue]) if policy == 'queue': LOG.warning("Failed to publish %d datapoints, queue them", data) return queue elif policy == 'drop': LOG.warning("Failed to publish %d datapoints, " "dropping them", data) return [] current_retry += 1 if current_retry >= self.max_retry: LOG.exception("Failed to retry to send sample data " "with max_retry times") raise else: queue.pop(0) return [] def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline. """ ev_list = [utils.message_from_event( event, self.conf.publisher.telemetry_secret) for event in events] topic = self.conf.publisher_notifier.event_topic with self.queue_lock: self.local_queue.append((topic, ev_list)) self.flush() @abc.abstractmethod def _send(self, topic, meters): """Send the meters to the messaging topic.""" class NotifierPublisher(MessagingPublisher): """Publish metering data from notifier publisher. The ip address and port number of notifier can be configured in ceilometer pipeline configuration file. User can customize the transport driver such as rabbit, kafka and so on. The Notifier uses `sample` method as default method to send notifications. This publisher has transmit options such as queue, drop, and retry. These options are specified using policy field of URL parameter. When queue option could be selected, local queue length can be determined using max_queue_length field as well. When the transfer fails with retry option, try to resend the data as many times as specified in max_retry field. If max_retry is not specified, by default the number of retry is 100. To enable this publisher, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing pipeline:: meter: - name: meter_notifier meters: - "*" sinks: - notifier_sink sinks: - name: notifier_sink publishers: - notifier://[notifier_ip]:[notifier_port]?topic=[topic]& driver=driver&max_retry=100 """ def __init__(self, conf, parsed_url, default_topic): super().__init__(conf, parsed_url) options = urlparse.parse_qs(parsed_url.query) topics = options.pop('topic', [default_topic]) driver = options.pop('driver', ['rabbit'])[0] self.max_retry = int(options.get('max_retry', [100])[-1]) url = None if parsed_url.netloc != '': url = urlparse.urlunsplit([driver, parsed_url.netloc, parsed_url.path, urlparse.urlencode(options, True), parsed_url.fragment]) self.notifier = oslo_messaging.Notifier( messaging.get_transport(self.conf, url), driver=self.conf.publisher_notifier.telemetry_driver, publisher_id='telemetry.publisher.%s' % self.conf.host, topics=topics, retry=self.retry ) def _send(self, event_type, data): try: self.notifier.sample({}, event_type=event_type, payload=data) except oslo_messaging.MessageDeliveryFailure as e: raise_delivery_failure(e) class SampleNotifierPublisher(NotifierPublisher): def __init__(self, conf, parsed_url): super().__init__( conf, parsed_url, conf.publisher_notifier.metering_topic) class EventNotifierPublisher(NotifierPublisher): def __init__(self, conf, parsed_url): super().__init__( conf, parsed_url, conf.publisher_notifier.event_topic) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1893568 ceilometer-27.0.0.0rc1/ceilometer/publisher/opentelemetry_http.py0000664000175000017500000001116415250267452023763 0ustar00zuulzuul# # Copyright 2024 cmss, inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import time from oslo_log import log from oslo_utils import timeutils from ceilometer.publisher import http from ceilometer import sample as smp LOG = log.getLogger(__name__) class OpentelemetryHttpPublisher(http.HttpPublisher): """Publish metering data to Opentelemetry Collector endpoint This dispatcher inherits from all options of the http dispatcher. To use this publisher for samples, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing pipeline:: - name: meter_file meters: - "*" publishers: - opentelemetryhttp://opentelemetry-http-ip:4318/v1/metrics """ HEADERS = {'Content-type': 'application/json'} @staticmethod def get_attribute_model(key, value): return { "key": key, "value": { "string_value": value } } def get_attributes_model(self, sample): attributes = [] resource_id_attr = self.get_attribute_model("resource_id", sample.resource_id) user_id_attr = self.get_attribute_model("user_id", sample.user_id) project_id_attr = self.get_attribute_model("project_id", sample.project_id) attributes.append(resource_id_attr) attributes.append(user_id_attr) attributes.append(project_id_attr) return attributes @staticmethod def get_metrics_model(sample, data_points): name = sample.name.replace(".", "_") desc = str(sample.name) + " unit:" + sample.unit unit = sample.unit metrics = dict() metric_type = None if sample.type == smp.TYPE_CUMULATIVE: metric_type = "counter" else: metric_type = "gauge" metrics.update({ "name": name, "description": desc, "unit": unit, metric_type: {"data_points": data_points} }) return metrics @staticmethod def get_data_points_model(timestamp, attributes, volume): data_points = dict() struct_time = timeutils.parse_isotime(timestamp).timetuple() unix_time = int(time.mktime(struct_time)) data_points.update({ 'attributes': attributes, "start_time_unix_nano": unix_time, "time_unix_nano": unix_time, "as_double": volume, "flags": 0 }) return data_points def get_data_model(self, sample, data_points): metrics = [self.get_metrics_model(sample, data_points)] data = { "resource_metrics": [{ "scope_metrics": [{ "scope": { "name": "ceilometer", "version": "v1" }, "metrics": metrics }] }] } return data def get_data_points(self, sample): # attributes contain basic metadata attributes = self.get_attributes_model(sample) try: return [self.get_data_points_model( sample.timestamp, attributes, sample.volume)] except Exception as e: LOG.warning("Get data point error, %s", e) return [] def get_opentelemetry_model(self, sample): data_points = self.get_data_points(sample) if data_points: data = self.get_data_model(sample, data_points) return data else: return None def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ if not samples: LOG.warning('Data samples is empty!') return for s in samples: data = self.get_opentelemetry_model(s) if data: self._do_post(json.dumps(data)) @staticmethod def publish_events(events): raise NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1903567 ceilometer-27.0.0.0rc1/ceilometer/publisher/prometheus.py0000664000175000017500000001023015250267452022214 0ustar00zuulzuul# # Copyright 2016 IBM # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from urllib import parse as urlparse from ceilometer.publisher import http from ceilometer import sample LOG = log.getLogger(__name__) class PrometheusPublisher(http.HttpPublisher): """Publish metering data to Prometheus Pushgateway endpoint This dispatcher inherits from all options of the http dispatcher. To use this publisher for samples, add the following section to the /etc/ceilometer/pipeline.yaml file or simply add it to an existing pipeline:: - name: meter_file meters: - "*" publishers: - prometheus://mypushgateway/metrics/job/ceilometer Notes about grouping labels: - Prometheus Pushgateway groups metrics by a set of grouping labels that appear in the URL path after the job segment. - Without grouping labels, pushing a metric family again will replace all time series of that metric name within the group. To avoid overrides when multiple pushes contain the same metric name with different label sets, this publisher, if no grouping labels are configured in the URL, will push per resource (using `resource_id` as grouping label in the path). - If grouping labels are already present in the path, they are preserved, and a single push is performed, leaving grouping control to the operator. """ HEADERS = {'Content-type': 'plain/text'} def __init__(self, conf, parsed_url): path = parsed_url.path or '' self._has_grouping = False if path.startswith('/metrics/job/'): segments = [seg for seg in path.split('/') if seg] # segments: ['metrics', 'job', '', ] self._has_grouping = len(segments) > 3 super().__init__(conf, parsed_url) @staticmethod def _sample_to_text(s, doc_done): # NOTE(sileht): delta can't be converted into prometheus data # format so don't set the metric type for it metric_type = None if s.type == sample.TYPE_CUMULATIVE: metric_type = "counter" elif s.type == sample.TYPE_GAUGE: metric_type = "gauge" curated_sname = s.name.replace(".", "_").replace("-", "_") lines = [] if metric_type and curated_sname not in doc_done: lines.append(f"# TYPE {curated_sname} {metric_type}\n") doc_done.add(curated_sname) lines.append('%s{resource_id="%s", user_id="%s", project_id="%s"}' ' %s\n' % (curated_sname, s.resource_id, s.user_id, s.project_id, s.volume)) return "".join(lines) def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ if not samples: return if self._has_grouping: doc_done = set() data = "".join(self._sample_to_text(s, doc_done) for s in samples) self._do_post(data) return payloads = {} doc_done_by_res = {} for s in samples: rid = s.resource_id if rid not in payloads: payloads[rid] = [] doc_done_by_res[rid] = set() payloads[rid].append(self._sample_to_text(s, doc_done_by_res[rid])) for rid, chunks in payloads.items(): rid_q = urlparse.quote(str(rid), safe='') self._do_post("".join(chunks), sub_path='/resource_id/' + rid_q) @staticmethod def publish_events(events): raise NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1913567 ceilometer-27.0.0.0rc1/ceilometer/publisher/tcp.py0000664000175000017500000000756015250267452020623 0ustar00zuulzuul# # Copyright 2022 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Publish a sample using a TCP mechanism """ import socket import msgpack from oslo_log import log from oslo_utils import netutils import ceilometer from ceilometer import publisher from ceilometer.publisher import utils LOG = log.getLogger(__name__) class TCPPublisher(publisher.ConfigPublisherBase): def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) self.inet_addr = netutils.parse_host_port( parsed_url.netloc, default_port=4952) self.socket = None self.connect_socket() def connect_socket(self): try: self.socket = socket.create_connection(self.inet_addr) return True except socket.gaierror: LOG.error("Unable to resolv the remote %(host)s", {'host': self.inet_addr[0], 'port': self.inet_addr[1]}) except TimeoutError: LOG.error("Unable to connect to the remote endpoint " "%(host)s:%(port)d. The connection timed out.", {'host': self.inet_addr[0], 'port': self.inet_addr[1]}) except ConnectionRefusedError: LOG.error("Unable to connect to the remote endpoint " "%(host)s:%(port)d. Connection refused.", {'host': self.inet_addr[0], 'port': self.inet_addr[1]}) return False def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ for sample in samples: msg = utils.meter_message_from_counter( sample, self.conf.publisher.telemetry_secret, self.conf.host) LOG.debug("Publishing sample %(msg)s over TCP to " "%(host)s:%(port)d", {'msg': msg, 'host': self.inet_addr[0], 'port': self.inet_addr[1]}) encoded_msg = msgpack.dumps(msg, use_bin_type=True) msg_len = len(encoded_msg).to_bytes(8, 'little') if self.socket: try: self.socket.send(msg_len + encoded_msg) continue except OSError: LOG.warning("Unable to send sample over TCP, trying " "to reconnect and resend the message") if self.connect_socket(): try: self.socket.send(msg_len + encoded_msg) continue except OSError: pass LOG.error("Unable to reconnect and resend sample over TCP") # NOTE (jokke): We do not handle exceptions in the calling code # so raising the exception from here needs quite a bit more work. # Same time we don't want to spam the retry messages as it's # unlikely to change between iterations on this loop. 'break' # rather than 'return' even the end result is the same feels # more appropriate for now. break def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline after transformation """ raise ceilometer.NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1913567 ceilometer-27.0.0.0rc1/ceilometer/publisher/test.py0000664000175000017500000000253715250267452021013 0ustar00zuulzuul# # Copyright 2013 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Publish a sample in memory, useful for testing """ from ceilometer import publisher class TestPublisher(publisher.ConfigPublisherBase): """Publisher used in unit testing.""" def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) self.samples = [] self.events = [] self.calls = 0 def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ self.samples.extend(samples) self.calls += 1 def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline after transformation """ self.events.extend(events) self.calls += 1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1913567 ceilometer-27.0.0.0rc1/ceilometer/publisher/udp.py0000664000175000017500000000553015250267452020620 0ustar00zuulzuul# # Copyright 2013 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Publish a sample using an UDP mechanism """ import socket import msgpack from oslo_log import log from oslo_utils import netutils import ceilometer from ceilometer import publisher from ceilometer.publisher import utils LOG = log.getLogger(__name__) class UDPPublisher(publisher.ConfigPublisherBase): def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) self.host, self.port = netutils.parse_host_port( parsed_url.netloc, default_port=4952) addrinfo = None try: addrinfo = socket.getaddrinfo(self.host, None, socket.AF_INET6, socket.SOCK_DGRAM)[0] except socket.gaierror: try: addrinfo = socket.getaddrinfo(self.host, None, socket.AF_INET, socket.SOCK_DGRAM)[0] except socket.gaierror: pass if addrinfo: addr_family = addrinfo[0] else: LOG.warning( "Cannot resolve host %s, creating AF_INET socket...", self.host) addr_family = socket.AF_INET self.socket = socket.socket(addr_family, socket.SOCK_DGRAM) def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ for sample in samples: msg = utils.meter_message_from_counter( sample, self.conf.publisher.telemetry_secret) host = self.host port = self.port LOG.debug("Publishing sample %(msg)s over UDP to " "%(host)s:%(port)d", {'msg': msg, 'host': host, 'port': port}) try: self.socket.sendto(msgpack.dumps(msg, use_bin_type=True), (self.host, self.port)) except Exception as e: LOG.warning("Unable to send sample over UDP") LOG.exception(e) def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline after transformation """ raise ceilometer.NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1913567 ceilometer-27.0.0.0rc1/ceilometer/publisher/utils.py0000664000175000017500000001152215250267452021166 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Utils for publishers """ import hashlib import hmac from oslo_config import cfg OPTS = [ cfg.StrOpt('telemetry_secret', secret=True, default='change this for valid signing', help='Secret value for signing messages. Set value empty if ' 'signing is not required to avoid computational overhead.', ), ] def decode_unicode(input): """Decode the unicode of the message, and encode it into utf-8.""" if isinstance(input, dict): temp = {} # If the input data is a dict, create an equivalent dict with a # predictable insertion order to avoid inconsistencies in the # message signature computation for equivalent payloads modulo # ordering for key, value in sorted(input.items()): temp[decode_unicode(key)] = decode_unicode(value) return temp elif isinstance(input, (tuple, list)): # When doing a pair of JSON encode/decode operations to the tuple, # the tuple would become list. So we have to generate the value as # list here. return [decode_unicode(element) for element in input] elif isinstance(input, str): return input.encode('utf-8') elif isinstance(input, bytes): return input.decode('utf-8') else: return input def recursive_keypairs(d, separator=':'): """Generator that produces sequence of keypairs for nested dictionaries.""" for name, value in sorted(d.items()): if isinstance(value, dict): for subname, subvalue in recursive_keypairs(value, separator): yield (f'{name}{separator}{subname}', subvalue) elif isinstance(value, (tuple, list)): yield name, decode_unicode(value) else: yield name, value def compute_signature(message, secret): """Return the signature for a message dictionary.""" if not secret: return '' if isinstance(secret, str): secret = secret.encode('utf-8') digest_maker = hmac.new(secret, b'', hashlib.sha256) for name, value in recursive_keypairs(message): if name == 'message_signature': # Skip any existing signature value, which would not have # been part of the original message. continue digest_maker.update(str(name).encode('utf-8')) digest_maker.update(str(value).encode('utf-8')) return digest_maker.hexdigest() def verify_signature(message, secret): """Check the signature in the message. Message is verified against the value computed from the rest of the contents. """ if not secret: return True old_sig = message.get('message_signature', '') new_sig = compute_signature(message, secret) if isinstance(old_sig, str): try: old_sig = old_sig.encode('ascii') except UnicodeDecodeError: return False new_sig = new_sig.encode('ascii') return hmac.compare_digest(new_sig, old_sig) def meter_message_from_counter(sample, secret, publisher_id=None): """Make a metering message ready to be published or stored. Returns a dictionary containing a metering message for a notification message and a Sample instance. """ msg = {'source': sample.source, 'counter_name': sample.name, 'counter_type': sample.type, 'counter_unit': sample.unit, 'counter_volume': sample.volume, 'user_id': sample.user_id, 'user_name': sample.user_name, 'project_id': sample.project_id, 'project_name': sample.project_name, 'resource_id': sample.resource_id, 'timestamp': sample.timestamp, 'resource_metadata': sample.resource_metadata, 'message_id': sample.id, 'monotonic_time': sample.monotonic_time, } if publisher_id is not None: msg['publisher_id'] = publisher_id msg['message_signature'] = compute_signature(msg, secret) return msg def message_from_event(event, secret): """Make an event message ready to be published or stored. Returns a serialized model of Event containing an event message """ msg = event.serialize() msg['message_signature'] = compute_signature(msg, secret) return msg ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1923568 ceilometer-27.0.0.0rc1/ceilometer/publisher/zaqar.py0000664000175000017500000000522415250267452021146 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from urllib import parse as urlparse from ceilometer import keystone_client from ceilometer import publisher from zaqarclient.queues.v2 import client as zaqarclient DEFAULT_TTL = 3600 class ZaqarPublisher(publisher.ConfigPublisherBase): """Publish metering data to a Zaqar queue. The target queue name must be configured in the ceilometer pipeline configuration file. The TTL can also optionally be specified as a query argument:: meter: - name: meter_zaqar meters: - "*" sinks: - zaqar_sink sinks: - name: zaqar_sink publishers: - zaqar://?queue=meter_queue&ttl=1200 The credentials to access Zaqar must be set in the [zaqar] section in the configuration. """ def __init__(self, conf, parsed_url): super().__init__(conf, parsed_url) options = urlparse.parse_qs(parsed_url.query) self.queue_name = options.get('queue', [None])[0] if not self.queue_name: raise ValueError('Must specify a queue in the zaqar publisher') self.ttl = int(options.pop('ttl', [DEFAULT_TTL])[0]) self._client = None @property def client(self): if self._client is None: session = keystone_client.get_session( self.conf, group=self.conf.zaqar.auth_section) self._client = zaqarclient.Client(session=session) return self._client def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline. """ queue = self.client.queue(self.queue_name) messages = [{'body': sample.as_dict(), 'ttl': self.ttl} for sample in samples] queue.post(messages) def publish_events(self, events): """Send an event message for publishing :param events: events from pipeline. """ queue = self.client.queue(self.queue_name) messages = [{'body': event.serialize(), 'ttl': self.ttl} for event in events] queue.post(messages) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1933568 ceilometer-27.0.0.0rc1/ceilometer/sample.py0000664000175000017500000001410615250267452017313 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # Copyright 2013 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Sample class for holding data about a metering event. A Sample doesn't really do anything, but we need a way to ensure that all of the appropriate fields have been filled in by the plugins that create them. """ import copy import uuid from oslo_config import cfg from oslo_utils import timeutils OPTS = [ cfg.StrOpt('sample_source', default='openstack', help='Source for samples emitted on this instance.'), cfg.ListOpt('reserved_metadata_namespace', default=['metering.'], help='List of metadata prefixes reserved for metering use.'), cfg.IntOpt('reserved_metadata_length', default=256, help='Limit on length of reserved metadata values.'), cfg.ListOpt('reserved_metadata_keys', default=[], help='List of metadata keys reserved for metering use. And ' 'these keys are additional to the ones included in the ' 'namespace.'), ] def add_reserved_user_metadata(conf, src_metadata, dest_metadata): limit = conf.reserved_metadata_length user_metadata = {} for prefix in conf.reserved_metadata_namespace: md = { k[len(prefix):].replace('.', '_'): v[:limit] if isinstance(v, str) else v for k, v in src_metadata.items() if (k.startswith(prefix) and k[len(prefix):].replace('.', '_') not in dest_metadata) } user_metadata.update(md) for metadata_key in conf.reserved_metadata_keys: md = { k.replace('.', '_'): v[:limit] if isinstance(v, str) else v for k, v in src_metadata.items() if (k == metadata_key and k.replace('.', '_') not in dest_metadata) } user_metadata.update(md) if user_metadata: dest_metadata['user_metadata'] = user_metadata return dest_metadata # Fields explanation: # # Source: the source of this sample # Name: the name of the meter, must be unique # Type: the type of the meter, must be either: # - cumulative: the value is incremented and never reset to 0 # - delta: the value is reset to 0 each time it is sent # - gauge: the value is an absolute value and is not a counter # Unit: the unit of the meter # Volume: the sample value # User ID: the user ID # Project ID: the project ID # Resource ID: the resource ID # Timestamp: when the sample has been read # Resource metadata: various metadata # id: an uuid of a sample, can be taken from API when post sample via API class Sample: SOURCE_DEFAULT = "openstack" def __init__(self, name, type, unit, volume, user_id, project_id, resource_id, timestamp=None, resource_metadata=None, source=None, id=None, monotonic_time=None, user_name=None, project_name=None): if type not in TYPES: raise ValueError('Unsupported type: %s') self.name = name self.type = type self.unit = unit self.volume = volume self.user_id = user_id self.user_name = user_name self.project_id = project_id self.project_name = project_name self.resource_id = resource_id self.timestamp = timestamp self.resource_metadata = resource_metadata or {} self.source = source or self.SOURCE_DEFAULT self.id = id or str(uuid.uuid1()) self.monotonic_time = monotonic_time def as_dict(self): return copy.copy(self.__dict__) def __repr__(self): return ''.format( self.name, self.volume, self.resource_id, self.timestamp) @classmethod def from_notification(cls, name, type, volume, unit, user_id, project_id, resource_id, message, timestamp=None, metadata=None, source=None, user_name=None, project_name=None): if not metadata: metadata = (copy.copy(message['payload']) if isinstance(message['payload'], dict) else {}) metadata['event_type'] = message['event_type'] metadata['host'] = message['publisher_id'] ts = timestamp if timestamp else message['metadata']['timestamp'] ts = timeutils.parse_isotime(ts).isoformat() # add UTC if necessary return cls(name=name, type=type, volume=volume, unit=unit, user_id=user_id, project_id=project_id, resource_id=resource_id, timestamp=ts, resource_metadata=metadata, source=source, user_name=user_name, project_name=project_name) def set_timestamp(self, timestamp): self.timestamp = timestamp def get_iso_timestamp(self): return timeutils.parse_isotime(self.timestamp) def __eq__(self, other): if isinstance(other, self.__class__): return self.__dict__ == other.__dict__ return False def __ne__(self, other): return not self.__eq__(other) def setup(conf): # NOTE(sileht): Instead of passing the cfg.CONF everywhere in ceilometer # prepare_service will override this default Sample.SOURCE_DEFAULT = conf.sample_source TYPE_GAUGE = 'gauge' TYPE_DELTA = 'delta' TYPE_CUMULATIVE = 'cumulative' TYPES = (TYPE_GAUGE, TYPE_DELTA, TYPE_CUMULATIVE) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1933568 ceilometer-27.0.0.0rc1/ceilometer/service.py0000664000175000017500000000364315250267452017476 0ustar00zuulzuul# Copyright 2012-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys from oslo_config import cfg import oslo_i18n from oslo_log import log from oslo_reports import guru_meditation_report as gmr from oslo_reports import opts as gmr_opts from ceilometer import keystone_client from ceilometer import messaging from ceilometer import opts from ceilometer import sample from ceilometer import version def prepare_service(argv=None, config_files=None, conf=None): if argv is None: argv = sys.argv if conf is None: conf = cfg.ConfigOpts() oslo_i18n.enable_lazy() for group, options in opts.list_opts(): conf.register_opts(list(options), group=None if group == "DEFAULT" else group) keystone_client.register_keystoneauth_opts(conf) log.register_options(conf) log_levels = ( log.get_default_log_levels() + ['futurist=INFO', 'openstack=INFO']) log.set_defaults(default_log_levels=log_levels) conf(argv[1:], project='ceilometer', validate_default_values=True, version=version.version_info.version_string(), default_config_files=config_files) keystone_client.post_register_keystoneauth_opts(conf) log.setup(conf, 'ceilometer') sample.setup(conf) gmr_opts.set_defaults(conf) gmr.TextGuruMeditation.setup_autorun(version, conf=conf) messaging.setup() return conf ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2541275 ceilometer-27.0.0.0rc1/ceilometer/share/0000775000175000017500000000000015250267567016567 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1943567 ceilometer-27.0.0.0rc1/ceilometer/share/__init__.py0000664000175000017500000000000015250267452020657 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1943567 ceilometer-27.0.0.0rc1/ceilometer/share/discovery.py0000664000175000017500000000201115250267452021133 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import manila_client from ceilometer.polling import plugin_base class ShareDiscovery(plugin_base.DiscoveryBase): """Discovery class for Manila shares.""" KEYSTONE_REQUIRED_FOR_SERVICE = 'manila' def __init__(self, conf): super().__init__(conf) self.manila_client = manila_client.Client(conf) def discover(self, manager, param=None): """Discover share resources to monitor.""" return self.manila_client.shares_list() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1953566 ceilometer-27.0.0.0rc1/ceilometer/share/manila.py0000664000175000017500000001027315250267452020376 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_log import log from ceilometer.polling import plugin_base from ceilometer import sample LOG = log.getLogger(__name__) # Manila share status values mapped to numeric values SHARE_STATUS = { 'available': 1, 'creating': 2, 'deleting': 3, 'error': 4, 'error_deleting': 5, 'migrating': 6, 'extending': 7, 'shrinking': 8, 'reverting': 9, 'inactive': 10, 'manage_starting': 11, 'manage_error': 12, 'unmanage_starting': 13, 'unmanage_error': 14, 'unmanaged': 15, 'extending_error': 16, 'shrinking_error': 17, 'reverting_error': 18, 'awaiting_transfer': 19, 'backup_creating': 20, 'backup_restoring': 21, 'backup_restoring_error': 22, 'creating_from_snapshot': 23, 'shrinking_possible_data_loss_error': 24, 'migrating_to': 25, 'replication_change': 26, } class _BaseSharePollster(plugin_base.PollsterBase): """Base pollster for Manila share metrics.""" # Fields to extract from openstacksdk Share objects # Note: share_protocol is renamed to 'protocol' in metadata for # compatibility with gnocchi_resources.yaml FIELDS = ['name', 'availability_zone', 'share_protocol', 'share_type', 'share_network_id', 'status', 'host', 'is_public', ] @property def default_discovery(self): return 'manila_shares' @staticmethod def extract_metadata(share): metadata = {k: getattr(share, k, None) for k in _BaseSharePollster.FIELDS} # Rename share_protocol to protocol for gnocchi_resources.yaml # compatibility with existing manila_share resource type if 'share_protocol' in metadata: metadata['protocol'] = metadata.pop('share_protocol') return metadata class ShareStatusPollster(_BaseSharePollster): """Pollster for Manila share status.""" @staticmethod def get_status_id(value): if not value: return -1 status = value.lower() return SHARE_STATUS.get(status, -1) def get_samples(self, manager, cache, resources): for share in resources or []: LOG.debug("Processing share: %s", share.id) status = self.get_status_id(share.status) if status == -1: LOG.warning( "Unknown status %(status)s for share " "%(name)s (%(id)s), setting status to -1", {"status": share.status, "name": share.name, "id": share.id}) yield sample.Sample( name='manila.share.status', type=sample.TYPE_GAUGE, unit='status', volume=status, user_id=getattr(share, 'user_id', None), project_id=share.project_id, resource_id=share.id, resource_metadata=self.extract_metadata(share) ) class ShareSizePollster(_BaseSharePollster): """Pollster for Manila share size.""" def get_samples(self, manager, cache, resources): for share in resources or []: LOG.debug("Processing share: %s", share.id) size = share.size if share.size is not None else 0 yield sample.Sample( name='manila.share.size', type=sample.TYPE_GAUGE, unit='GiB', volume=size, user_id=getattr(share, 'user_id', None), project_id=share.project_id, resource_id=share.id, resource_metadata=self.extract_metadata(share) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2551277 ceilometer-27.0.0.0rc1/ceilometer/telemetry/0000775000175000017500000000000015250267567017477 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1953566 ceilometer-27.0.0.0rc1/ceilometer/telemetry/__init__.py0000664000175000017500000000000015250267452021567 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1963568 ceilometer-27.0.0.0rc1/ceilometer/telemetry/notifications.py0000664000175000017500000000356015250267452022717 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.pipeline import sample as endpoint from ceilometer import sample class TelemetryIpc(endpoint.SampleEndpoint): """Handle sample from notification bus Telemetry samples polled by polling agent. """ event_types = ['telemetry.polling'] def build_sample(self, message): samples = message['payload']['samples'] for sample_dict in samples: yield sample.Sample( name=sample_dict['counter_name'], type=sample_dict['counter_type'], unit=sample_dict['counter_unit'], volume=sample_dict['counter_volume'], user_id=sample_dict['user_id'], project_id=sample_dict['project_id'], resource_id=sample_dict['resource_id'], timestamp=sample_dict['timestamp'], resource_metadata=sample_dict['resource_metadata'], source=sample_dict['source'], id=sample_dict['message_id'], # Project name and username might not be set, depending on the # configuration `identity_name_discovery`. Therefore, we cannot # assume that they exist in the sample dictionary. user_name=sample_dict.get('user_name'), project_name=sample_dict.get('project_name') ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2551277 ceilometer-27.0.0.0rc1/ceilometer/tests/0000775000175000017500000000000015250267567016627 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1963568 ceilometer-27.0.0.0rc1/ceilometer/tests/__init__.py0000664000175000017500000000000015250267452020717 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1963568 ceilometer-27.0.0.0rc1/ceilometer/tests/base.py0000664000175000017500000001022015250267452020077 0ustar00zuulzuul# Copyright 2012 New Dream Network (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Test base classes. """ import functools import os import tempfile import unittest from unittest import mock import fixtures from oslo_config import cfg from oslo_log import log import oslo_messaging.conffixture from oslotest import base import yaml import ceilometer from ceilometer import messaging from ceilometer.tests import fixtures as ceilo_fixtures from ceilometer.tests.unit import fakes CONF = cfg.CONF try: log.register_options(CONF) except cfg.ArgsAlreadyParsedError: pass CONF.set_override('use_stderr', False) class BaseTestCase(base.BaseTestCase): def setUp(self): super().setUp() if os.environ.get('OS_LOG_CAPTURE') in ('True', 'true', '1', 'yes'): self.stdlog = self.useFixture(ceilo_fixtures.StandardLogging()) self.addCleanup(CONF.reset) _fix = self.useFixture(ceilo_fixtures.FakeConnectionFixture()) self.fake_conn = _fix.fake_conn self.fake_conn_class_mock = _fix.connection_class_mock def setup_connection(self, **kwargs): """Convenience method to allow re-defining the fake connection members Used if the default Connection is not sufficient for testing e.g. if you need some resources empty, or want duplicates to force a particular behaviour in the test. """ self.fake_conn = mock.Mock(wraps=fakes.FakeConnection(**kwargs)) # This should be called before any code-under-test is run, but just in # case someone calls setup_connection mid-test, resetting the mock # prevents confusing AssertionErrors when checking the call count. self.fake_conn_class_mock.reset_mock() self.fake_conn_class_mock.return_value = self.fake_conn def setup_messaging(self, conf, exchange=None): self.useFixture(oslo_messaging.conffixture.ConfFixture(conf)) conf.set_override("notification_driver", ["messaging"]) if not exchange: exchange = 'ceilometer' conf.set_override("control_exchange", exchange) # NOTE(sileht): Ensure a new oslo.messaging driver is loaded # between each tests self.transport = messaging.get_transport(conf, "fake://", cache=False) self.useFixture(fixtures.MockPatch( 'ceilometer.messaging.get_transport', return_value=self.transport)) def cfg2file(self, data): cfgfile = tempfile.NamedTemporaryFile(mode='w', delete=False) self.addCleanup(os.remove, cfgfile.name) cfgfile.write(yaml.safe_dump(data)) cfgfile.close() return cfgfile.name @staticmethod def path_get(project_file=None): root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', ) ) if project_file: return os.path.join(root, project_file) else: return root def _skip_decorator(func): @functools.wraps(func) def skip_if_not_implemented(*args, **kwargs): try: return func(*args, **kwargs) except ceilometer.NotImplementedError as e: raise unittest.SkipTest(str(e)) return skip_if_not_implemented class SkipNotImplementedMeta(type): def __new__(cls, name, bases, local): for attr in local: value = local[attr] if callable(value) and ( attr.startswith('test_') or attr == 'setUp'): local[attr] = _skip_decorator(value) return type.__new__(cls, name, bases, local) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1973567 ceilometer-27.0.0.0rc1/ceilometer/tests/fixtures.py0000664000175000017500000001317715250267452021054 0ustar00zuulzuul# Copyright 2026 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # """Fixtures for Ceilometer tests.""" import logging as std_logging import os from unittest import mock import fixtures from ceilometer.tests.unit import fakes class FakeConnectionFixture(fixtures.Fixture): """Patches openstack.connection.Connection to return a FakeConnection spy. Attributes: fake_conn: mock.Mock wrapping the FakeConnection instance. Use this to assert on calls made by the code under test, e.g. ``fix.fake_conn.list_projects.assert_called_once_with(...)`` connection_class_mock: the mock of the Connection class itself (the patched callable). Use to assert the constructor was called with expected args, e.g. ``fix.connection_class_mock.assert_called_once_with(session=...)`` Usage:: fix = self.useFixture(ceilo_fixtures.FakeConnectionFixture( projects=[fakes.PROJECT_ADMIN_sdk])) fix.fake_conn.list_projects.assert_called_once_with(...) fix.connection_class_mock.assert_called_once_with( session=..., oslo_conf=..., service_types=...) """ def __init__(self, **kwargs): super().__init__() self._kwargs = kwargs def _setUp(self): """Patch Connection and set up return values. The Connection class is mocked with autospec=True, so that the constructor is validated at test time. This means if the code ever calls Connection(badkwarg=...) or if the SDK changes Connection.__init__'s signature, there'll be a TypeError rather than a silently passing test. """ self.fake_conn = mock.Mock(wraps=fakes.FakeConnection(**self._kwargs)) patcher = mock.patch( 'openstack.connection.Connection', return_value=self.fake_conn, autospec=True ) self.connection_class_mock = patcher.start() self.addCleanup(patcher.stop) class NullHandler(std_logging.Handler): """custom default NullHandler to attempt to format the record. Used in conjunction with log_fixture.get_logging_handle_error_fixture to detect formatting errors in debug level logs without saving the logs. """ def handle(self, record): self.format(record) def emit(self, record): pass def createLock(self): self.lock = None class StandardLogging(fixtures.Fixture): """Setup Logging redirection for tests. There are a number of things we want to handle with logging in tests: * Redirect the logging to somewhere that we can test or dump it later. * Ensure that as many DEBUG messages as possible are actually executed, to ensure they are actually syntactically valid * Ensure that we create useful output for tests that doesn't overwhelm the testing. To do this we create a logger fixture at the root level, which defaults to INFO and create a Null Logger at DEBUG which lets us execute log messages at DEBUG but not keep the output. To support local debugging OS_DEBUG=True can be set in the environment, which will print out the full debug logging. """ # External libraries that are excessively verbose at DEBUG/INFO level. # Set to WARNING so they do not flood the test output or the FakeLogger # capture buffer, while still surfacing WARNING-level events on failures. NOISY_LOGGERS = [ 'botocore', 'keystoneauth1', 'openstack', 'oslo.messaging', 'requests', 'stevedore', 'urllib3', ] def setUp(self): super().setUp() # set root logger to debug root = std_logging.getLogger() root.setLevel(std_logging.DEBUG) # Suppress verbose external library output; WARNING+ is still # captured by FakeLogger and surfaced when a test fails. for name in self.NOISY_LOGGERS: std_logging.getLogger(name).setLevel(std_logging.WARNING) # supports collecting debug level for local runs if os.environ.get('OS_DEBUG') in ('True', 'true', '1', 'yes'): level = std_logging.DEBUG else: level = std_logging.INFO # Collect logs fs = '%(asctime)s %(levelname)s [%(name)s] %(message)s' self.logger = self.useFixture( fixtures.FakeLogger(format=fs, level=None)) root.handlers[0].setLevel(level) if level > std_logging.DEBUG: # Just attempt to format debug level logs, but don't save them handler = NullHandler() self.useFixture(fixtures.LogHandler(handler, nuke_handlers=False)) handler.setLevel(std_logging.DEBUG) # At times we end up calling back into main() functions in # testing. This has the possibility of calling logging.setup # again, which completely unwinds the logging capture we've # created here. Once we've setup the logging the way we want, # disable the ability for the test to change this. def fake_logging_setup(*args): pass self.useFixture( fixtures.MonkeyPatch('oslo_log.log.setup', fake_logging_setup)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2601275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/0000775000175000017500000000000015250267567017606 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1973567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/__init__.py0000664000175000017500000000000015250267452021676 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2601275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/alarm/0000775000175000017500000000000015250267567020702 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1973567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/alarm/__init__.py0000664000175000017500000000000015250267452022772 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1983566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/alarm/test_aodh.py0000664000175000017500000000452015250267452023220 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.alarm import aodh from ceilometer.polling import manager from ceilometer import service import ceilometer.tests.base as base ALARM_METRIC_LIST = [ { 'evaluation_results': [{ 'alarm_id': 'b8e17f58-089a-43fc-a96b-e9bcac4d4b53', 'project_id': '2dd8edd6c8c24f49bf04670534f6b357', 'state_counters': { 'ok': 2, 'alarm': 5, 'insufficient data': 0, } }, { 'alarm_id': 'fa386719-67e3-42ff-aec8-17e547dac77a', 'project_id': 'd45b070bcce04ca99546128a40854e7c', 'state_counters': { 'ok': 50, 'alarm': 3, 'insufficient data': 10, } }], }, ] class TestAlarmEvaluationResultPollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = aodh.EvaluationResultPollster(conf) def test_alarm_pollster(self): alarm_samples = list( self.pollster.get_samples(self.manager, {}, resources=ALARM_METRIC_LIST)) self.assertEqual(6, len(alarm_samples)) self.assertEqual('alarm.evaluation_result', alarm_samples[0].name) self.assertEqual(2, alarm_samples[0].volume) self.assertEqual('2dd8edd6c8c24f49bf04670534f6b357', alarm_samples[0].project_id) self.assertEqual('b8e17f58-089a-43fc-a96b-e9bcac4d4b53', alarm_samples[0].resource_id) self.assertEqual('ok', alarm_samples[0].resource_metadata['alarm_state']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1983566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/alarm/test_discovery.py0000664000175000017500000000431715250267452024320 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from aodhclient import exceptions as aodh_exc import fixtures from ceilometer.alarm import discovery from ceilometer import service from ceilometer.tests import base class TestAlarmDiscovery(base.BaseTestCase): def setUp(self): super().setUp() self.conf = service.prepare_service([], []) self.useFixture( fixtures.MockPatch('ceilometer.keystone_client.get_session')) self.useFixture( fixtures.MockPatch('aodhclient.client.Client')) self.discovery = discovery.AlarmDiscovery(self.conf) self.manager = mock.Mock() def test_discover_returns_metrics(self): fake_metrics = {'evaluation_results': [{'alarm_id': 'fake'}]} self.discovery.aodh_client.metrics.get.return_value = fake_metrics resources = self.discovery.discover(self.manager) self.assertEqual([fake_metrics], resources) self.discovery.aodh_client.metrics.get.assert_called_once_with( all_projects=True) def test_discover_forbidden_does_not_raise(self): self.discovery.aodh_client.metrics.get.side_effect = ( aodh_exc.Forbidden()) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_forbidden_logs_error(self): self.discovery.aodh_client.metrics.get.side_effect = ( aodh_exc.Forbidden('metric endpoint disabled')) with mock.patch.object(discovery.LOG, 'error') as mock_log: self.discovery.discover(self.manager) mock_log.assert_called_once() self.assertIn('Skipping alarm discovery', mock_log.call_args[0][0]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2601275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/cmd/0000775000175000017500000000000015250267567020351 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1983566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/cmd/__init__.py0000664000175000017500000000000015250267452022441 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1983566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/cmd/test_status.py0000664000175000017500000000175515250267452023306 0ustar00zuulzuul# Copyright (c) 2018 NEC, Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_upgradecheck.upgradecheck import Code from ceilometer.cmd import status from ceilometer.tests import base class TestUpgradeChecks(base.BaseTestCase): def setUp(self): super().setUp() self.cmd = status.Checks() def test__sample_check(self): check_result = self.cmd._sample_check() self.assertEqual( Code.SUCCESS, check_result.code) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2611275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/0000775000175000017500000000000015250267567021262 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1993568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/__init__.py0000664000175000017500000000000015250267452023352 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2631276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/0000775000175000017500000000000015250267567023311 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1993568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/__init__.py0000664000175000017500000000000015250267452025401 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.1993568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/base.py0000664000175000017500000000633015250267452024570 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from ceilometer.compute.virt import inspector as virt_inspector from ceilometer import service import ceilometer.tests.base as base class TestPollsterBase(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.inspector = mock.Mock() self.instance = mock.MagicMock() self.instance.name = 'instance-00000001' setattr(self.instance, 'OS-EXT-SRV-ATTR:instance_name', self.instance.name) setattr(self.instance, 'OS-EXT-STS:vm_state', 'active') setattr(self.instance, 'OS-EXT-STS:task_state', None) self.instance.id = 1 self.instance.flavor = {'name': 'm1.small', 'id': 'eba4213d-3c6c-4b5f-8158-dd0022d71d62', 'vcpus': 1, 'ram': 512, 'disk': 20, 'ephemeral': 0, 'extra_specs': {'hw_rng:allowed': 'true'}} self.instance.status = 'active' self.instance.metadata = { 'fqdn': 'vm_fqdn', 'metering.stack': '2cadc4b4-8789-123c-b4eg-edd2f0a9c128', 'project_cos': 'dev'} self.instance.image = {'id': '0ff4d118-4947-49e6-963a-7a28e65f3f11'} self.instance.image_meta = { 'base_image_ref': self.instance.image['id'], 'container_format': 'bare', 'disk_format': 'raw', 'min_disk': '1', 'min_ram': '0', 'os_distro': 'ubuntu', 'os_type': 'linux'} self.useFixture(fixtures.MockPatch( 'ceilometer.compute.virt.inspector.get_hypervisor_inspector', new=mock.Mock(return_value=self.inspector))) # as we're having lazy hypervisor inspector singleton object in the # base compute pollster class, that leads to the fact that we # need to mock all this class property to avoid context sharing between # the tests self.useFixture(fixtures.MockPatch( 'ceilometer.compute.pollsters.' 'GenericComputePollster._get_inspector', return_value=self.inspector)) def _mock_inspect_instance(self, *data): next_value = iter(data) def inspect(instance, duration): value = next(next_value) if isinstance(value, virt_inspector.InstanceStats): return value else: raise value self.inspector.inspect_instance = mock.Mock(side_effect=inspect) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2003567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_cpu.py0000664000175000017500000001675615250267452025521 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import time from ceilometer.compute.pollsters import instance_stats from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class TestCPUPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(cpu_time=1 * (10 ** 6), cpu_number=2), virt_inspector.InstanceStats(cpu_time=3 * (10 ** 6), cpu_number=2), # cpu_time resets on instance restart virt_inspector.InstanceStats(cpu_time=2 * (10 ** 6), cpu_number=2), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) def _verify_cpu_metering(expected_time): cache = {} samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'cpu'}, {s.name for s in samples}) self.assertEqual(expected_time, samples[0].volume) self.assertEqual(2, samples[0].resource_metadata.get('cpu_number')) # ensure elapsed time between polling cycles is non-zero time.sleep(0.001) _verify_cpu_metering(1 * (10 ** 6)) _verify_cpu_metering(3 * (10 ** 6)) _verify_cpu_metering(2 * (10 ** 6)) # the following apply to all instance resource pollsters but are tested # here alone. def test_get_metadata(self): mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual(1, samples[0].resource_metadata['vcpus']) self.assertEqual(512, samples[0].resource_metadata['memory_mb']) self.assertEqual(20, samples[0].resource_metadata['disk_gb']) self.assertEqual(20, samples[0].resource_metadata['root_gb']) self.assertEqual(0, samples[0].resource_metadata['ephemeral_gb']) self.assertEqual('active', samples[0].resource_metadata['status']) self.assertEqual('active', samples[0].resource_metadata['state']) self.assertIsNone(samples[0].resource_metadata['task_state']) self.assertEqual(self.instance.flavor, samples[0].resource_metadata['flavor']) self.assertEqual(self.instance.image['id'], samples[0].resource_metadata['image_ref']) self.assertEqual(self.instance.image_meta, samples[0].resource_metadata['image_meta']) def test_get_reserved_metadata_with_keys(self): self.CONF.set_override('reserved_metadata_keys', ['fqdn']) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual({'fqdn': 'vm_fqdn', 'stack': '2cadc4b4-8789-123c-b4eg-edd2f0a9c128'}, samples[0].resource_metadata['user_metadata']) def test_get_reserved_metadata_with_namespace(self): mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual({'stack': '2cadc4b4-8789-123c-b4eg-edd2f0a9c128'}, samples[0].resource_metadata['user_metadata']) self.CONF.set_override('reserved_metadata_namespace', []) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertNotIn('user_metadata', samples[0].resource_metadata) def test_get_flavor_name_as_metadata_instance_type(self): mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual('m1.small', samples[0].resource_metadata['instance_type']) class TestVCPUsPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(cpu_time=1 * (10 ** 6), cpu_number=1), virt_inspector.InstanceStats(cpu_time=3 * (10 ** 6), cpu_number=1), # cpu_time resets on instance restart virt_inspector.InstanceStats(cpu_time=2 * (10 ** 6), cpu_number=2), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.VCPUsPollster(self.CONF) def _verify_cpu_metering(expected_cpu_number): cache = {} samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'vcpus'}, {s.name for s in samples}) self.assertEqual(expected_cpu_number, samples[0].volume) # ensure elapsed time between polling cycles is non-zero time.sleep(0.001) _verify_cpu_metering(1) _verify_cpu_metering(1) _verify_cpu_metering(2) def test_get_samples_shutoff(self): self._mock_inspect_instance( virt_inspector.InstanceStats(power_state=5, cpu_number=4, memory_actual=2048.0), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.VCPUsPollster(self.CONF) cache = {} samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual('vcpus', samples[0].name) self.assertEqual(4, samples[0].volume) class TestPowerStatePollster(base.TestPollsterBase): def test_get_samples_shutoff(self): self._mock_inspect_instance( virt_inspector.InstanceStats(power_state=5, cpu_number=2, memory_actual=1024.0), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.PowerStatePollster(self.CONF) cache = {} samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual('power.state', samples[0].name) self.assertEqual(5, samples[0].volume) class TestCPUPollsterShutoff(base.TestPollsterBase): def test_get_samples_shutoff_no_cpu_time(self): self._mock_inspect_instance( virt_inspector.InstanceStats(power_state=5, cpu_number=2, memory_actual=1024.0), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.CPUPollster(self.CONF) cache = {} samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(0, len(samples)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2003567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_disk.py0000664000175000017500000001202115250267452025641 0ustar00zuulzuul# Copyright 2025 Catalyst Cloud Limited # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.compute.pollsters import disk from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class TestDiskPollsterBase(base.TestPollsterBase): TYPE = 'gauge' def setUp(self): super().setUp() self.instances = self._get_fake_instances() def _get_fake_instances(self, ephemeral=0): instances = [] for i in [1, 2]: instance = mock.MagicMock() instance.name = f'instance-{i}' setattr(instance, 'OS-EXT-SRV-ATTR:instance_name', instance.name) instance.id = i instance.flavor = {'name': 'm1.small', 'id': 2, 'vcpus': 1, 'ram': 512, 'disk': 20, 'ephemeral': ephemeral} instance.status = 'active' instances.append(instance) return instances def _check_get_samples(self, factory, name, instances=None, expected_count=2): pollster = factory(self.CONF) mgr = manager.AgentManager(0, self.CONF) samples = list(pollster.get_samples(mgr, {}, instances or self.instances)) self.assertGreater(len(samples), 0) self.assertEqual({name}, {s.name for s in samples}, (f"Only samples for meter {name} " "should be published")) self.assertEqual(expected_count, len(samples)) return samples class TestDiskSizePollsters(TestDiskPollsterBase): TYPE = 'gauge' def test_ephemeral_disk_zero(self): samples = { sample.resource_id: sample for sample in self._check_get_samples( disk.EphemeralSizePollster, 'disk.ephemeral.size', expected_count=len(self.instances))} for instance in self.instances: with self.subTest(instance.name): self.assertIn(instance.id, samples) sample = samples[instance.id] self.assertEqual(instance.flavor['ephemeral'], sample.volume) self.assertEqual(self.TYPE, sample.type) def test_ephemeral_disk_nonzero(self): instances = self._get_fake_instances(ephemeral=10) samples = { sample.resource_id: sample for sample in self._check_get_samples( disk.EphemeralSizePollster, 'disk.ephemeral.size', instances=instances, expected_count=len(instances))} for instance in instances: with self.subTest(instance.name): self.assertIn(instance.id, samples) sample = samples[instance.id] self.assertEqual(instance.flavor['ephemeral'], sample.volume) self.assertEqual(self.TYPE, sample.type) def test_root_disk(self): samples = { sample.resource_id: sample for sample in self._check_get_samples( disk.RootSizePollster, 'disk.root.size', expected_count=len(self.instances))} for instance in self.instances: with self.subTest(instance.name): self.assertIn(instance.id, samples) sample = samples[instance.id] self.assertEqual((instance.flavor['disk'] - instance.flavor['ephemeral']), sample.volume) self.assertEqual(self.TYPE, sample.type) def test_root_disk_ephemeral_nonzero(self): instances = self._get_fake_instances(ephemeral=10) samples = { sample.resource_id: sample for sample in self._check_get_samples( disk.RootSizePollster, 'disk.root.size', instances=instances, expected_count=len(instances))} for instance in instances: with self.subTest(instance.name): self.assertIn(instance.id, samples) sample = samples[instance.id] self.assertEqual((instance.flavor['disk'] - instance.flavor['ephemeral']), sample.volume) self.assertEqual(self.TYPE, sample.type) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2013566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_diskio.py0000664000175000017500000002045215250267452026200 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # Copyright 2014 Cisco Systems, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.compute.pollsters import disk from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class TestBaseDiskIO(base.TestPollsterBase): TYPE = 'cumulative' def setUp(self): super().setUp() self.instance = self._get_fake_instances() @staticmethod def _get_fake_instances(): instances = [] for i in [1, 2]: instance = mock.MagicMock() instance.name = 'instance-%s' % i setattr(instance, 'OS-EXT-SRV-ATTR:instance_name', instance.name) instance.id = i instance.flavor = {'name': 'm1.small', 'id': 2, 'vcpus': 1, 'ram': 512, 'disk': 20, 'ephemeral': 0} instance.status = 'active' instances.append(instance) return instances def _check_get_samples(self, factory, name, expected_count=2): pollster = factory(self.CONF) mgr = manager.AgentManager(0, self.CONF) cache = {} samples = list(pollster.get_samples(mgr, cache, self.instance)) self.assertNotEqual(samples, []) cache_key = pollster.inspector_method self.assertIn(cache_key, cache) for instance in self.instance: self.assertIn(instance.id, cache[cache_key]) self.assertEqual({name}, {s.name for s in samples}) match = [s for s in samples if s.name == name] self.assertEqual(len(match), expected_count, 'missing counter %s' % name) return match def _check_aggregate_samples(self, factory, name, expected_volume, expected_device=None): match = self._check_get_samples(factory, name) self.assertEqual(expected_volume, match[0].volume) self.assertEqual(self.TYPE, match[0].type) if expected_device is not None: self.assertEqual(set(expected_device), set(match[0].resource_metadata.get('device'))) instances = [i.id for i in self.instance] for m in match: self.assertIn(m.resource_id, instances) def _check_per_device_samples(self, factory, name, expected_volume, expected_device=None): match = self._check_get_samples(factory, name, expected_count=4) match_dict = {} for m in match: match_dict[m.resource_id] = m for instance in self.instance: key = f"{instance.id}-{expected_device}" self.assertEqual(expected_volume, match_dict[key].volume) self.assertEqual(self.TYPE, match_dict[key].type) self.assertEqual(key, match_dict[key].resource_id) class TestDiskPollsters(TestBaseDiskIO): DISKS = [ virt_inspector.DiskStats(device='vda1', read_bytes=1, read_requests=2, write_bytes=3, write_requests=4, errors=-1, rd_total_times=100, wr_total_times=200,), virt_inspector.DiskStats(device='vda2', read_bytes=2, read_requests=3, write_bytes=5, write_requests=7, errors=-1, rd_total_times=300, wr_total_times=400,), ] def setUp(self): super().setUp() self.inspector.inspect_disks = mock.Mock(return_value=self.DISKS) def test_per_disk_read_requests(self): self._check_per_device_samples(disk.PerDeviceReadRequestsPollster, 'disk.device.read.requests', 2, 'vda1') self._check_per_device_samples(disk.PerDeviceReadRequestsPollster, 'disk.device.read.requests', 3, 'vda2') def test_per_disk_write_requests(self): self._check_per_device_samples(disk.PerDeviceWriteRequestsPollster, 'disk.device.write.requests', 4, 'vda1') self._check_per_device_samples(disk.PerDeviceWriteRequestsPollster, 'disk.device.write.requests', 7, 'vda2') def test_per_disk_read_bytes(self): self._check_per_device_samples(disk.PerDeviceReadBytesPollster, 'disk.device.read.bytes', 1, 'vda1') self._check_per_device_samples(disk.PerDeviceReadBytesPollster, 'disk.device.read.bytes', 2, 'vda2') def test_per_disk_write_bytes(self): self._check_per_device_samples(disk.PerDeviceWriteBytesPollster, 'disk.device.write.bytes', 3, 'vda1') self._check_per_device_samples(disk.PerDeviceWriteBytesPollster, 'disk.device.write.bytes', 5, 'vda2') def test_per_device_read_latency(self): self._check_per_device_samples( disk.PerDeviceDiskReadLatencyPollster, 'disk.device.read.latency', 100, 'vda1') self._check_per_device_samples( disk.PerDeviceDiskReadLatencyPollster, 'disk.device.read.latency', 300, 'vda2') def test_per_device_write_latency(self): self._check_per_device_samples( disk.PerDeviceDiskWriteLatencyPollster, 'disk.device.write.latency', 200, 'vda1') self._check_per_device_samples( disk.PerDeviceDiskWriteLatencyPollster, 'disk.device.write.latency', 400, 'vda2') class TestDiskInfoPollsters(TestBaseDiskIO): DISKS = [ virt_inspector.DiskInfo(device="vda1", capacity=3, allocation=2, physical=1), virt_inspector.DiskInfo(device="vda2", capacity=4, allocation=3, physical=2), ] TYPE = 'gauge' def setUp(self): super().setUp() self.inspector.inspect_disk_info = mock.Mock(return_value=self.DISKS) def test_per_disk_capacity(self): self._check_per_device_samples(disk.PerDeviceCapacityPollster, 'disk.device.capacity', 3, 'vda1') self._check_per_device_samples(disk.PerDeviceCapacityPollster, 'disk.device.capacity', 4, 'vda2') def test_per_disk_allocation(self): self._check_per_device_samples(disk.PerDeviceAllocationPollster, 'disk.device.allocation', 2, 'vda1') self._check_per_device_samples(disk.PerDeviceAllocationPollster, 'disk.device.allocation', 3, 'vda2') def test_per_disk_physical(self): self._check_per_device_samples(disk.PerDevicePhysicalPollster, 'disk.device.usage', 1, 'vda1') self._check_per_device_samples(disk.PerDevicePhysicalPollster, 'disk.device.usage', 2, 'vda2') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2023568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_location_metadata.py0000664000175000017500000001634715250267452030376 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for the compute pollsters. """ from ceilometer.compute.pollsters import util from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class FauxInstance: def __init__(self, **kwds): for name, value in kwds.items(): setattr(self, name, value) def __getitem__(self, key): return getattr(self, key) def get(self, key, default): try: return getattr(self, key) except AttributeError: return default class TestLocationMetadata(base.BaseTestCase): def setUp(self): self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) super().setUp() # Mimics an instance returned from nova api call self.INSTANCE_PROPERTIES = {'name': 'display name', 'id': ('234cbe81-4e09-4f64-9b2a-' '714f6b9046e3'), 'OS-EXT-SRV-ATTR:instance_name': 'instance-000001', 'OS-EXT-AZ:availability_zone': 'foo-zone', 'reservation_id': 'reservation id', 'architecture': 'x86_64', 'kernel_id': 'kernel id', 'os_type': 'linux', 'ramdisk_id': 'ramdisk id', 'status': 'active', 'ephemeral_gb': 0, 'root_gb': 20, 'disk_gb': 20, 'image': {'id': 1, 'links': [{"rel": "bookmark", 'href': 2}]}, 'image_meta': {'base_image_ref': 1, 'container_format': 'bare', 'disk_format': 'raw', 'min_disk': '20', 'min_ram': '0', 'os_distro': 'ubuntu', 'os_type': 'linux'}, 'hostId': '1234-5678', 'OS-EXT-SRV-ATTR:host': 'host-test', 'flavor': {'name': 'm1.tiny', 'id': ('eba4213d-3c6c-' '4b5f-8158-' 'dd0022d71d62'), 'disk': 20, 'ram': 512, 'vcpus': 2, 'ephemeral': 0, 'extra_specs': { 'hw_rng:allowed': 'true'}}, 'metadata': {'metering.autoscale.group': 'X' * 512, 'metering.ephemeral_gb': 42}} self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) def test_metadata(self): md = util._get_metadata_from_object(self.CONF, self.instance) for prop, value in self.INSTANCE_PROPERTIES.items(): if prop not in ("metadata"): # Special cases if prop == 'name': prop = 'display_name' elif prop == 'hostId': prop = "host" elif prop == 'OS-EXT-SRV-ATTR:host': prop = "instance_host" elif prop == 'OS-EXT-SRV-ATTR:instance_name': prop = 'name' elif prop == "id": prop = "instance_id" self.assertEqual(value, md[prop]) user_metadata = md['user_metadata'] expected = self.INSTANCE_PROPERTIES[ 'metadata']['metering.autoscale.group'][:256] self.assertEqual(expected, user_metadata['autoscale_group']) self.assertEqual(1, len(user_metadata)) self.assertEqual(self.INSTANCE_PROPERTIES['image']['id'], md['image_ref']) self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'], md['image_meta']) def test_metadata_empty_image(self): self.INSTANCE_PROPERTIES['image'] = None self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertIsNone(md['image']) self.assertIsNone(md['image_ref']) self.assertIsNone(md['image_ref_url']) def test_metadata_image_through_conductor(self): # There should be no links here, should default to None self.INSTANCE_PROPERTIES['image'] = {'id': 1} self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertEqual(1, md['image_ref']) self.assertIsNone(md['image_ref_url']) def test_metadata_image_meta_volume_image(self): self.INSTANCE_PROPERTIES['image_meta']['base_image_ref'] = '' self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'], md['image_meta']) def test_metadata_image_meta_volume_no_image(self): self.INSTANCE_PROPERTIES['image_meta'] = {'base_image_ref': ''} self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertEqual(self.INSTANCE_PROPERTIES['image_meta'], md['image_meta']) def test_metadata_image_meta_none(self): self.INSTANCE_PROPERTIES['image_meta'] = None self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertNotIn('image_meta', md) def test_metadata_image_meta_noexist(self): del self.INSTANCE_PROPERTIES['image_meta'] self.instance = FauxInstance(**self.INSTANCE_PROPERTIES) md = util._get_metadata_from_object(self.CONF, self.instance) self.assertNotIn('image_meta', md) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2023568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_memory.py0000664000175000017500000002211015250267452026217 0ustar00zuulzuul# Copyright (c) 2014 VMware, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.compute.pollsters import instance_stats from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class TestMemoryPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(memory_actual=1024.0), virt_inspector.InstanceStats(memory_actual=2048.0), virt_inspector.InstanceStats(), virt_inspector.InstanceShutOffException(), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryPollster(self.CONF) @mock.patch('ceilometer.compute.pollsters.LOG') def _verify_memory_metering(expected_count, expected_memory_mb, expected_warnings, mylog): samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(expected_count, len(samples)) if expected_count > 0: self.assertEqual({'memory'}, {s.name for s in samples}) self.assertEqual(expected_memory_mb, samples[0].volume) else: self.assertEqual(expected_warnings, mylog.warning.call_count) self.assertEqual(0, mylog.exception.call_count) _verify_memory_metering(1, 1024.0, 0) _verify_memory_metering(1, 2048.0, 0) _verify_memory_metering(0, 0, 1) _verify_memory_metering(0, 0, 0) def test_get_samples_with_empty_stats(self): self._mock_inspect_instance(virt_inspector.NoDataException()) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryPollster(self.CONF) def all_samples(): return list(pollster.get_samples(mgr, {}, [self.instance])) class TestMemoryAvailablePollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(memory_available=1024.0), virt_inspector.InstanceStats(memory_available=2048.0), virt_inspector.InstanceStats(), virt_inspector.InstanceShutOffException(), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryAvailablePollster(self.CONF) @mock.patch('ceilometer.compute.pollsters.LOG') def _verify_memory_available_metering(expected_count, expected_memory_mb, expected_warnings, mylog): samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(expected_count, len(samples)) if expected_count > 0: self.assertEqual({'memory.available'}, {s.name for s in samples}) self.assertEqual(expected_memory_mb, samples[0].volume) else: self.assertEqual(expected_warnings, mylog.warning.call_count) self.assertEqual(0, mylog.exception.call_count) _verify_memory_available_metering(1, 1024.0, 0) _verify_memory_available_metering(1, 2048.0, 0) _verify_memory_available_metering(0, 0, 1) _verify_memory_available_metering(0, 0, 0) def test_get_samples_with_empty_stats(self): self._mock_inspect_instance(virt_inspector.NoDataException()) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryPollster(self.CONF) def all_samples(): return list(pollster.get_samples(mgr, {}, [self.instance])) class TestMemoryUsagePollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(memory_usage=1.0), virt_inspector.InstanceStats(memory_usage=2.0), virt_inspector.InstanceStats(), virt_inspector.InstanceShutOffException(), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryUsagePollster(self.CONF) @mock.patch('ceilometer.compute.pollsters.LOG') def _verify_memory_usage_metering(expected_count, expected_memory_mb, expected_warnings, mylog): samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(expected_count, len(samples)) if expected_count > 0: self.assertEqual({'memory.usage'}, {s.name for s in samples}) self.assertEqual(expected_memory_mb, samples[0].volume) else: self.assertEqual(expected_warnings, mylog.warning.call_count) self.assertEqual(0, mylog.exception.call_count) _verify_memory_usage_metering(1, 1.0, 0) _verify_memory_usage_metering(1, 2.0, 0) _verify_memory_usage_metering(0, 0, 1) _verify_memory_usage_metering(0, 0, 0) def test_get_samples_with_empty_stats(self): self._mock_inspect_instance(virt_inspector.NoDataException()) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryUsagePollster(self.CONF) def all_samples(): return list(pollster.get_samples(mgr, {}, [self.instance])) class TestResidentMemoryPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(memory_resident=1.0), virt_inspector.InstanceStats(memory_resident=2.0), virt_inspector.InstanceStats(), virt_inspector.InstanceShutOffException(), ) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemoryResidentPollster(self.CONF) @mock.patch('ceilometer.compute.pollsters.LOG') def _verify_resident_memory_metering(expected_count, expected_resident_memory_mb, expected_warnings, mylog): samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(expected_count, len(samples)) if expected_count > 0: self.assertEqual({'memory.resident'}, {s.name for s in samples}) self.assertEqual(expected_resident_memory_mb, samples[0].volume) else: self.assertEqual(expected_warnings, mylog.warning.call_count) self.assertEqual(0, mylog.exception.call_count) _verify_resident_memory_metering(1, 1.0, 0) _verify_resident_memory_metering(1, 2.0, 0) _verify_resident_memory_metering(0, 0, 1) _verify_resident_memory_metering(0, 0, 0) class TestMemorySwapPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(memory_swap_in=1.0, memory_swap_out=2.0), virt_inspector.InstanceStats(memory_swap_in=3.0, memory_swap_out=4.0), ) mgr = manager.AgentManager(0, self.CONF) def _check_memory_swap_in(expected_swap_in): pollster = instance_stats.MemorySwapInPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'memory.swap.in'}, {s.name for s in samples}) self.assertEqual(expected_swap_in, samples[0].volume) def _check_memory_swap_out(expected_swap_out): pollster = instance_stats.MemorySwapOutPollster(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'memory.swap.out'}, {s.name for s in samples}) self.assertEqual(expected_swap_out, samples[0].volume) _check_memory_swap_in(1.0) _check_memory_swap_out(4.0) def test_get_samples_with_empty_stats(self): self._mock_inspect_instance(virt_inspector.NoDataException()) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.MemorySwapInPollster(self.CONF) def all_samples(): return list(pollster.get_samples(mgr, {}, [self.instance])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2023568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_net.py0000664000175000017500000003352515250267452025511 0ustar00zuulzuul# # Copyright 2012 eNovance # Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.compute.pollsters import net from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class FauxInstance: def __init__(self, **kwargs): for name, value in kwargs.items(): setattr(self, name, value) def __getitem__(self, key): return getattr(self, key) def get(self, key, default): return getattr(self, key, default) class TestNetPollster(base.TestPollsterBase): def setUp(self): super().setUp() self.vnic0 = virt_inspector.InterfaceStats( name='vnet0', fref='fa163e71ec6e', mac='fa:16:3e:71:ec:6d', parameters=dict(ip='10.0.0.2', projmask='255.255.255.0', projnet='proj1', dhcp_server='10.0.0.1'), rx_bytes=1, rx_packets=2, rx_drop=20, rx_errors=21, tx_bytes=3, tx_packets=4, tx_drop=22, tx_errors=23, rx_bytes_delta=42, tx_bytes_delta=43) self.vnic1 = virt_inspector.InterfaceStats( name='vnet1', fref='fa163e71ec6f', mac='fa:16:3e:71:ec:6e', parameters=dict(ip='192.168.0.3', projmask='255.255.255.0', projnet='proj2', dhcp_server='10.0.0.2'), rx_bytes=5, rx_packets=6, rx_drop=24, rx_errors=25, tx_bytes=7, tx_packets=8, tx_drop=26, tx_errors=27, rx_bytes_delta=44, tx_bytes_delta=45) self.vnic2 = virt_inspector.InterfaceStats( name='vnet2', fref=None, mac='fa:18:4e:72:fc:7e', parameters=dict(ip='192.168.0.4', projmask='255.255.255.0', projnet='proj3', dhcp_server='10.0.0.3'), rx_bytes=9, rx_packets=10, rx_drop=28, rx_errors=29, tx_bytes=11, tx_packets=12, tx_drop=30, tx_errors=31, rx_bytes_delta=46, tx_bytes_delta=47) vnics = [ self.vnic0, self.vnic1, self.vnic2, ] self.inspector.inspect_vnics = mock.Mock(return_value=vnics) self.INSTANCE_PROPERTIES = {'name': 'display name', 'OS-EXT-SRV-ATTR:instance_name': 'instance-000001', 'OS-EXT-AZ:availability_zone': 'foo-zone', 'reservation_id': 'reservation id', 'id': 'instance id', 'user_id': 'user id', 'tenant_id': 'tenant id', 'architecture': 'x86_64', 'kernel_id': 'kernel id', 'os_type': 'linux', 'ramdisk_id': 'ramdisk id', 'status': 'active', 'ephemeral_gb': 0, 'root_gb': 20, 'disk_gb': 20, 'image': {'id': 1, 'links': [{"rel": "bookmark", 'href': 2}]}, 'hostId': '1234-5678', 'OS-EXT-SRV-ATTR:host': 'host-test', 'flavor': {'disk': 20, 'ram': 512, 'name': 'tiny', 'vcpus': 2, 'ephemeral': 0}, 'metadata': {'metering.autoscale.group': 'X' * 512, 'metering.foobar': 42}} self.faux_instance = FauxInstance(**self.INSTANCE_PROPERTIES) def _check_get_samples(self, factory, expected, expected_name, kind='cumulative'): mgr = manager.AgentManager(0, self.CONF) pollster = factory(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(3, len(samples)) # one for each nic self.assertEqual({expected_name}, {s.name for s in samples}) def _verify_vnic_metering(ip, expected_volume, expected_rid): match = [s for s in samples if s.resource_metadata['parameters']['ip'] == ip ] self.assertEqual(len(match), 1, 'missing ip %s' % ip) self.assertEqual(expected_volume, match[0].volume) self.assertEqual(kind, match[0].type) self.assertEqual(expected_rid, match[0].resource_id) for ip, volume, rid in expected: _verify_vnic_metering(ip, volume, rid) def test_incoming_bytes(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingBytesPollster, [('10.0.0.2', 1, self.vnic0.fref), ('192.168.0.3', 5, self.vnic1.fref), ('192.168.0.4', 9, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.bytes', ) def test_outgoing_bytes(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingBytesPollster, [('10.0.0.2', 3, self.vnic0.fref), ('192.168.0.3', 7, self.vnic1.fref), ('192.168.0.4', 11, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.bytes', ) def test_incoming_bytes_delta(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingBytesDeltaPollster, [('10.0.0.2', 42, self.vnic0.fref), ('192.168.0.3', 44, self.vnic1.fref), ('192.168.0.4', 46, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.bytes.delta', 'delta', ) def test_outgoing_bytes_delta(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingBytesDeltaPollster, [('10.0.0.2', 43, self.vnic0.fref), ('192.168.0.3', 45, self.vnic1.fref), ('192.168.0.4', 47, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.bytes.delta', 'delta', ) def test_incoming_packets(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingPacketsPollster, [('10.0.0.2', 2, self.vnic0.fref), ('192.168.0.3', 6, self.vnic1.fref), ('192.168.0.4', 10, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.packets', ) def test_outgoing_packets(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingPacketsPollster, [('10.0.0.2', 4, self.vnic0.fref), ('192.168.0.3', 8, self.vnic1.fref), ('192.168.0.4', 12, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.packets', ) def test_incoming_drops(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingDropPollster, [('10.0.0.2', 20, self.vnic0.fref), ('192.168.0.3', 24, self.vnic1.fref), ('192.168.0.4', 28, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.packets.drop', ) def test_outgoing_drops(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingDropPollster, [('10.0.0.2', 22, self.vnic0.fref), ('192.168.0.3', 26, self.vnic1.fref), ('192.168.0.4', 30, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.packets.drop', ) def test_incoming_errors(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingErrorsPollster, [('10.0.0.2', 21, self.vnic0.fref), ('192.168.0.3', 25, self.vnic1.fref), ('192.168.0.4', 29, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.packets.error', ) def test_outgoing_errors(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingErrorsPollster, [('10.0.0.2', 23, self.vnic0.fref), ('192.168.0.3', 27, self.vnic1.fref), ('192.168.0.4', 31, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.packets.error', ) def test_metadata(self): factory = net.OutgoingBytesPollster pollster = factory(self.CONF) mgr = manager.AgentManager(0, self.CONF) pollster = factory(self.CONF) s = list(pollster.get_samples(mgr, {}, [self.faux_instance]))[0] user_metadata = s.resource_metadata['user_metadata'] expected = self.INSTANCE_PROPERTIES[ 'metadata']['metering.autoscale.group'][:256] self.assertEqual(expected, user_metadata['autoscale_group']) self.assertEqual(2, len(user_metadata)) class TestNetRatesPollster(base.TestPollsterBase): def setUp(self): super().setUp() self.vnic0 = virt_inspector.InterfaceRateStats( name='vnet0', fref='fa163e71ec6e', mac='fa:16:3e:71:ec:6d', parameters=dict(ip='10.0.0.2', projmask='255.255.255.0', projnet='proj1', dhcp_server='10.0.0.1'), rx_bytes_rate=1, tx_bytes_rate=2) self.vnic1 = virt_inspector.InterfaceRateStats( name='vnet1', fref='fa163e71ec6f', mac='fa:16:3e:71:ec:6e', parameters=dict(ip='192.168.0.3', projmask='255.255.255.0', projnet='proj2', dhcp_server='10.0.0.2'), rx_bytes_rate=3, tx_bytes_rate=4) self.vnic2 = virt_inspector.InterfaceRateStats( name='vnet2', fref=None, mac='fa:18:4e:72:fc:7e', parameters=dict(ip='192.168.0.4', projmask='255.255.255.0', projnet='proj3', dhcp_server='10.0.0.3'), rx_bytes_rate=5, tx_bytes_rate=6) vnics = [ self.vnic0, self.vnic1, self.vnic2, ] self.inspector.inspect_vnic_rates = mock.Mock(return_value=vnics) def _check_get_samples(self, factory, expected, expected_name): mgr = manager.AgentManager(0, self.CONF) pollster = factory(self.CONF) samples = list(pollster.get_samples(mgr, {}, [self.instance])) self.assertEqual(3, len(samples)) # one for each nic self.assertEqual({expected_name}, {s.name for s in samples}) def _verify_vnic_metering(ip, expected_volume, expected_rid): match = [s for s in samples if s.resource_metadata['parameters']['ip'] == ip ] self.assertEqual(1, len(match), 'missing ip %s' % ip) self.assertEqual(expected_volume, match[0].volume) self.assertEqual('gauge', match[0].type) self.assertEqual(expected_rid, match[0].resource_id) for ip, volume, rid in expected: _verify_vnic_metering(ip, volume, rid) def test_incoming_bytes_rate(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.IncomingBytesRatePollster, [('10.0.0.2', 1, self.vnic0.fref), ('192.168.0.3', 3, self.vnic1.fref), ('192.168.0.4', 5, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.incoming.bytes.rate', ) def test_outgoing_bytes_rate(self): instance_name_id = f"{self.instance.name}-{self.instance.id}" self._check_get_samples( net.OutgoingBytesRatePollster, [('10.0.0.2', 2, self.vnic0.fref), ('192.168.0.3', 4, self.vnic1.fref), ('192.168.0.4', 6, f"{instance_name_id}-{self.vnic2.name}"), ], 'network.outgoing.bytes.rate', ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2033567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/pollsters/test_perf.py0000664000175000017500000000673015250267452025655 0ustar00zuulzuul# Copyright 2016 Intel # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.compute.pollsters import instance_stats from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.polling import manager from ceilometer.tests.unit.compute.pollsters import base class TestPerfPollster(base.TestPollsterBase): def test_get_samples(self): self._mock_inspect_instance( virt_inspector.InstanceStats(cpu_cycles=7259361, instructions=8815623, cache_references=74184, cache_misses=16737) ) mgr = manager.AgentManager(0, self.CONF) cache = {} def _check_perf_events_cpu_cycles(expected_usage): pollster = instance_stats.PerfCPUCyclesPollster(self.CONF) samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'perf.cpu.cycles'}, {s.name for s in samples}) self.assertEqual(expected_usage, samples[0].volume) def _check_perf_events_instructions(expected_usage): pollster = instance_stats.PerfInstructionsPollster(self.CONF) samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'perf.instructions'}, {s.name for s in samples}) self.assertEqual(expected_usage, samples[0].volume) def _check_perf_events_cache_references(expected_usage): pollster = instance_stats.PerfCacheReferencesPollster( self.CONF) samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'perf.cache.references'}, {s.name for s in samples}) self.assertEqual(expected_usage, samples[0].volume) def _check_perf_events_cache_misses(expected_usage): pollster = instance_stats.PerfCacheMissesPollster(self.CONF) samples = list(pollster.get_samples(mgr, cache, [self.instance])) self.assertEqual(1, len(samples)) self.assertEqual({'perf.cache.misses'}, {s.name for s in samples}) self.assertEqual(expected_usage, samples[0].volume) _check_perf_events_cpu_cycles(7259361) _check_perf_events_instructions(8815623) _check_perf_events_cache_references(74184) _check_perf_events_cache_misses(16737) def test_get_samples_with_empty_stats(self): self._mock_inspect_instance(virt_inspector.NoDataException()) mgr = manager.AgentManager(0, self.CONF) pollster = instance_stats.PerfCPUCyclesPollster(self.CONF) def all_samples(): return list(pollster.get_samples(mgr, {}, [self.instance])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2033567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/test_discovery.py0000664000175000017500000012213315250267452024675 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import datetime from unittest import mock import fixtures from novaclient import exceptions from ceilometer.compute import discovery from ceilometer.compute.pollsters import util from ceilometer import service from ceilometer.tests import base # Mock libvirt constants and exceptions to avoid libvirt dependency VIR_ERR_NO_DOMAIN_METADATA = 80 class FakeLibvirtError(Exception): """Fake exception to replace libvirt.libvirtError in tests.""" def __init__(self, message, error_code=None, error_domain=None): super().__init__(message) self._error_code = error_code self._error_domain = error_domain def get_error_code(self): return self._error_code def get_error_domain(self): return self._error_domain LIBVIRT_METADATA_XML = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 true bare raw 1 0 ubuntu linux admin admin """ LIBVIRT_METADATA_XML_OLD = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 admin admin """ LIBVIRT_METADATA_XML_EMPTY_FLAVOR_ID = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 true admin admin """ LIBVIRT_METADATA_XML_NO_FLAVOR_ID = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 true admin admin """ LIBVIRT_METADATA_XML_EMPTY_FLAVOR_EXTRA_SPECS = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 admin admin """ LIBVIRT_METADATA_XML_NO_FLAVOR_EXTRA_SPECS = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 admin admin """ LIBVIRT_METADATA_XML_FROM_VOLUME_IMAGE = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 true bare raw 1 0 ubuntu linux admin admin """ LIBVIRT_METADATA_XML_FROM_VOLUME_NO_IMAGE = """ test.dom.com 2016-11-16 07:35:06 512 1 0 0 1 true admin admin """ LIBVIRT_DESC_XML = """ instance-00000001 a75c2fa5-6c03-45a8-bbf7-b993cfcdec27 hvm /opt/stack/data/nova/instances/a75c2fa5-6c03-45a8-bbf7-b993cfcdec27/kernel /opt/stack/data/nova/instances/a75c2fa5-6c03-45a8-bbf7-b993cfcdec27/ramdisk root=/dev/vda console=tty0 console=ttyS0 """ LIBVIRT_MANUAL_INSTANCE_DESC_XML = """ Manual-instance-00000001 5e637d0d-8c0e-441a-a11a-a9dc95aed84e hvm /opt/instances/5e637d0d-8c0e-441a-a11a-a9dc95aed84e/kernel /opt/instances/5e637d0d-8c0e-441a-a11a-a9dc95aed84e/ramdisk root=/dev/vda console=tty0 console=ttyS0 """ class FakeDomain: def __init__(self, desc=None, metadata=None): self._desc = desc or LIBVIRT_DESC_XML self._metadata = metadata or LIBVIRT_METADATA_XML def state(self): return [1, 2] def name(self): return "instance-00000001" def UUIDString(self): return "a75c2fa5-6c03-45a8-bbf7-b993cfcdec27" def XMLDesc(self): return self._desc def metadata(self, flags, url): return self._metadata class FakeConn: def __init__(self, domains=None): self._domains = domains or [FakeDomain()] def listAllDomains(self): return list(self._domains) def isAlive(self): return True class FakeManualInstanceDomain: def state(self): return [1, 2] def name(self): return "Manual-instance-00000001" def UUIDString(self): return "5e637d0d-8c0e-441a-a11a-a9dc95aed84e" def XMLDesc(self): return LIBVIRT_MANUAL_INSTANCE_DESC_XML def metadata(self, flags, url): # Note(xiexianbin): vm not create by nova-compute don't have metadata # elements like: '' # When invoke get metadata method, raise libvirtError. raise FakeLibvirtError( "metadata not found: Requested metadata element is not present", VIR_ERR_NO_DOMAIN_METADATA) class FakeManualInstanceConn: def listAllDomains(self): return [FakeManualInstanceDomain()] def isAlive(self): return True class TestDiscovery(base.BaseTestCase): def setUp(self): super().setUp() self.instance = mock.MagicMock() self.instance.name = 'instance-00000001' setattr(self.instance, 'OS-EXT-SRV-ATTR:instance_name', self.instance.name) setattr(self.instance, 'OS-EXT-STS:vm_state', 'active') # FIXME(sileht): This is wrong, this should be a uuid # The internal id of nova can't be retrieved via API or notification self.instance.id = 1 self.instance.flavor = {'name': 'm1.small', 'id': 'eba4213d-3c6c-4b5f-8158-dd0022d71d62', 'vcpus': 1, 'ram': 512, 'disk': 20, 'ephemeral': 0, 'extra_specs': {'hw_rng:allowed': 'true'}} self.instance.status = 'active' self.instance.metadata = { 'fqdn': 'vm_fqdn', 'metering.stack': '2cadc4b4-8789-123c-b4eg-edd2f0a9c128', 'project_cos': 'dev'} # as we're having lazy hypervisor inspector singleton object in the # base compute pollster class, that leads to the fact that we # need to mock all this class property to avoid context sharing between # the tests self.client = mock.MagicMock() self.client.instance_get_all_by_host.return_value = [self.instance] patch_client = fixtures.MockPatch('ceilometer.nova_client.Client', return_value=self.client) self.useFixture(patch_client) self.utc_now = mock.MagicMock( return_value=datetime.datetime( 2016, 1, 1, tzinfo=datetime.timezone.utc)) patch_timeutils = fixtures.MockPatch('oslo_utils.timeutils.utcnow', self.utc_now) self.useFixture(patch_timeutils) self.CONF = service.prepare_service([], []) self.CONF.set_override('host', 'test') # Add a mocked Libvirt, so we don't have to rely on python-libvirt # being installed only for testing self.libvirt = mock.MagicMock() self.libvirt.libvirtError = FakeLibvirtError patch_libvirt = fixtures.MockPatch( 'ceilometer.compute.virt.libvirt.utils.libvirt', self.libvirt) self.useFixture(patch_libvirt) def test_normal_discovery(self): self.CONF.set_override("instance_discovery_method", "naive", group="compute") dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) self.assertEqual(1, list(resources)[0].id) self.client.instance_get_all_by_host.assert_called_once_with( 'test', None) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) self.assertEqual(1, list(resources)[0].id) self.client.instance_get_all_by_host.assert_called_with( self.CONF.host, "2016-01-01T00:00:00+00:00") def test_discovery_with_resource_update_interval(self): self.CONF.set_override("instance_discovery_method", "naive", group="compute") self.CONF.set_override("resource_update_interval", 600, group="compute") dsc = discovery.InstanceDiscovery(self.CONF) dsc.last_run = datetime.datetime( 2016, 1, 1, tzinfo=datetime.timezone.utc) self.utc_now.return_value = datetime.datetime( 2016, 1, 1, minute=5, tzinfo=datetime.timezone.utc) resources = dsc.discover(mock.MagicMock()) self.assertEqual(0, len(resources)) self.client.instance_get_all_by_host.assert_not_called() self.utc_now.return_value = datetime.datetime( 2016, 1, 1, minute=20, tzinfo=datetime.timezone.utc) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) self.assertEqual(1, list(resources)[0].id) self.client.instance_get_all_by_host.assert_called_once_with( self.CONF.host, "2016-01-01T00:00:00+00:00") @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn() dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27", s.resource_id) self.assertEqual("d99c829753f64057bc0f2030da309943", s.project_id) self.assertEqual("a1f4684e58bd4c88aefd2ecb0783b497", s.user_id) metadata = s.resource_metadata self.assertEqual(1, metadata["vcpus"]) self.assertEqual(512, metadata["memory_mb"]) self.assertEqual(1, metadata["disk_gb"]) self.assertEqual(0, metadata["ephemeral_gb"]) self.assertEqual(1, metadata["root_gb"]) self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11", metadata["image_ref"]) self.assertEqual("test.dom.com", metadata["display_name"]) self.assertEqual("instance-00000001", metadata["name"]) self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27", metadata["instance_id"]) self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {"hw_rng:allowed": "true"}}, metadata["flavor"]) self.assertEqual( "4d0bc931ea7f0513da2efd9acb4cf3a273c64b7bcc544e15c070e662", metadata["host"]) self.assertEqual(self.CONF.host, metadata["instance_host"]) self.assertEqual("active", metadata["status"]) self.assertEqual("running", metadata["state"]) self.assertEqual("hvm", metadata["os_type"]) self.assertEqual("x86_64", metadata["architecture"]) self.assertNotIn("user_metadata", metadata) self.assertEqual({"id"}, set(metadata["image"].keys())) self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11", metadata["image"]["id"]) self.assertIn("image_meta", metadata) self.assertEqual({"base_image_ref", "container_format", "disk_format", "min_disk", "min_ram", "os_distro", "os_type"}, set(metadata["image_meta"].keys())) self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11", metadata["image_meta"]["base_image_ref"]) self.assertEqual("bare", metadata["image_meta"]["container_format"]) self.assertEqual("raw", metadata["image_meta"]["disk_format"]) self.assertEqual("1", metadata["image_meta"]["min_disk"]) self.assertEqual("0", metadata["image_meta"]["min_ram"]) self.assertEqual("ubuntu", metadata["image_meta"]["os_distro"]) self.assertEqual("linux", metadata["image_meta"]["os_type"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_old( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain(metadata=LIBVIRT_METADATA_XML_OLD)]) mock_get_flavor_id.return_value = ( "eba4213d-3c6c-4b5f-8158-dd0022d71d62") dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_called_with("m1.tiny") mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27", s.resource_id) self.assertEqual("d99c829753f64057bc0f2030da309943", s.project_id) self.assertEqual("a1f4684e58bd4c88aefd2ecb0783b497", s.user_id) metadata = s.resource_metadata self.assertEqual(1, metadata["vcpus"]) self.assertEqual(512, metadata["memory_mb"]) self.assertEqual(1, metadata["disk_gb"]) self.assertEqual(0, metadata["ephemeral_gb"]) self.assertEqual(1, metadata["root_gb"]) self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11", metadata["image_ref"]) self.assertEqual("test.dom.com", metadata["display_name"]) self.assertEqual("instance-00000001", metadata["name"]) self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27", metadata["instance_id"]) self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1}, metadata["flavor"]) self.assertEqual( "4d0bc931ea7f0513da2efd9acb4cf3a273c64b7bcc544e15c070e662", metadata["host"]) self.assertEqual(self.CONF.host, metadata["instance_host"]) self.assertEqual("active", metadata["status"]) self.assertEqual("running", metadata["state"]) self.assertEqual("hvm", metadata["os_type"]) self.assertEqual("x86_64", metadata["architecture"]) self.assertNotIn("user_metadata", metadata) self.assertEqual({"id"}, set(metadata["image"].keys())) self.assertEqual("bdaf114a-35e9-4163-accd-226d5944bf11", metadata["image"]["id"]) self.assertNotIn("image_meta", metadata) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_fetch_extra_metadata( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") self.CONF.set_override("fetch_extra_metadata", True, group="compute") mock_libvirt_conn.return_value = FakeConn() mock_get_server.return_value = argparse.Namespace( metadata={"metering.server_group": "group1"}) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_called_with( "a75c2fa5-6c03-45a8-bbf7-b993cfcdec27") self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual({"server_group": "group1"}, metadata["user_metadata"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_fetch_extra_metadata_old( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") self.CONF.set_override("fetch_extra_metadata", True, group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain(metadata=LIBVIRT_METADATA_XML_OLD)]) mock_get_server.return_value = argparse.Namespace( flavor={"id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62"}, metadata={"metering.server_group": "group1"}) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_called_with( "a75c2fa5-6c03-45a8-bbf7-b993cfcdec27") self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual({"server_group": "group1"}, metadata["user_metadata"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_empty_flavor_id_get_by_flavor( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain( metadata=LIBVIRT_METADATA_XML_EMPTY_FLAVOR_ID)]) mock_get_flavor_id.return_value = ( "eba4213d-3c6c-4b5f-8158-dd0022d71d62") dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_called_with("m1.tiny") mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {"hw_rng:allowed": "true"}}, metadata["flavor"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_empty_flavor_id_get_by_server( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") self.CONF.set_override("fetch_extra_metadata", True, group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain( metadata=LIBVIRT_METADATA_XML_EMPTY_FLAVOR_ID)]) mock_get_server.return_value = argparse.Namespace( flavor={"id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62"}, metadata={}) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_called_with( "a75c2fa5-6c03-45a8-bbf7-b993cfcdec27") self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {"hw_rng:allowed": "true"}}, metadata["flavor"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_no_flavor_id_get_by_flavor( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain(metadata=LIBVIRT_METADATA_XML_NO_FLAVOR_ID)]) mock_get_flavor_id.return_value = ( "eba4213d-3c6c-4b5f-8158-dd0022d71d62") dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_called_with("m1.tiny") mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {"hw_rng:allowed": "true"}}, metadata["flavor"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_no_flavor_id_get_by_server( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") self.CONF.set_override("fetch_extra_metadata", True, group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain(metadata=LIBVIRT_METADATA_XML_NO_FLAVOR_ID)]) mock_get_server.return_value = argparse.Namespace( flavor={"id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62"}, metadata={}) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_called_with( "a75c2fa5-6c03-45a8-bbf7-b993cfcdec27") self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {"hw_rng:allowed": "true"}}, metadata["flavor"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_empty_flavor_extra_specs( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain( metadata=LIBVIRT_METADATA_XML_EMPTY_FLAVOR_EXTRA_SPECS)]) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1, "extra_specs": {}}, metadata["flavor"]) @mock.patch.object(discovery.InstanceDiscovery, "get_server") @mock.patch.object(discovery.InstanceDiscovery, "get_flavor_id") @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_no_flavor_extra_specs( self, mock_libvirt_conn, mock_get_flavor_id, mock_get_server): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[FakeDomain( metadata=LIBVIRT_METADATA_XML_NO_FLAVOR_EXTRA_SPECS)]) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) mock_get_flavor_id.assert_not_called() mock_get_server.assert_not_called() self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertEqual("m1.tiny", metadata["instance_type"]) self.assertEqual({"name": "m1.tiny", "id": "eba4213d-3c6c-4b5f-8158-dd0022d71d62", "ram": 512, "disk": 1, "swap": 0, "ephemeral": 0, "vcpus": 1}, metadata["flavor"]) @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_from_volume_image( self, mock_libvirt_conn): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[ FakeDomain(metadata=LIBVIRT_METADATA_XML_FROM_VOLUME_IMAGE)]) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) self.assertEqual("a75c2fa5-6c03-45a8-bbf7-b993cfcdec27", s.resource_id) self.assertEqual("d99c829753f64057bc0f2030da309943", s.project_id) self.assertEqual("a1f4684e58bd4c88aefd2ecb0783b497", s.user_id) metadata = s.resource_metadata self.assertIsNone(metadata["image"]) self.assertIn("image_meta", metadata) self.assertEqual({"base_image_ref", "container_format", "disk_format", "min_disk", "min_ram", "os_distro", "os_type"}, set(metadata["image_meta"].keys())) self.assertEqual("", metadata["image_meta"]["base_image_ref"]) self.assertEqual("bare", metadata["image_meta"]["container_format"]) self.assertEqual("raw", metadata["image_meta"]["disk_format"]) self.assertEqual("1", metadata["image_meta"]["min_disk"]) self.assertEqual("0", metadata["image_meta"]["min_ram"]) self.assertEqual("ubuntu", metadata["image_meta"]["os_distro"]) self.assertEqual("linux", metadata["image_meta"]["os_type"]) @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_from_volume_no_image( self, mock_libvirt_conn): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeConn( domains=[ FakeDomain( metadata=LIBVIRT_METADATA_XML_FROM_VOLUME_NO_IMAGE)]) dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) r = list(resources)[0] s = util.make_sample_from_instance(self.CONF, r, "metric", "delta", "carrot", 1) metadata = s.resource_metadata self.assertIsNone(metadata["image"]) self.assertIn("image_meta", metadata) self.assertEqual({"base_image_ref"}, set(metadata["image_meta"].keys())) self.assertEqual("", metadata["image_meta"]["base_image_ref"]) def test_discovery_with_legacy_resource_cache_cleanup(self): self.CONF.set_override("instance_discovery_method", "naive", group="compute") self.CONF.set_override("resource_update_interval", 600, group="compute") self.CONF.set_override("resource_cache_expiry", 1800, group="compute") dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) self.utc_now.return_value = datetime.datetime( 2016, 1, 1, minute=20, tzinfo=datetime.timezone.utc) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) self.utc_now.return_value = datetime.datetime( 2016, 1, 1, minute=31, tzinfo=datetime.timezone.utc) resources = dsc.discover(mock.MagicMock()) self.assertEqual(1, len(resources)) expected_calls = [mock.call('test', None), mock.call('test', '2016-01-01T00:00:00+00:00'), mock.call('test', None)] self.assertEqual(expected_calls, self.client.instance_get_all_by_host.call_args_list) @mock.patch("ceilometer.compute.virt.libvirt.utils." "refresh_libvirt_connection") def test_discovery_with_libvirt_error(self, mock_libvirt_conn): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") mock_libvirt_conn.return_value = FakeManualInstanceConn() dsc = discovery.InstanceDiscovery(self.CONF) resources = dsc.discover(mock.MagicMock()) self.assertEqual(0, len(resources)) def test_get_flavor_id(self): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") fake_flavor = argparse.Namespace( id="eba4213d-3c6c-4b5f-8158-dd0022d71d62") self.client.nova_client.flavors.find.return_value = fake_flavor dsc = discovery.InstanceDiscovery(self.CONF) self.assertEqual(fake_flavor.id, dsc.get_flavor_id("m1.tiny")) def test_get_flavor_id_notfound(self): self.CONF.set_override("instance_discovery_method", "libvirt_metadata", group="compute") self.client.nova_client.flavors.find.side_effect = ( exceptions.NotFound(404)) dsc = discovery.InstanceDiscovery(self.CONF) self.assertIsNone(dsc.get_flavor_id("m1.tiny")) def test_get_server(self): self.client.nova_client = mock.MagicMock() self.client.nova_client.servers = mock.MagicMock() fake_server = mock.MagicMock() fake_server.metadata = {'metering.server_group': 'group1'} fake_flavor = mock.MagicMock() fake_flavor.id = 'fake_id' fake_server.flavor = fake_flavor self.client.nova_client.servers.get = mock.MagicMock( return_value=fake_server) dsc = discovery.InstanceDiscovery(self.CONF) uuid = '123456' ret_server = dsc.get_server(uuid) self.assertEqual('fake_id', ret_server.flavor.id) self.assertEqual({'metering.server_group': 'group1'}, ret_server.metadata) def test_get_server_notfound(self): self.client.nova_client = mock.MagicMock() self.client.nova_client.servers = mock.MagicMock() self.client.nova_client.servers.get = mock.MagicMock( side_effect=exceptions.NotFound(404)) dsc = discovery.InstanceDiscovery(self.CONF) uuid = '123456' ret_server = dsc.get_server(uuid) self.assertIsNone(ret_server) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2631276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/virt/0000775000175000017500000000000015250267567022246 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2043567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/virt/__init__.py0000664000175000017500000000000015250267452024336 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2631276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/virt/libvirt/0000775000175000017500000000000015250267567023721 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2043567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/virt/libvirt/__init__.py0000664000175000017500000000000015250267452026011 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2053566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/compute/virt/libvirt/test_inspector.py0000664000175000017500000006615615250267452027347 0ustar00zuulzuul# Copyright 2012 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for libvirt inspector.""" from unittest import mock import fixtures from oslo_utils import units from ceilometer.compute.virt import inspector as virt_inspector from ceilometer.compute.virt.libvirt import inspector as libvirt_inspector from ceilometer.compute.virt.libvirt import utils from ceilometer import service from ceilometer.tests import base class FakeLibvirtError(Exception): pass class VMInstance: id = 'ff58e738-12f4-4c58-acde-77617b68da56' name = 'instance-00000001' class TestLibvirtInspection(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.instance = VMInstance() libvirt_inspector.libvirt = mock.Mock() libvirt_inspector.libvirt.getVersion.return_value = 5001001 libvirt_inspector.libvirt.VIR_DOMAIN_SHUTOFF = 5 libvirt_inspector.libvirt.libvirtError = FakeLibvirtError utils.libvirt = libvirt_inspector.libvirt with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=None): self.inspector = libvirt_inspector.LibvirtInspector(conf) def test_inspect_instance_stats(self): domain = mock.Mock() domain.info.return_value = (0, 0, 0, 2, 999999) domain.memoryStats.return_value = {'actual': 54400, 'available': 51200, 'unused': 25600, 'rss': 30000, 'swap_in': 5120, 'swap_out': 8192} conn = mock.Mock() conn.lookupByUUIDString.return_value = domain conn.domainListGetStats.return_value = [({}, { 'cpu.time': 999999, 'vcpu.maximum': 4, 'vcpu.current': 2, 'vcpu.0.time': 10000, 'vcpu.0.wait': 10000, 'vcpu.2.time': 10000, 'vcpu.2.wait': 10000, 'perf.cpu_cycles': 7259361, 'perf.instructions': 8815623, 'perf.cache_references': 74184, 'perf.cache_misses': 16737})] with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertEqual(0, stats.power_state) self.assertEqual(2, stats.cpu_number) self.assertEqual(40000, stats.cpu_time) self.assertEqual(54400 / units.Ki, stats.memory_actual) self.assertEqual(51200 / units.Ki, stats.memory_available) self.assertEqual(25600 / units.Ki, stats.memory_usage) self.assertEqual(30000 / units.Ki, stats.memory_resident) self.assertEqual(5120 / units.Ki, stats.memory_swap_in) self.assertEqual(8192 / units.Ki, stats.memory_swap_out) self.assertEqual(7259361, stats.cpu_cycles) self.assertEqual(8815623, stats.instructions) self.assertEqual(74184, stats.cache_references) self.assertEqual(16737, stats.cache_misses) def test_inspect_instance_stats_fallback_cpu_time(self): domain = mock.Mock() domain.info.return_value = (0, 0, 0, 2, 20000) domain.memoryStats.return_value = {'available': 51200, 'unused': 25600, 'rss': 30000} conn = mock.Mock() conn.lookupByUUIDString.return_value = domain conn.domainListGetStats.return_value = [({}, { 'vcpu.current': 2, 'vcpu.maximum': 4, 'vcpu.0.time': 10000, 'vcpu.1.time': 10000, 'cpu.time': 999999})] with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance) self.assertEqual(2, stats.cpu_number) self.assertEqual(999999, stats.cpu_time) def test_inspect_instance_with_domain_shutoff(self): domain = mock.Mock() domain.info.return_value = (5, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): self.assertRaises(virt_inspector.InstanceShutOffException, self.inspector.inspect_instance, self.instance, None) def test_inspect_instance_with_domain_shutoff_reporting_enabled(self): self.inspector.conf.set_override( 'report_stopped_instance_metrics', True, group='compute') domain = mock.Mock() domain.info.return_value = (5, 2097152, 0, 2, 0) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertEqual(5, stats.power_state) self.assertEqual(2, stats.cpu_number) self.assertEqual(2097152 / units.Ki, stats.memory_actual) self.assertIsNone(stats.cpu_time) self.assertIsNone(stats.memory_available) self.assertIsNone(stats.memory_usage) self.assertIsNone(stats.memory_resident) self.assertIsNone(stats.memory_swap_in) self.assertIsNone(stats.memory_swap_out) self.assertIsNone(stats.cpu_cycles) self.assertIsNone(stats.instructions) self.assertIsNone(stats.cache_references) self.assertIsNone(stats.cache_misses) def test_inspect_vnics(self): dom_xml = """
""" interface_stats = { 'vnet0': (1, 2, 21, 22, 3, 4, 23, 24), 'vnet1': (5, 6, 25, 26, 7, 8, 27, 28), 'vnet2': (9, 10, 29, 30, 11, 12, 31, 32), } interfaceStats = interface_stats.__getitem__ domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.info.return_value = (0, 0, 0, 2, 999999) domain.interfaceStats.side_effect = interfaceStats conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): interfaces = list(self.inspector.inspect_vnics( self.instance, None)) self.assertEqual(3, len(interfaces)) vnic0 = interfaces[0] self.assertEqual('vnet0', vnic0.name) self.assertEqual('fa:16:3e:71:ec:6d', vnic0.mac) self.assertEqual('nova-instance-00000001-fa163e71ec6d', vnic0.fref) self.assertEqual('255.255.255.0', vnic0.parameters.get('projmask')) self.assertEqual('10.0.0.2', vnic0.parameters.get('ip')) self.assertEqual('10.0.0.0', vnic0.parameters.get('projnet')) self.assertEqual('10.0.0.1', vnic0.parameters.get('dhcpserver')) self.assertEqual(1, vnic0.rx_bytes) self.assertEqual(2, vnic0.rx_packets) self.assertEqual(3, vnic0.tx_bytes) self.assertEqual(4, vnic0.tx_packets) self.assertEqual(21, vnic0.rx_errors) self.assertEqual(22, vnic0.rx_drop) self.assertEqual(23, vnic0.tx_errors) self.assertEqual(24, vnic0.tx_drop) vnic1 = interfaces[1] self.assertEqual('vnet1', vnic1.name) self.assertEqual('fa:16:3e:71:ec:6e', vnic1.mac) self.assertEqual('nova-instance-00000001-fa163e71ec6e', vnic1.fref) self.assertEqual('255.255.255.0', vnic1.parameters.get('projmask')) self.assertEqual('192.168.0.2', vnic1.parameters.get('ip')) self.assertEqual('192.168.0.0', vnic1.parameters.get('projnet')) self.assertEqual('192.168.0.1', vnic1.parameters.get('dhcpserver')) self.assertEqual(5, vnic1.rx_bytes) self.assertEqual(6, vnic1.rx_packets) self.assertEqual(7, vnic1.tx_bytes) self.assertEqual(8, vnic1.tx_packets) self.assertEqual(25, vnic1.rx_errors) self.assertEqual(26, vnic1.rx_drop) self.assertEqual(27, vnic1.tx_errors) self.assertEqual(28, vnic1.tx_drop) vnic2 = interfaces[2] self.assertEqual('vnet2', vnic2.name) self.assertEqual('fa:16:3e:96:33:f0', vnic2.mac) self.assertIsNone(vnic2.fref) self.assertEqual( {'interfaceid': None, 'bridge': 'qbr420008b3-7c'}, vnic2.parameters) self.assertEqual(9, vnic2.rx_bytes) self.assertEqual(10, vnic2.rx_packets) self.assertEqual(11, vnic2.tx_bytes) self.assertEqual(12, vnic2.tx_packets) self.assertEqual(29, vnic2.rx_errors) self.assertEqual(30, vnic2.rx_drop) self.assertEqual(31, vnic2.tx_errors) self.assertEqual(32, vnic2.tx_drop) def test_inspect_vnics_with_domain_shutoff(self): domain = mock.Mock() domain.info.return_value = (5, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): inspect = self.inspector.inspect_vnics self.assertRaises(virt_inspector.InstanceShutOffException, list, inspect(self.instance, None)) def test_inspect_disks(self): dom_xml = """
""" blockStatsFlags = {'wr_total_times': 91752302267, 'rd_operations': 6756, 'flush_total_times': 1310427331, 'rd_total_times': 29142253616, 'rd_bytes': 171460096, 'flush_operations': 746, 'wr_operations': 1437, 'wr_bytes': 13574656} domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.info.return_value = (0, 0, 0, 2, 999999) domain.blockStats.return_value = (1, 2, 3, 4, -1) domain.blockStatsFlags.return_value = blockStatsFlags conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disks(self.instance, None)) self.assertEqual(1, len(disks)) self.assertEqual('vda', disks[0].device) self.assertEqual(1, disks[0].read_requests) self.assertEqual(2, disks[0].read_bytes) self.assertEqual(3, disks[0].write_requests) self.assertEqual(4, disks[0].write_bytes) self.assertEqual(91752302267, disks[0].wr_total_times) self.assertEqual(29142253616, disks[0].rd_total_times) def test_inspect_disks_with_domain_shutoff(self): domain = mock.Mock() domain.info.return_value = (5, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): inspect = self.inspector.inspect_disks self.assertRaises(virt_inspector.InstanceShutOffException, list, inspect(self.instance, None)) def test_inspect_disk_info(self): dom_xml = """
""" domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.blockInfo.return_value = (1, 2, 3, -1) domain.info.return_value = (0, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disk_info( self.instance, None)) self.assertEqual(1, len(disks)) self.assertEqual('vda', disks[0].device) self.assertEqual(3, disks[0].capacity) self.assertEqual(2, disks[0].allocation) self.assertEqual(3, disks[0].physical) def test_inspect_disk_info_with_domain_shutoff(self): self.inspector.conf.set_override( 'report_stopped_instance_metrics', True, group='compute') dom_xml = """
""" domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.blockInfo.return_value = (10737418240, 2147483648, 3221225472, -1) domain.info.return_value = (5, 0, 0, 2, 0) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disk_info( self.instance, None)) self.assertEqual(1, len(disks)) self.assertEqual('vda', disks[0].device) self.assertEqual(10737418240, disks[0].capacity) self.assertEqual(2147483648, disks[0].allocation) self.assertEqual(3221225472, disks[0].physical) def test_inspect_disk_info_network_type(self): dom_xml = """
""" domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.blockInfo.return_value = (1, 2, 3, -1) domain.info.return_value = (0, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disk_info(self.instance, None)) self.assertEqual(1, len(disks)) def test_inspect_disk_info_without_source_element(self): dom_xml = """
""" domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.blockInfo.return_value = (1, 2, 3, -1) domain.info.return_value = (0, 0, 0, 2, 999999) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disk_info(self.instance, None)) self.assertEqual(0, len(disks)) def test_inspect_disks_without_source_element(self): dom_xml = """
""" blockStatsFlags = {'wr_total_times': 91752302267, 'rd_operations': 6756, 'flush_total_times': 1310427331, 'rd_total_times': 29142253616, 'rd_bytes': 171460096, 'flush_operations': 746, 'wr_operations': 1437, 'wr_bytes': 13574656} domain = mock.Mock() domain.XMLDesc.return_value = dom_xml domain.info.return_value = (0, 0, 0, 2, 999999) domain.blockStats.return_value = (1, 2, 3, 4, -1) domain.blockStatsFlags.return_value = blockStatsFlags conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): disks = list(self.inspector.inspect_disks(self.instance, None)) self.assertEqual(0, len(disks)) def test_inspect_memory_with_domain_shutoff(self): self.inspector.conf.set_override( 'report_stopped_instance_metrics', True, group='compute') domain = mock.Mock() domain.info.return_value = (5, 524288, 51200, 4, 0) conn = mock.Mock() conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertEqual(5, stats.power_state) self.assertEqual(4, stats.cpu_number) self.assertEqual(524288 / units.Ki, stats.memory_actual) self.assertIsNone(stats.memory_usage) self.assertIsNone(stats.memory_available) self.assertIsNone(stats.memory_resident) def test_inspect_memory_with_empty_stats(self): domain = mock.Mock() domain.info.return_value = (0, 0, 51200, 2, 999999) domain.memoryStats.return_value = {} conn = mock.Mock() conn.domainListGetStats.return_value = [({}, {})] conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertIsNone(stats.memory_actual) self.assertIsNone(stats.memory_available) self.assertIsNone(stats.memory_usage) self.assertIsNone(stats.memory_resident) self.assertIsNone(stats.memory_swap_in) self.assertIsNone(stats.memory_swap_out) def test_inspect_memory_with_usable(self): domain = mock.Mock() domain.info.return_value = (0, 0, 0, 2, 999999) domain.memoryStats.return_value = {'actual': 80000, 'available': 76800, 'rss': 30000, 'swap_in': 5120, 'swap_out': 8192, 'unused': 25600, 'usable': 51200} conn = mock.Mock() conn.domainListGetStats.return_value = [({}, {})] conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertEqual(80000 / units.Ki, stats.memory_actual) self.assertEqual(76800 / units.Ki, stats.memory_available) self.assertEqual(25600 / units.Ki, stats.memory_usage) self.assertEqual(30000 / units.Ki, stats.memory_resident) self.assertEqual(5120 / units.Ki, stats.memory_swap_in) self.assertEqual(8192 / units.Ki, stats.memory_swap_out) def test_inspect_perf_events_libvirt_less_than_2_3_0(self): domain = mock.Mock() domain.info.return_value = (0, 0, 51200, 2, 999999) domain.memoryStats.return_value = {'rss': 0, 'available': 51200, 'unused': 25600} conn = mock.Mock() conn.domainListGetStats.return_value = [({}, {})] conn.lookupByUUIDString.return_value = domain with mock.patch('ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', return_value=conn): stats = self.inspector.inspect_instance(self.instance, None) self.assertIsNone(stats.cpu_cycles) self.assertIsNone(stats.instructions) self.assertIsNone(stats.cache_references) self.assertIsNone(stats.cache_misses) class TestLibvirtInspectionWithError(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.useFixture(fixtures.MonkeyPatch( 'ceilometer.compute.virt.libvirt.utils.' 'refresh_libvirt_connection', mock.MagicMock(side_effect=[None, Exception('dummy')]))) libvirt_inspector.libvirt = mock.Mock() libvirt_inspector.libvirt.libvirtError = FakeLibvirtError utils.libvirt = libvirt_inspector.libvirt self.inspector = libvirt_inspector.LibvirtInspector(conf) def test_inspect_unknown_error(self): self.assertRaises(virt_inspector.InspectorException, self.inspector.inspect_instance, 'foo', None) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2641275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/dns/0000775000175000017500000000000015250267567020372 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2053566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/dns/__init__.py0000664000175000017500000000000015250267452022462 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2053566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/dns/test_designate.py0000664000175000017500000001303415250267452023740 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.dns import designate from ceilometer.dns import discovery from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes class _BaseTestDNSPollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestZoneStatusPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() self.pollster = designate.ZoneStatusPollster(self.CONF) def test_zone_get_samples(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual(len(resources), len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(resources[0], field), samples[0].resource_metadata[field]) def test_zone_status_volume(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) # ACTIVE = 1, PENDING = 2, ERROR = 3 self.assertEqual(1, samples[0].volume) self.assertEqual(2, samples[1].volume) self.assertEqual(3, samples[2].volume) def test_get_zone_meter_names(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual({'dns.zone.status'}, {s.name for s in samples}) def test_zone_discovery(self): discovered_zones = discovery.ZoneDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(fakes.FakeSDKDesignateClient.default_zones), len(list(discovered_zones))) class TestZoneRecordsetCountPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() self.pollster = designate.ZoneRecordsetCountPollster(self.CONF) def test_zone_recordsets_volume(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) # Each zone should have the mocked number of recordsets num_recordsets = len(fakes.FakeSDKDesignateClient.default_recordsets) for sample in samples: self.assertEqual(num_recordsets, sample.volume) def test_get_zone_meter_names(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual({'dns.zone.recordsets'}, {s.name for s in samples}) class TestZoneTTLPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() self.pollster = designate.ZoneTTLPollster(self.CONF) def test_zone_ttl_volume(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual(3600, samples[0].volume) self.assertEqual(7200, samples[1].volume) self.assertEqual(1800, samples[2].volume) def test_get_zone_meter_names(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual({'dns.zone.ttl'}, {s.name for s in samples}) class TestZoneSerialPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() self.pollster = designate.ZoneSerialPollster(self.CONF) def test_zone_serial_volume(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual(1234567890, samples[0].volume) self.assertEqual(1234567891, samples[1].volume) self.assertEqual(1234567892, samples[2].volume) def test_get_zone_meter_names(self): resources = fakes.FakeSDKDesignateClient.default_zones samples = list(self.pollster.get_samples( self.manager, {}, resources=resources)) self.assertEqual({'dns.zone.serial'}, {s.name for s in samples}) class TestZonePollsterUnknownStatus(_BaseTestDNSPollster): def test_unknown_zone_status(self): pollster = designate.ZoneStatusPollster(self.CONF) samples = list(pollster.get_samples( self.manager, {}, resources=[fakes.ZONE_UNKNOWN_STATUS])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2651277 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/event/0000775000175000017500000000000015250267567020727 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2053566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/event/__init__.py0000664000175000017500000000000015250267452023017 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2063568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/event/test_converter.py0000664000175000017500000010024315250267452024340 0ustar00zuulzuul# # Copyright 2013 Rackspace Hosting. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime from unittest import mock import jsonpath_rw_ext from ceilometer import declarative from ceilometer.event import converter from ceilometer.event import models from ceilometer import service as ceilometer_service from ceilometer.tests import base class ConverterBase(base.BaseTestCase): @staticmethod def _create_test_notification(event_type, message_id, **kw): return dict(event_type=event_type, metadata=dict(message_id=message_id, timestamp="2013-08-08 21:06:37.803826"), publisher_id="compute.host-1-2-3", payload=kw, ) def assertIsValidEvent(self, event, notification): self.assertIsNotNone( event, "Notification dropped unexpectedly:" " %s" % str(notification)) self.assertIsInstance(event, models.Event) def assertIsNotValidEvent(self, event, notification): self.assertIsNone( event, "Notification NOT dropped when expected to be dropped:" " %s" % str(notification)) def assertHasTrait(self, event, name, value=None, dtype=None): traits = [trait for trait in event.traits if trait.name == name] self.assertGreater( len(traits), 0, f"Trait {name} not found in event {event}") trait = traits[0] if value is not None: self.assertEqual(value, trait.value) if dtype is not None: self.assertEqual(dtype, trait.dtype) if dtype == models.Trait.INT_TYPE: self.assertIsInstance(trait.value, int) elif dtype == models.Trait.FLOAT_TYPE: self.assertIsInstance(trait.value, float) elif dtype == models.Trait.DATETIME_TYPE: self.assertIsInstance(trait.value, datetime.datetime) elif dtype == models.Trait.TEXT_TYPE: self.assertIsInstance(trait.value, str) def assertDoesNotHaveTrait(self, event, name): traits = [trait for trait in event.traits if trait.name == name] self.assertEqual( len(traits), 0, f"Extra Trait {name} found in event {event}") def assertHasDefaultTraits(self, event): text = models.Trait.TEXT_TYPE self.assertHasTrait(event, 'service', dtype=text) def _cmp_tree(self, this, other): if hasattr(this, 'right') and hasattr(other, 'right'): return (self._cmp_tree(this.right, other.right) and self._cmp_tree(this.left, other.left)) if not hasattr(this, 'right') and not hasattr(other, 'right'): return this == other return False def assertPathsEqual(self, path1, path2): self.assertTrue(self._cmp_tree(path1, path2), f'JSONPaths not equivalent {path1} {path2}') class TestTraitDefinition(ConverterBase): def setUp(self): super().setUp() self.n1 = self._create_test_notification( "test.thing", "uuid-for-notif-0001", instance_uuid="uuid-for-instance-0001", instance_id="id-for-instance-0001", instance_uuid2=None, instance_id2=None, host='host-1-2-3', bogus_date='', image_meta=dict( disk_gb='20', thing='whatzit'), foobar=50) self.ext1 = mock.MagicMock(name='mock_test_plugin') self.test_plugin_class = self.ext1.plugin self.test_plugin = self.test_plugin_class() self.test_plugin.trait_values.return_value = ['foobar'] self.ext1.reset_mock() self.ext2 = mock.MagicMock(name='mock_nothing_plugin') self.nothing_plugin_class = self.ext2.plugin self.nothing_plugin = self.nothing_plugin_class() self.nothing_plugin.trait_values.return_value = [None] self.ext2.reset_mock() self.fake_plugin_mgr = dict(test=self.ext1, nothing=self.ext2) def test_to_trait_with_plugin(self): cfg = dict(type='text', fields=['payload.instance_id', 'payload.instance_uuid'], plugin=dict(name='test')) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('test_trait', t.name) self.assertEqual(models.Trait.TEXT_TYPE, t.dtype) self.assertEqual('foobar', t.value) self.test_plugin_class.assert_called_once_with() self.test_plugin.trait_values.assert_called_once_with([ ('payload.instance_id', 'id-for-instance-0001'), ('payload.instance_uuid', 'uuid-for-instance-0001')]) def test_to_trait_null_match_with_plugin(self): cfg = dict(type='text', fields=['payload.nothere', 'payload.bogus'], plugin=dict(name='test')) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('test_trait', t.name) self.assertEqual(models.Trait.TEXT_TYPE, t.dtype) self.assertEqual('foobar', t.value) self.test_plugin_class.assert_called_once_with() self.test_plugin.trait_values.assert_called_once_with([]) def test_to_trait_with_plugin_null(self): cfg = dict(type='text', fields=['payload.instance_id', 'payload.instance_uuid'], plugin=dict(name='nothing')) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsNone(t) self.nothing_plugin_class.assert_called_once_with() self.nothing_plugin.trait_values.assert_called_once_with([ ('payload.instance_id', 'id-for-instance-0001'), ('payload.instance_uuid', 'uuid-for-instance-0001')]) def test_to_trait_with_plugin_with_parameters(self): cfg = dict(type='text', fields=['payload.instance_id', 'payload.instance_uuid'], plugin=dict(name='test', parameters=dict(a=1, b='foo'))) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('test_trait', t.name) self.assertEqual(models.Trait.TEXT_TYPE, t.dtype) self.assertEqual('foobar', t.value) self.test_plugin_class.assert_called_once_with(a=1, b='foo') self.test_plugin.trait_values.assert_called_once_with([ ('payload.instance_id', 'id-for-instance-0001'), ('payload.instance_uuid', 'uuid-for-instance-0001')]) def test_to_trait(self): cfg = dict(type='text', fields='payload.instance_id') tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('test_trait', t.name) self.assertEqual(models.Trait.TEXT_TYPE, t.dtype) self.assertEqual('id-for-instance-0001', t.value) cfg = dict(type='int', fields='payload.image_meta.disk_gb') tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('test_trait', t.name) self.assertEqual(models.Trait.INT_TYPE, t.dtype) self.assertEqual(20, t.value) def test_to_trait_multiple(self): cfg = dict(type='text', fields=['payload.instance_id', 'payload.instance_uuid']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('id-for-instance-0001', t.value) cfg = dict(type='text', fields=['payload.instance_uuid', 'payload.instance_id']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('uuid-for-instance-0001', t.value) def test_to_trait_multiple_different_nesting(self): cfg = dict(type='int', fields=['payload.foobar', 'payload.image_meta.disk_gb']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual(50, t.value) cfg = dict(type='int', fields=['payload.image_meta.disk_gb', 'payload.foobar']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual(20, t.value) def test_to_trait_some_null_multiple(self): cfg = dict(type='text', fields=['payload.instance_id2', 'payload.instance_uuid']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('uuid-for-instance-0001', t.value) def test_to_trait_some_missing_multiple(self): cfg = dict(type='text', fields=['payload.not_here_boss', 'payload.instance_uuid']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsInstance(t, models.Trait) self.assertEqual('uuid-for-instance-0001', t.value) def test_to_trait_missing(self): cfg = dict(type='text', fields='payload.not_here_boss') tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsNone(t) def test_to_trait_null(self): cfg = dict(type='text', fields='payload.instance_id2') tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsNone(t) def test_to_trait_empty_nontext(self): cfg = dict(type='datetime', fields='payload.bogus_date') tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsNone(t) def test_to_trait_multiple_null_missing(self): cfg = dict(type='text', fields=['payload.not_here_boss', 'payload.instance_id2']) tdef = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) t = tdef.to_trait(self.n1) self.assertIsNone(t) def test_missing_fields_config(self): self.assertRaises(declarative.DefinitionException, converter.TraitDefinition, 'bogus_trait', dict(), self.fake_plugin_mgr) def test_string_fields_config(self): cfg = dict(fields='payload.test') t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertPathsEqual(t.getter.__self__, jsonpath_rw_ext.parse('payload.test')) def test_list_fields_config(self): cfg = dict(fields=['payload.test', 'payload.other']) t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertPathsEqual( t.getter.__self__, jsonpath_rw_ext.parse('(payload.test)|(payload.other)')) def test_invalid_path_config(self): # test invalid jsonpath... cfg = dict(fields='payload.bogus(') self.assertRaises(declarative.DefinitionException, converter.TraitDefinition, 'bogus_trait', cfg, self.fake_plugin_mgr) def test_invalid_plugin_config(self): # test invalid jsonpath... cfg = dict(fields='payload.test', plugin=dict(bogus="true")) self.assertRaises(declarative.DefinitionException, converter.TraitDefinition, 'test_trait', cfg, self.fake_plugin_mgr) def test_unknown_plugin(self): # test invalid jsonpath... cfg = dict(fields='payload.test', plugin=dict(name='bogus')) self.assertRaises(declarative.DefinitionException, converter.TraitDefinition, 'test_trait', cfg, self.fake_plugin_mgr) def test_type_config(self): cfg = dict(type='text', fields='payload.test') t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertEqual(models.Trait.TEXT_TYPE, t.trait_type) cfg = dict(type='int', fields='payload.test') t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertEqual(models.Trait.INT_TYPE, t.trait_type) cfg = dict(type='float', fields='payload.test') t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertEqual(models.Trait.FLOAT_TYPE, t.trait_type) cfg = dict(type='datetime', fields='payload.test') t = converter.TraitDefinition('test_trait', cfg, self.fake_plugin_mgr) self.assertEqual(models.Trait.DATETIME_TYPE, t.trait_type) def test_invalid_type_config(self): # test invalid jsonpath... cfg = dict(type='bogus', fields='payload.test') self.assertRaises(declarative.DefinitionException, converter.TraitDefinition, 'bogus_trait', cfg, self.fake_plugin_mgr) class TestEventDefinition(ConverterBase): def setUp(self): super().setUp() self.traits_cfg = { 'instance_id': { 'type': 'text', 'fields': ['payload.instance_uuid', 'payload.instance_id'], }, 'host': { 'type': 'text', 'fields': 'payload.host', }, } self.test_notification1 = self._create_test_notification( "test.thing", "uuid-for-notif-0001", instance_id="uuid-for-instance-0001", host='host-1-2-3') self.test_notification2 = self._create_test_notification( "test.thing", "uuid-for-notif-0002", instance_id="uuid-for-instance-0002") self.test_notification3 = self._create_test_notification( "test.thing", "uuid-for-notif-0003", instance_id="uuid-for-instance-0003", host=None) self.fake_plugin_mgr = {} def test_to_event(self): dtype = models.Trait.TEXT_TYPE cfg = dict(event_type='test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) e = edef.to_event('INFO', self.test_notification1) self.assertEqual('test.thing', e.event_type) self.assertEqual(datetime.datetime(2013, 8, 8, 21, 6, 37, 803826), e.generated) self.assertHasDefaultTraits(e) self.assertHasTrait(e, 'host', value='host-1-2-3', dtype=dtype) self.assertHasTrait(e, 'instance_id', value='uuid-for-instance-0001', dtype=dtype) def test_to_event_missing_trait(self): dtype = models.Trait.TEXT_TYPE cfg = dict(event_type='test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) e = edef.to_event('INFO', self.test_notification2) self.assertHasDefaultTraits(e) self.assertHasTrait(e, 'instance_id', value='uuid-for-instance-0002', dtype=dtype) self.assertDoesNotHaveTrait(e, 'host') def test_to_event_null_trait(self): dtype = models.Trait.TEXT_TYPE cfg = dict(event_type='test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) e = edef.to_event('INFO', self.test_notification3) self.assertHasDefaultTraits(e) self.assertHasTrait(e, 'instance_id', value='uuid-for-instance-0003', dtype=dtype) self.assertDoesNotHaveTrait(e, 'host') def test_bogus_cfg_no_traits(self): bogus = dict(event_type='test.foo') self.assertRaises(declarative.DefinitionException, converter.EventDefinition, bogus, self.fake_plugin_mgr, []) def test_bogus_cfg_no_type(self): bogus = dict(traits=self.traits_cfg) self.assertRaises(declarative.DefinitionException, converter.EventDefinition, bogus, self.fake_plugin_mgr, []) def test_included_type_string(self): cfg = dict(event_type='test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertEqual(1, len(edef._included_types)) self.assertEqual('test.thing', edef._included_types[0]) self.assertEqual(0, len(edef._excluded_types)) self.assertTrue(edef.included_type('test.thing')) self.assertFalse(edef.excluded_type('test.thing')) self.assertTrue(edef.match_type('test.thing')) self.assertFalse(edef.match_type('random.thing')) def test_included_type_list(self): cfg = dict(event_type=['test.thing', 'other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertEqual(2, len(edef._included_types)) self.assertEqual(0, len(edef._excluded_types)) self.assertTrue(edef.included_type('test.thing')) self.assertTrue(edef.included_type('other.thing')) self.assertFalse(edef.excluded_type('test.thing')) self.assertTrue(edef.match_type('test.thing')) self.assertTrue(edef.match_type('other.thing')) self.assertFalse(edef.match_type('random.thing')) def test_excluded_type_string(self): cfg = dict(event_type='!test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertEqual(1, len(edef._included_types)) self.assertEqual('*', edef._included_types[0]) self.assertEqual('test.thing', edef._excluded_types[0]) self.assertEqual(1, len(edef._excluded_types)) self.assertEqual('test.thing', edef._excluded_types[0]) self.assertTrue(edef.excluded_type('test.thing')) self.assertTrue(edef.included_type('random.thing')) self.assertFalse(edef.match_type('test.thing')) self.assertTrue(edef.match_type('random.thing')) def test_excluded_type_list(self): cfg = dict(event_type=['!test.thing', '!other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertEqual(1, len(edef._included_types)) self.assertEqual(2, len(edef._excluded_types)) self.assertTrue(edef.excluded_type('test.thing')) self.assertTrue(edef.excluded_type('other.thing')) self.assertFalse(edef.excluded_type('random.thing')) self.assertFalse(edef.match_type('test.thing')) self.assertFalse(edef.match_type('other.thing')) self.assertTrue(edef.match_type('random.thing')) def test_mixed_type_list(self): cfg = dict(event_type=['*.thing', '!test.thing', '!other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertEqual(1, len(edef._included_types)) self.assertEqual(2, len(edef._excluded_types)) self.assertTrue(edef.excluded_type('test.thing')) self.assertTrue(edef.excluded_type('other.thing')) self.assertFalse(edef.excluded_type('random.thing')) self.assertFalse(edef.match_type('test.thing')) self.assertFalse(edef.match_type('other.thing')) self.assertFalse(edef.match_type('random.whatzit')) self.assertTrue(edef.match_type('random.thing')) def test_catchall(self): cfg = dict(event_type=['*.thing', '!test.thing', '!other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertFalse(edef.is_catchall) cfg = dict(event_type=['!other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertFalse(edef.is_catchall) cfg = dict(event_type=['other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertFalse(edef.is_catchall) cfg = dict(event_type=['*', '!other.thing'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertFalse(edef.is_catchall) cfg = dict(event_type=['*'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertTrue(edef.is_catchall) cfg = dict(event_type=['*', 'foo'], traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) self.assertTrue(edef.is_catchall) def test_default_traits(self): cfg = dict(event_type='test.thing', traits={}) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) default_traits = converter.EventDefinition.DEFAULT_TRAITS.keys() traits = set(edef.traits.keys()) for dt in default_traits: self.assertIn(dt, traits) self.assertEqual(len(converter.EventDefinition.DEFAULT_TRAITS), len(edef.traits)) def test_traits(self): cfg = dict(event_type='test.thing', traits=self.traits_cfg) edef = converter.EventDefinition(cfg, self.fake_plugin_mgr, []) default_traits = converter.EventDefinition.DEFAULT_TRAITS.keys() traits = set(edef.traits.keys()) for dt in default_traits: self.assertIn(dt, traits) self.assertIn('host', traits) self.assertIn('instance_id', traits) self.assertEqual(len(converter.EventDefinition.DEFAULT_TRAITS) + 2, len(edef.traits)) class TestNotificationConverter(ConverterBase): def setUp(self): super().setUp() self.CONF = ceilometer_service.prepare_service([], []) self.valid_event_def1 = [{ 'event_type': 'compute.instance.create.*', 'traits': { 'instance_id': { 'type': 'text', 'fields': ['payload.instance_uuid', 'payload.instance_id'], }, 'host': { 'type': 'text', 'fields': 'payload.host', }, }, }] self.test_notification1 = self._create_test_notification( "compute.instance.create.start", "uuid-for-notif-0001", instance_id="uuid-for-instance-0001", host='host-1-2-3') self.test_notification2 = self._create_test_notification( "bogus.notification.from.mars", "uuid-for-notif-0002", weird='true', host='cydonia') self.fake_plugin_mgr = {} @mock.patch('oslo_utils.timeutils.utcnow') def test_converter_missing_keys(self, mock_utcnow): self.CONF.set_override('drop_unmatched_notifications', False, group='event') # test a malformed notification now = datetime.datetime.utcnow() mock_utcnow.return_value = now c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) message = {'event_type': "foo", 'metadata': {'message_id': "abc", 'timestamp': str(now)}, 'publisher_id': "1"} e = c.to_event('INFO', message) self.assertIsValidEvent(e, message) self.assertEqual(1, len(e.traits)) self.assertEqual("foo", e.event_type) self.assertEqual(now, e.generated) def test_converter_with_catchall(self): self.CONF.set_override('drop_unmatched_notifications', False, group='event') c = converter.NotificationEventsConverter( self.CONF, self.valid_event_def1, self.fake_plugin_mgr) self.assertEqual(2, len(c.definitions)) e = c.to_event('INFO', self.test_notification1) self.assertIsValidEvent(e, self.test_notification1) self.assertEqual(3, len(e.traits)) self.assertHasDefaultTraits(e) self.assertHasTrait(e, 'instance_id') self.assertHasTrait(e, 'host') e = c.to_event('INFO', self.test_notification2) self.assertIsValidEvent(e, self.test_notification2) self.assertEqual(1, len(e.traits)) self.assertHasDefaultTraits(e) self.assertDoesNotHaveTrait(e, 'instance_id') self.assertDoesNotHaveTrait(e, 'host') def test_converter_without_catchall(self): self.CONF.set_override('drop_unmatched_notifications', True, group='event') c = converter.NotificationEventsConverter( self.CONF, self.valid_event_def1, self.fake_plugin_mgr) self.assertEqual(1, len(c.definitions)) e = c.to_event('INFO', self.test_notification1) self.assertIsValidEvent(e, self.test_notification1) self.assertEqual(3, len(e.traits)) self.assertHasDefaultTraits(e) self.assertHasTrait(e, 'instance_id') self.assertHasTrait(e, 'host') e = c.to_event('INFO', self.test_notification2) self.assertIsNotValidEvent(e, self.test_notification2) def test_converter_empty_cfg_with_catchall(self): self.CONF.set_override('drop_unmatched_notifications', False, group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertEqual(1, len(c.definitions)) e = c.to_event('INFO', self.test_notification1) self.assertIsValidEvent(e, self.test_notification1) self.assertEqual(1, len(e.traits)) self.assertHasDefaultTraits(e) e = c.to_event('INFO', self.test_notification2) self.assertIsValidEvent(e, self.test_notification2) self.assertEqual(1, len(e.traits)) self.assertHasDefaultTraits(e) def test_converter_empty_cfg_without_catchall(self): self.CONF.set_override('drop_unmatched_notifications', True, group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertEqual(0, len(c.definitions)) e = c.to_event('INFO', self.test_notification1) self.assertIsNotValidEvent(e, self.test_notification1) e = c.to_event('INFO', self.test_notification2) self.assertIsNotValidEvent(e, self.test_notification2) @staticmethod def _convert_message(convert, level): message = {'priority': level, 'event_type': "foo", 'publisher_id': "1", 'metadata': {'message_id': "abc", 'timestamp': "2013-08-08 21:06:37.803826"}} return convert.to_event(level, message) def test_store_raw_all(self): self.CONF.set_override('store_raw', ['info', 'error'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertTrue(self._convert_message(c, 'info').raw) self.assertTrue(self._convert_message(c, 'error').raw) def test_store_raw_info_only(self): self.CONF.set_override('store_raw', ['info'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertTrue(self._convert_message(c, 'info').raw) self.assertFalse(self._convert_message(c, 'error').raw) def test_store_raw_error_only(self): self.CONF.set_override('store_raw', ['error'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertFalse(self._convert_message(c, 'info').raw) self.assertTrue(self._convert_message(c, 'error').raw) def test_store_raw_skip_all(self): c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertFalse(self._convert_message(c, 'info').raw) self.assertFalse(self._convert_message(c, 'error').raw) def test_store_raw_info_only_no_case(self): self.CONF.set_override('store_raw', ['INFO'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertTrue(self._convert_message(c, 'info').raw) self.assertFalse(self._convert_message(c, 'error').raw) def test_store_raw_bad_skip_all(self): self.CONF.set_override('store_raw', ['unknown'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertFalse(self._convert_message(c, 'info').raw) self.assertFalse(self._convert_message(c, 'error').raw) def test_store_raw_bad_and_good(self): self.CONF.set_override('store_raw', ['info', 'unknown'], group='event') c = converter.NotificationEventsConverter( self.CONF, [], self.fake_plugin_mgr) self.assertTrue(self._convert_message(c, 'info').raw) self.assertFalse(self._convert_message(c, 'error').raw) @mock.patch('ceilometer.declarative.LOG') def test_setup_events_load_config_in_code_tree(self, mocked_log): self.CONF.set_override('definitions_cfg_file', '/not/existing/file', group='event') self.CONF.set_override('drop_unmatched_notifications', False, group='event') c = converter.setup_events(self.CONF, self.fake_plugin_mgr) self.assertIsInstance(c, converter.NotificationEventsConverter) log_called_args = mocked_log.debug.call_args_list self.assertEqual( 'No Definitions configuration file found! Using default config.', log_called_args[0][0][0]) self.assertTrue(log_called_args[1][0][0].startswith( 'Loading definitions configuration file:')) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2073567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/event/test_endpoint.py0000664000175000017500000001642015250267452024154 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for Ceilometer notify daemon.""" from unittest import mock import fixtures import oslo_messaging from oslo_utils import fileutils import yaml from ceilometer.pipeline import event as event_pipe from ceilometer import publisher from ceilometer.publisher import test from ceilometer import service from ceilometer.tests import base as tests_base TEST_NOTICE_CTXT = { 'auth_token': '3d8b13de1b7d499587dfc69b77dc09c2', 'is_admin': True, 'project_id': '7c150a59fe714e6f9263774af9688f0e', 'quota_class': None, 'read_deleted': 'no', 'remote_address': '10.0.2.15', 'request_id': 'req-d68b36e0-9233-467f-9afb-d81435d64d66', 'roles': ['admin'], 'timestamp': '2012-05-08T20:23:41.425105', 'user_id': '1e3ce043029547f1a61c1996d1a531a2', } TEST_NOTICE_METADATA = { 'message_id': 'dae6f69c-00e0-41c0-b371-41ec3b7f4451', 'timestamp': '2012-05-08 20:23:48.028195', } TEST_NOTICE_PAYLOAD = { 'created_at': '2012-05-08 20:23:41', 'deleted_at': '', 'disk_gb': 0, 'display_name': 'testme', 'fixed_ips': [{'address': '10.0.0.2', 'floating_ips': [], 'meta': {}, 'type': 'fixed', 'version': 4}], 'image_ref_url': 'http://10.0.2.15:9292/images/UUID', 'instance_id': '9f9d01b9-4a58-4271-9e27-398b21ab20d1', 'instance_type': 'm1.tiny', 'instance_type_id': 2, 'launched_at': '2012-05-08 20:23:47.985999', 'memory_mb': 512, 'state': 'active', 'state_description': '', 'tenant_id': '7c150a59fe714e6f9263774af9688f0e', 'user_id': '1e3ce043029547f1a61c1996d1a531a2', 'reservation_id': '1e3ce043029547f1a61c1996d1a531a3', 'vcpus': 1, 'root_gb': 0, 'ephemeral_gb': 0, 'host': 'compute-host-name', 'availability_zone': '1e3ce043029547f1a61c1996d1a531a4', 'os_type': 'linux?', 'architecture': 'x86', 'image_ref': 'UUID', 'kernel_id': '1e3ce043029547f1a61c1996d1a531a5', 'ramdisk_id': '1e3ce043029547f1a61c1996d1a531a6', } class TestEventEndpoint(tests_base.BaseTestCase): @staticmethod def get_publisher(conf, url, namespace=''): fake_drivers = {'test://': test.TestPublisher, 'except://': test.TestPublisher} return fake_drivers[url](conf, url) def _setup_pipeline(self, publishers): ev_pipeline = yaml.dump({ 'sources': [{ 'name': 'test_event', 'events': ['test.test'], 'sinks': ['test_sink'] }], 'sinks': [{ 'name': 'test_sink', 'publishers': publishers }] }) ev_pipeline = ev_pipeline.encode('utf-8') ev_pipeline_cfg_file = fileutils.write_to_tempfile( content=ev_pipeline, prefix="event_pipeline", suffix="yaml") self.CONF.set_override('event_pipeline_cfg_file', ev_pipeline_cfg_file) ev_pipeline_mgr = event_pipe.EventPipelineManager(self.CONF) return ev_pipeline_mgr def _setup_endpoint(self, publishers): ev_pipeline_mgr = self._setup_pipeline(publishers) self.endpoint = event_pipe.EventEndpoint( ev_pipeline_mgr.conf, ev_pipeline_mgr.publisher()) self.endpoint.event_converter = mock.MagicMock() self.endpoint.event_converter.to_event.return_value = mock.MagicMock( event_type='test.test') def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.setup_messaging(self.CONF) self.useFixture(fixtures.MockPatchObject( publisher, 'get_publisher', side_effect=self.get_publisher)) self.fake_publisher = mock.Mock() self.useFixture(fixtures.MockPatch( 'ceilometer.publisher.test.TestPublisher', return_value=self.fake_publisher)) def test_message_to_event(self): self._setup_endpoint(['test://']) self.endpoint.info([{'ctxt': TEST_NOTICE_CTXT, 'publisher_id': 'compute.vagrant-precise', 'event_type': 'compute.instance.create.end', 'payload': TEST_NOTICE_PAYLOAD, 'metadata': TEST_NOTICE_METADATA}]) def test_bad_event_non_ack_and_requeue(self): self._setup_endpoint(['test://']) self.fake_publisher.publish_events.side_effect = Exception self.CONF.set_override("ack_on_event_error", False, group="notification") ret = self.endpoint.info([{'ctxt': TEST_NOTICE_CTXT, 'publisher_id': 'compute.vagrant-precise', 'event_type': 'compute.instance.create.end', 'payload': TEST_NOTICE_PAYLOAD, 'metadata': TEST_NOTICE_METADATA}]) self.assertEqual(oslo_messaging.NotificationResult.REQUEUE, ret) def test_message_to_event_bad_event(self): self._setup_endpoint(['test://']) self.fake_publisher.publish_events.side_effect = Exception self.CONF.set_override("ack_on_event_error", False, group="notification") message = { 'payload': {'event_type': "foo", 'message_id': "abc"}, 'metadata': {}, 'ctxt': {} } with mock.patch("ceilometer.pipeline.event.LOG") as mock_logger: ret = self.endpoint.process_notifications('info', [message]) self.assertEqual(oslo_messaging.NotificationResult.REQUEUE, ret) exception_mock = mock_logger.exception self.assertIn('Exit after error from publisher', exception_mock.call_args_list[0][0][0] % exception_mock.call_args_list[0][0][1]) def test_message_to_event_bad_event_multi_publish(self): self._setup_endpoint(['test://', 'except://']) self.fake_publisher.publish_events.side_effect = Exception self.CONF.set_override("ack_on_event_error", False, group="notification") message = { 'payload': {'event_type': "foo", 'message_id': "abc"}, 'metadata': {}, 'ctxt': {} } with mock.patch("ceilometer.pipeline.event.LOG") as mock_logger: ret = self.endpoint.process_notifications('info', [message]) self.assertEqual(oslo_messaging.NotificationResult.HANDLED, ret) exception_mock = mock_logger.exception self.assertIn('Continue after error from publisher', exception_mock.call_args_list[0][0][0] % exception_mock.call_args_list[0][0][1]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2073567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/event/test_trait_plugins.py0000664000175000017500000001343215250267452025220 0ustar00zuulzuul# # Copyright 2013 Rackspace Hosting. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.event import trait_plugins from ceilometer.tests import base class TestSplitterPlugin(base.BaseTestCase): def setUp(self): super().setUp() self.pclass = trait_plugins.SplitterTraitPlugin def test_split(self): param = dict(separator='-', segment=0) plugin = self.pclass(**param) match_list = [('test.thing', 'test-foobar-baz')] value = plugin.trait_values(match_list)[0] self.assertEqual('test', value) param = dict(separator='-', segment=1) plugin = self.pclass(**param) match_list = [('test.thing', 'test-foobar-baz')] value = plugin.trait_values(match_list)[0] self.assertEqual('foobar', value) param = dict(separator='-', segment=1, max_split=1) plugin = self.pclass(**param) match_list = [('test.thing', 'test-foobar-baz')] value = plugin.trait_values(match_list)[0] self.assertEqual('foobar-baz', value) def test_no_sep(self): param = dict(separator='-', segment=0) plugin = self.pclass(**param) match_list = [('test.thing', 'test.foobar.baz')] value = plugin.trait_values(match_list)[0] self.assertEqual('test.foobar.baz', value) def test_no_segment(self): param = dict(separator='-', segment=5) plugin = self.pclass(**param) match_list = [('test.thing', 'test-foobar-baz')] value = plugin.trait_values(match_list)[0] self.assertIsNone(value) def test_no_match(self): param = dict(separator='-', segment=0) plugin = self.pclass(**param) match_list = [] value = plugin.trait_values(match_list) self.assertEqual([], value) class TestBitfieldPlugin(base.BaseTestCase): def setUp(self): super().setUp() self.pclass = trait_plugins.BitfieldTraitPlugin self.init = 0 self.params = dict(initial_bitfield=self.init, flags=[dict(path='payload.foo', bit=0, value=42), dict(path='payload.foo', bit=1, value=12), dict(path='payload.thud', bit=1, value=23), dict(path='thingy.boink', bit=4), dict(path='thingy.quux', bit=6, value="wokka"), dict(path='payload.bar', bit=10, value='test')]) def test_bitfield(self): match_list = [('payload.foo', 12), ('payload.bar', 'test'), ('thingy.boink', 'testagain')] plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual(0x412, value[0]) def test_initial(self): match_list = [('payload.foo', 12), ('payload.bar', 'test'), ('thingy.boink', 'testagain')] self.params['initial_bitfield'] = 0x2000 plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual(0x2412, value[0]) def test_no_match(self): match_list = [] plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual(self.init, value[0]) def test_multi(self): match_list = [('payload.foo', 12), ('payload.thud', 23), ('payload.bar', 'test'), ('thingy.boink', 'testagain')] plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual(0x412, value[0]) class TestMapTraitPlugin(base.BaseTestCase): def setUp(self): super().setUp() self.pclass = trait_plugins.MapTraitPlugin self.params = dict(values={'ACTIVE': 1, 'ERROR': 2, 3: 4}, default=-1) def test_map(self): match_list = [('payload.foo', 'ACTIVE'), ('payload.bar', 'ERROR'), ('thingy.boink', 3), ('thingy.invalid', 999)] plugin = self.pclass(**self.params) value = plugin.trait_values(match_list) self.assertEqual([1, 2, 4, -1], value) def test_case_sensitive(self): match_list = [('payload.foo', 'ACTIVE'), ('payload.bar', 'error'), ('thingy.boink', 3), ('thingy.invalid', 999)] plugin = self.pclass(case_sensitive=True, **self.params) value = plugin.trait_values(match_list) self.assertEqual([1, -1, 4, -1], value) def test_case_insensitive(self): match_list = [('payload.foo', 'active'), ('payload.bar', 'ErRoR'), ('thingy.boink', 3), ('thingy.invalid', 999)] plugin = self.pclass(case_sensitive=False, **self.params) value = plugin.trait_values(match_list) self.assertEqual([1, 2, 4, -1], value) def test_values_undefined(self): self.assertRaises(ValueError, self.pclass) def test_values_invalid(self): self.assertRaises( ValueError, lambda: self.pclass(values=[('ACTIVE', 1), ('ERROR', 2), (3, 4)])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2083566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/fakes.py0000664000175000017500000010321315250267452021242 0ustar00zuulzuul# Copyright (C) 2026 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import openstack from openstack.block_storage.v3 import backup as cinder_backup from openstack.block_storage.v3 import service as cinder_service from openstack.block_storage.v3 import snapshot as cinder_snapshot from openstack.block_storage.v3 import stats as cinder_stats from openstack.block_storage.v3 import volume as cinder_volume from openstack.dns.v2 import recordset from openstack.dns.v2 import zone from openstack.identity.v3 import domain as sdk_domain from openstack.identity.v3 import project as sdk_project from openstack.load_balancer.v2 import load_balancer as sdk_load_balancer from openstack.network.v2 import firewall_group as sdk_firewall_group from openstack.network.v2 import firewall_policy as sdk_firewall_policy from openstack.network.v2 import floating_ip as sdk_floating_ip from openstack.network.v2 import vpn_ipsec_site_connection as sdk_ipsec_conn from openstack.network.v2 import vpn_service as sdk_vpn_service from openstack.shared_file_system.v2 import share as sdk_share from ceilometer import keystone_client DOMAIN_DEFAULT_ceilo = keystone_client.Domain( id='default', name='Default', is_enabled=True) DOMAIN_DEFAULT_sdk = sdk_domain.Domain( connection=None, id='default', name='Default', is_enabled=True) DOMAIN_HEAT_ceilo = keystone_client.Domain( id='2f42ab40b7ad4140815ef830d816a16c', name='heat', is_enabled=True) DOMAIN_HEAT_sdk = sdk_domain.Domain( connection=None, id='2f42ab40b7ad4140815ef830d816a16c', name='heat', is_enabled=True) DOMAIN_DISABLED_ceilo = keystone_client.Domain( id='disabled-domain', name='Disabled', is_enabled=False) DOMAIN_DISABLED_sdk = sdk_domain.Domain( connection=None, id='disabled-domain', name='Disabled', is_enabled=False) PROJECT_ADMIN_ceilo = keystone_client.Project( id='2ce92449a23145ef9c539f3327960ce3', name='admin', parent_id='default', domain_id='default', is_domain=False, is_enabled=True) PROJECT_ADMIN_sdk = sdk_project.Project( connection=None, id='2ce92449a23145ef9c539f3327960ce3', name='admin', parent_id='default', domain_id='default', is_domain=False, is_enabled=True) PROJECT_SERVICE_ceilo = keystone_client.Project( id='a2d42c23-d518-46b6-96ab-3fba2e146859', name='service', parent_id='default', domain_id='default', is_domain=False, is_enabled=True) PROJECT_SERVICE_sdk = sdk_project.Project( connection=None, id='a2d42c23-d518-46b6-96ab-3fba2e146859', name='service', domain_id='default', parent_id='default', is_domain=False, is_enabled=True) PROJECT_DEMO_ceilo = keystone_client.Project( id='57d96b9af18d43bb9d047f436279b0be', name='demo', parent_id='default', domain_id='2f42ab40b7ad4140815ef830d816a16c', is_domain=False, is_enabled=True) PROJECT_DEMO_sdk = sdk_project.Project( connection=None, id='57d96b9af18d43bb9d047f436279b0be', name='demo', domain_id='2f42ab40b7ad4140815ef830d816a16c', parent_id='default', is_domain=False, is_enabled=True) PROJECT_DISABLED_ceilo = keystone_client.Project( id='disabled-project', name='disabled', parent_id='default', domain_id='default', is_domain=False, is_enabled=False) PROJECT_DISABLED_sdk = sdk_project.Project( connection=None, id='disabled-project', name='disabled', parent_id='default', domain_id='default', is_domain=False, is_enabled=False) # These are the default set of keystone resources that are used to populate # FakeKeyStoneClient and FakeConnection if no project or domain parameters # are passed to the constructor DEFAULT_PROJECTS_ceilo = [ PROJECT_ADMIN_ceilo, PROJECT_SERVICE_ceilo, PROJECT_DEMO_ceilo, PROJECT_DISABLED_ceilo] DEFAULT_PROJECTS_sdk = [ PROJECT_ADMIN_sdk, PROJECT_SERVICE_sdk, PROJECT_DEMO_sdk, PROJECT_DISABLED_sdk] DEFAULT_DOMAINS_ceilo = [DOMAIN_HEAT_ceilo, DOMAIN_DEFAULT_ceilo] DEFAULT_DOMAINS_sdk = [DOMAIN_HEAT_sdk, DOMAIN_DEFAULT_sdk] ##################### # SDK Fakes ##################### RECORDSET_1 = recordset.Recordset( connection=None, id='rs-1-uuid', name='www.example.com.', type='A', records=['192.168.1.1'], ttl=3600, ) RECORDSET_2 = recordset.Recordset( connection=None, id='rs-2-uuid', name='mail.example.com.', type='MX', records=['10 mail.example.com.'], ttl=3600, ) ZONE_1 = zone.Zone( connection=None, id='zone-1-uuid', name='example.com.', email='admin@example.com', ttl=3600, description='Example zone', type='PRIMARY', status='ACTIVE', action='NONE', serial=1234567890, pool_id='pool-1', project_id='tenant-1-uuid', ) ZONE_2 = zone.Zone( connection=None, id='zone-2-uuid', name='test.org.', email='admin@test.org', ttl=7200, description='Test zone', type='PRIMARY', status='PENDING', action='CREATE', serial=1234567891, pool_id='pool-1', project_id='tenant-2-uuid', ) ZONE_3 = zone.Zone( connection=None, id='zone-3-uuid', name='error.net.', email='admin@error.net', ttl=1800, description='Error zone', type='PRIMARY', status='ERROR', action='UPDATE', serial=1234567892, pool_id='pool-2', project_id='tenant-1-uuid', ) ZONE_UNKNOWN_STATUS = zone.Zone( connection=None, id='zone-unknown-uuid', name='unknown.com.', email='admin@unknown.com', ttl=3600, description='Unknown zone', type='PRIMARY', status='UNKNOWN_STATUS', action='NONE', serial=1234567893, pool_id='pool-1', project_id='tenant-1-uuid', ) LB_1 = sdk_load_balancer.LoadBalancer( connection=None, id='lb-1-uuid', name='my-lb-1', availability_zone='az-1', vip_address='192.168.1.10', vip_port_id='port-1-uuid', provisioning_status='ACTIVE', operating_status='ONLINE', provider='amphora', flavor_id='flavor-1', project_id='tenant-1-uuid', ) LB_2 = sdk_load_balancer.LoadBalancer( connection=None, id='lb-2-uuid', name='my-lb-2', availability_zone='az-2', vip_address='192.168.1.11', vip_port_id='port-2-uuid', provisioning_status='PENDING_UPDATE', operating_status='OFFLINE', provider='amphora', flavor_id='flavor-1', project_id='tenant-2-uuid', ) LB_3 = sdk_load_balancer.LoadBalancer( connection=None, id='lb-3-uuid', name='my-lb-3', availability_zone=None, vip_address='192.168.1.12', vip_port_id='port-3-uuid', provisioning_status='ERROR', operating_status='ERROR', provider='amphora', flavor_id='flavor-1', project_id='tenant-1-uuid', ) LB_4 = sdk_load_balancer.LoadBalancer( connection=None, id='lb-4-uuid', name='my-lb-4', availability_zone='az-1', vip_address='192.168.1.13', vip_port_id='port-4-uuid', provisioning_status='PENDING_DELETE', operating_status='DEGRADED', provider='amphora', flavor_id='flavor-1', project_id='tenant-1-uuid', ) LB_UNKNOWN_OPERATING_STATUS = sdk_load_balancer.LoadBalancer( connection=None, id='lb-unknown-uuid', name='my-lb-unknown', availability_zone=None, vip_address='192.168.1.99', vip_port_id='port-unknown-uuid', provisioning_status='ACTIVE', operating_status='UNKNOWN_STATUS', provider='amphora', flavor_id='flavor-1', project_id='tenant-1-uuid', ) LB_UNKNOWN_PROVISIONING_STATUS = sdk_load_balancer.LoadBalancer( connection=None, id='lb-unknown-uuid', name='my-lb-unknown', availability_zone=None, vip_address='192.168.1.99', vip_port_id='port-unknown-uuid', provisioning_status='UNKNOWN_STATUS', operating_status='ONLINE', provider='amphora', flavor_id='flavor-1', project_id='tenant-1-uuid', ) class FakeSDKOctaviaClient: default_load_balancers = [LB_1, LB_2, LB_3, LB_4] def __init__(self, load_balancers=None): self._load_balancers = (load_balancers if load_balancers is not None else self.default_load_balancers) def load_balancers(self): return iter(self._load_balancers) SHARE_1 = sdk_share.Share( connection=None, id='share-1-uuid', name='my-share-1', availability_zone='az-1', share_protocol='NFS', share_type='default', share_network_id='network-1-uuid', status='available', host='host-1', is_public=False, size=100, project_id='tenant-1-uuid', ) SHARE_2 = sdk_share.Share( connection=None, id='share-2-uuid', name='my-share-2', availability_zone='az-2', share_protocol='CIFS', share_type='default', share_network_id='network-2-uuid', status='creating', host='host-2', is_public=True, size=50, project_id='tenant-2-uuid', ) SHARE_3 = sdk_share.Share( connection=None, id='share-3-uuid', name='my-share-3', availability_zone=None, share_protocol='NFS', share_type='default', share_network_id='network-3-uuid', status='error', host='host-3', is_public=False, size=200, project_id='tenant-1-uuid', ) SHARE_UNKNOWN_STATUS = sdk_share.Share( connection=None, id='share-unknown-uuid', name='my-share-unknown', availability_zone=None, share_protocol='NFS', share_type='default', share_network_id='network-unknown-uuid', status='UNKNOWN_STATUS', host='host-unknown', is_public=False, size=100, project_id='tenant-1-uuid', ) class FakeSDKManilaClient: default_shares = [SHARE_1, SHARE_2, SHARE_3] def __init__(self, shares=None): self._shares = shares if shares is not None else self.default_shares def shares(self, all_projects=True): return iter(self._shares) class FakeSDKDesignateClient: default_recordsets = [RECORDSET_1, RECORDSET_2] default_zones = [ZONE_1, ZONE_2, ZONE_3] def __init__(self, zones=None, recordsets=None): self._zones = zones if zones is not None else self.default_zones self._recordsets = (recordsets if recordsets is not None else self.default_recordsets) def zones(self, all_projects=True): return iter(self._zones) def recordsets(self, zone, all_projects=True): return iter(self._recordsets) class FakeSession: """Minimal fake for keystoneauth1.session.Session. Exposes only the attributes accessed by ceilometer's keystone_client helper functions: get_service_catalog(), get_auth_token(), url_for(), and get_urls() all call session.auth.get_access(session). """ class FakeAuth: def get_access(self, session): return "fake_token" def __init__(self): self.auth = self.FakeAuth() class FakeSDKCinderClient: def __init__( self, volumes=None, snapshots=None, backups=None, pools=None, services=None): self._volumes = volumes if volumes is not None else VOLUME_LIST self._snapshots = snapshots if snapshots is not None else SNAPSHOT_LIST self._backups = backups if backups is not None else BACKUP_LIST self._pools = pools if pools is not None else POOL_LIST self._services = services if services is not None else SERVICE_LIST def volumes(self, *, details=True, all_projects=False, **query): return iter(self._volumes) def snapshots(self, *, details=True, **query): return iter(self._snapshots) def backups(self, *, details=True, **query): return iter(self._backups) def backend_pools(self, **query): return iter(self._pools) def services(self, **query): return iter(self._services) class FakeSDKNetworkClient: def ips(self): FLOATING_IP_0 = sdk_floating_ip.FloatingIP( connection=None, id='fip-123', floating_ip_address='192.168.1.100', fixed_ip_address='10.0.0.5', status='ACTIVE', project_id='project-abc', router_id='router-456' ) return iter([FLOATING_IP_0]) def firewall_groups(self): FIREWALL_GROUP_0 = sdk_firewall_group.FirewallGroup( connection=None, id='fw-123', name='my-firewall', status='ACTIVE', project_id='project-abc', ingress_firewall_policy_id='policy-1', egress_firewall_policy_id='policy-2' ) return iter([FIREWALL_GROUP_0]) def firewall_policies(self): FIREWALL_POLICY_0 = sdk_firewall_policy.FirewallPolicy( connection=None, id='policy-123', name='my-policy', project_id='project-abc', firewall_rules=['rule-1', 'rule-2'] ) return iter([FIREWALL_POLICY_0]) def vpn_ipsec_site_connections(self): VPN_IPSEC_CONN_0 = sdk_ipsec_conn.VpnIPSecSiteConnection( connection=None, id='ipsec-123', name='my-ipsec', status='ACTIVE', project_id='project-abc' ) return iter([VPN_IPSEC_CONN_0]) def vpn_services(self): VPN_SERVICE_0 = sdk_vpn_service.VpnService( connection=None, id='vpn-123', name='my-vpn', status='ACTIVE', project_id='project-abc' ) return iter([VPN_SERVICE_0]) IMAGE_UBUNTU = openstack.image.v2.image.Image( connection=None, status="active", visibility="public", name="ubuntu-12.04-x86", container_format="bare", created_at="2026-07-23T16:31:34Z", disk_format="raw", updated_at="2026-08-06T16:31:34Z", min_disk=1, protected=False, checksum="4708c575c7bc57bea8d112c64a5a6fa1", min_ram=0, tags=[], virtual_size=2000000000, size=250000000, owner="2d1689c3a5d3482a98544acaa7edef2f", id="a1f4684e-58bd-4c88-aefd-2ecb0783b497", ) IMAGE_1 = openstack.image.v2.image.Image( connection=None, id=1, name='ubuntu-12.04-x86', kernel_id=11, ramdisk_id=21, disk_format='qcow2', container_format='bare', min_disk=1, min_ram=0, os_distro='ubuntu', os_type='linux') IMAGE_2 = openstack.image.v2.image.Image( connection=None, id=2, name='rhel-6-x64', kernel_id=12, ramdisk_id=22, disk_format='qcow2', container_format='bare', min_disk=0, min_ram=0) IMAGE_MISSING_METADATA = openstack.image.v2.image.Image( connection=None, id=3, name='rhel-6-x64', kernel_id=None, ramdisk_id=None, disk_format='qcow2', container_format='bare', min_disk=0, min_ram=0) IMAGE_MISSING_KERNEL_RAMDISK = openstack.image.v2.image.Image( connection=None, id=4, name='rhel-6-x64', kernel_id=None, ramdisk_id=None, disk_format='qcow2', container_format='bare', min_disk=0, min_ram=0) IMAGE_MISSING_RAMDISK = openstack.image.v2.image.Image( connection=None, id=5, name='rhel-6-x64-missing-ramdisk', kernel_id=11, ramdisk_id=None, disk_format='qcow2', container_format='bare', min_disk=0, min_ram=0) IMAGE_MISSING_KERNEL = openstack.image.v2.image.Image( connection=None, id=6, name='rhel-6-x64-missing-kernel', kernel_id=None, ramdisk_id=21, disk_format='qcow2', container_format='bare', min_disk=0, min_ram=0) IMAGE_FEDORA = openstack.image.v2.image.Image( connection=None, name="Fedora-Cloud-Base-37-1.7.x86_64", disk_format="qcow2", container_format="bare", visibility="public", size=492830720, virtual_size=5368709120, status="active", checksum="36f7b464b6ab46ad97c001b539495e90", protected=False, min_ram=0, min_disk=0, owner="2d1689c3a5d3482a98544acaa7edef2f", id="32ab7d96-fabf-4d1a-a8af-38d0fe3a2b92", tags=[], ) IMAGE_AMPHORA = openstack.image.v2.image.Image( connection=None, name="amphora-x64-haproxy", disk_format="qcow2", container_format="bare", visibility="public", size=379393024, virtual_size=2147483648, status="active", checksum="9a78cb8545b522bd59a3a31d5a131468", protected=False, min_ram=0, min_disk=0, owner="2d1689c3a5d3482a98544acaa7edef2f", id="1cfa6322-5302-4b2d-9748-3b230ea4447d", created_at="2026-01-26T14:36:30Z", updated_at="2026-01-26T14:36:39Z", tags=['amphora'], ) IMAGE_CIRROS = openstack.image.v2.image.Image( connection=None, name="cirros-0.6.3-x86_64-disk", disk_format="qcow2", container_format="bare", visibility="public", size=21692416, virtual_size=117440512, status="active", checksum="87617e24a5e30cb3b87fda8c0764838f", protected=False, min_ram=0, min_disk=0, owner="2d1689c3a5d3482a98544acaa7edef2f", id="8e16de93-6ba5-42a7-8fbd-679607110d7a", created_at="2026-01-26T14:35:30Z", updated_at="2026-01-26T14:35:31Z", tags=[], ) IMAGE_CIRROS_DISK = openstack.image.v2.image.Image( connection=None, name="cirros-0.6.1-x86_64-disk", disk_format="qcow2", container_format="bare", visibility="public", size=21233664, virtual_size=117440512, status="active", protected=False, min_ram=0, min_disk=0, owner="2d1689c3a5d3482a98544acaa7edef2f", id="389c678d-4686-4426-ba7c-0697f065c520", created_at="2026-01-26T14:35:27Z", updated_at="2026-01-26T14:35:29Z", tags=[], ) IMAGE_LIST = [ IMAGE_FEDORA, IMAGE_AMPHORA, IMAGE_CIRROS, IMAGE_CIRROS_DISK, IMAGE_UBUNTU, IMAGE_1, IMAGE_2] class FakeSDKImageClient: """Fake openstack/image/v2/_proxy.Proxy.""" def __init__(self, images=None): self._images = images if images is not None else IMAGE_LIST def images(self, **query): """Return a generator of images :param kwargs query: Optional query parameters to be sent to limit the resources being returned. :returns: A generator of image objects :rtype: :class:`~openstack.image.v2.image.Image` """ if query: raise NotImplementedError( "FakeSDKImageClient.images does not support query param") return iter(self._images) def get_image(self, image): """Get a single image :param image: The value can be the ID of a image or a :class:`~openstack.image.v2.image.Image` instance. :returns: One :class:`~openstack.image.v2.image.Image` :raises: :class:`~openstack.exceptions.NotFoundException` when no resource can be found. """ if isinstance(image, openstack.image.v2.image.Image): image_id = image.id else: image_id = image image_list = [i for i in self.images() if i.id == image_id] try: return image_list[0] except IndexError: raise openstack.exceptions.NotFoundException class FakeConnection: """Fake connection object for testing.""" def __init__(self, session=None, domains=None, projects=None, zones=None, recordsets=None, shares=None, load_balancers=None, volumes=None, snapshots=None, backups=None, pools=None, services=None, images=None): """Initialize FakeConnection. :param projects: Optional list of SDK Project objects. Defaults to the class-level SDK_PROJECT_* fixtures. :param domains: Optional list of SDK Domain objects. Defaults to the class-level DOMAIN_* fixtures. :param zones: Optional list of SDK Zone objects. Passed to FakeSDKDesignateClient. :param recordsets: Optional list of SDK RecordSet objects. Passed to FakeSDKDesignateClient. :param shares: Optional list of SDK Share objects. Passed to FakeSDKManilaClient. :param load_balancers: Optional list of SDK LoadBalancer objects. Passed to FakeSDKOctaviaClient. :param volumes: Optional list of SDK Volumes. Passed to FakeSDKCinderClient. :param snapshots: Optional list of SDK Volume Snapshots. Passed to FakeSDKCinderClient. :param backups: Optional list of SDK Volume Backups. Passed to FakeSDKCinderClient. :param pools: Optional list of SDK Volume Pools. Passed to FakeSDKCinderClient. :param services: Optional list of SDK Volume Service. Passed to FakeSDKCinderClient. :param images: Optional list of SDK Image objects. Passed to FakeSDKImageClient. """ self.dns = FakeSDKDesignateClient(zones=zones, recordsets=recordsets) self.shared_file_system = FakeSDKManilaClient(shares=shares) self.load_balancer = FakeSDKOctaviaClient( load_balancers=load_balancers) self.network = FakeSDKNetworkClient() self.image = FakeSDKImageClient(images=images) self.session = session or FakeSession() # Don't use a short-circuit or here. The explicit check for None is # needed since [] is falsey, but is a valid input e.g. to create a # connection with no projects self._domains = domains if domains is not None else DEFAULT_DOMAINS_sdk self._projects = ( projects if projects is not None else DEFAULT_PROJECTS_sdk) self.block_storage = FakeSDKCinderClient( volumes=volumes, snapshots=snapshots, backups=backups, pools=pools, services=services) def list_projects(self, domain_id=None, name_or_id=None, filters=None): """List projects. emulates openstacksdk/cloud/_identity.py:list_projects With no parameters, returns a full listing of all visible projects. :param domain_id: Domain ID to scope the searched projects. :param name_or_id: Name or ID of the project(s). :param filters: A dictionary of meta data to use for further filtering. :returns: A list of identity ``Project`` objects. :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ result = list(self._projects) # Filter by domain_id if domain_id: result = [p for p in result if p.domain_id == domain_id] # Filter by name_or_id if name_or_id: result = [p for p in result if p.name == name_or_id or p.id == name_or_id] # Apply additional filters # SDK resources expose boolean fields as is_ (e.g. is_enabled), # so fall back to the is_ variant if the raw key is not found. if filters: for key, value in filters.items(): result = [p for p in result if getattr(p, key, getattr(p, 'is_' + key, None)) == value] return result def search_projects(self, name_or_id=None, filters=None, domain_id=None): """Search projects. emulates openstacksdk/cloud/_identity.py:search_projects :param name_or_id: Name or ID of the project(s). :param filters: A dictionary of meta data to use for further filtering. :param domain_id: Domain ID to scope the searched projects. :returns: A list of identity ``Project`` objects. :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ result = self.list_projects(domain_id=domain_id, filters=filters) if name_or_id: result = [p for p in result if p.name == name_or_id or p.id == name_or_id] return result def search_domains(self, name_or_id=None, filters=None): """Search domains. emulates openstacksdk/cloud/_identity.py:search_domains :param name_or_id: Name or ID of the domain(s). :param filters: A dictionary of meta data to use for further filtering. :returns: A list of identity ``Domain`` objects. :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ result = list(self._domains) if name_or_id: result = [d for d in result if d.name == name_or_id or d.id == name_or_id] if filters: for key, value in filters.items(): result = [d for d in result if getattr(d, key, getattr(d, 'is_' + key, None)) == value] return result def list_domains(self, filters=None): """List Keystone domains. emulates openstacksdk/cloud/_identity.py:list_domains :param filters: A dictionary of meta data to use for further filtering. :returns: A list of identity ``Domain`` objects. :raises: :class:`~openstack.exceptions.SDKException` if something goes wrong during the OpenStack API call. """ return self.search_domains(filters=filters) ##################### # Cinder Fakes ##################### VOLUME = cinder_volume.Volume(connection=None, **{ 'migration_status': None, 'attachments': [ {'server_id': '1ae69721-d071-4156-a2bd-b11bb43ec2e3', 'attachment_id': 'f903d95e-f999-4a34-8be7-119eadd9bb4f', 'attached_at': '2016-07-14T03:55:57.000000', 'host_name': None, 'volume_id': 'd94c18fb-b680-4912-9741-da69ee83c94f', 'device': '/dev/vdb', 'id': 'd94c18fb-b680-4912-9741-da69ee83c94f'}], 'links': [ {'href': 'http://fake_link3', 'rel': 'self'}, {'href': 'http://fake_link4', 'rel': 'bookmark'}], 'availability_zone': 'nova', 'os-vol-host-attr:host': 'test@lvmdriver-1#lvmdriver-1', 'encrypted': False, 'updated_at': '2016-07-14T03:55:57.000000', 'replication_status': 'disabled', 'snapshot_id': None, 'id': 'd94c18fb-b680-4912-9741-da69ee83c94f', 'size': 1, 'user_id': 'be255bd31eb944578000fc762fde6dcf', 'os-vol-tenant-attr:tenant_id': '6824974c08974d4db864bbaa6bc08303', 'os-vol-mig-status-attr:migstat': None, 'metadata': {'readonly': 'False', 'attached_mode': 'rw'}, 'status': 'in-use', 'description': None, 'multiattach': False, 'source_volid': None, 'consistencygroup_id': None, 'volume_image_metadata': { 'checksum': '17d9daa4fb8e20b0f6b7dec0d46fdddf', 'container_format': 'bare', 'disk_format': 'raw', 'hw_disk_bus': 'scsi', 'hw_scsi_model': 'virtio-scsi', 'image_id': 'f0019ee3-523c-45ab-b0b6-3adc529673e7', 'image_name': 'debian-jessie-scsi', 'min_disk': '0', 'min_ram': '0', 'size': '1572864000', }, 'os-vol-mig-status-attr:name_id': None, 'group_id': None, 'provider_id': None, 'shared_targets': False, 'service_uuid': '2f6b5a18-0cd5-4421-b97e-d2c3e85ed758', 'cluster_name': None, 'volume_type_id': '65a9f65a-4696-4435-a09d-bc44d797c529', 'name': None, 'bootable': 'false', 'created_at': '2016-06-23T08:27:45.000000', 'volume_type': 'lvmdriver-1', }) SNAPSHOT = cinder_snapshot.Snapshot(connection=None, **{ 'status': 'available', 'os-extended-snapshot-attributes:progress': '100%', 'description': None, 'os-extended-snapshot-attributes:project_id': '6824974c08974d4db864bbaa6bc08303', 'size': 1, 'user_id': 'be255bd31eb944578000fc762fde6dcf', 'updated_at': '2016-10-19T07:56:55.000000', 'id': 'b1ea6783-f952-491e-a4ed-23a6a562e1cf', 'volume_id': '6f27bc42-c834-49ea-ae75-8d1073b37806', 'metadata': {}, 'created_at': '2016-10-19T07:56:55.000000', 'group_snapshot_id': None, 'name': None, }) BACKUP = cinder_backup.Backup(connection=None, **{ 'status': 'available', 'object_count': 0, 'container': None, 'name': None, 'links': [ {'href': 'http://fake_urla', 'rel': 'self'}, {'href': 'http://fake_urlb', 'rel': 'bookmark'}], 'availability_zone': 'nova', 'created_at': '2016-10-19T06:55:23.000000', 'snapshot_id': None, 'updated_at': '2016-10-19T06:55:23.000000', 'data_timestamp': '2016-10-19T06:55:23.000000', 'description': None, 'has_dependent_backups': False, 'volume_id': '6f27bc42-c834-49ea-ae75-8d1073b37806', 'os-backup-project-attr:project_id': '6824974c08974d4db864bbaa6bc08303', 'fail_reason': '', 'is_incremental': False, 'metadata': {}, 'user_id': 'be255bd31eb944578000fc762fde6dcf', 'id': '75a52125-85ff-4a8d-b2aa-580f3b22273f', 'size': 1, }) POOL_LVM = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-1#lvmdriver-1', 'capabilities': { 'pool_name': 'lvmdriver-1', 'total_capacity_gb': 28.5, 'free_capacity_gb': 28.39, 'reserved_percentage': 0, 'location_info': 'LVMVolumeDriver:localhost.localdomain:stack-volumes:thin:0', 'QoS_support': False, 'provisioned_capacity_gb': 4.0, 'max_over_subscription_ratio': 20.0, 'thin_provisioning_support': True, 'thick_provisioning_support': False, 'total_volumes': 3, 'filter_function': None, 'goodness_function': None, 'multiattach': True, 'backend_state': 'up', 'allocated_capacity_gb': 4, 'cacheable': True, 'volume_backend_name': 'lvmdriver-1', 'storage_protocol': 'iSCSI', 'vendor_name': 'Open Source', 'driver_version': '3.0.0', 'timestamp': '2025-03-21T14:19:02.901750', }, }) POOL_CEPH = cinder_stats.Pools(connection=None, **{ 'name': 'cinder-3ceee-volume-ceph-0@ceph#ceph', 'capabilities': { 'vendor_name': 'Open Source', 'driver_version': '1.3.0', 'storage_protocol': 'ceph', 'total_capacity_gb': 85.0, 'free_capacity_gb': 85.0, 'reserved_percentage': 0, 'multiattach': True, 'thin_provisioning_support': True, 'max_over_subscription_ratio': '20.0', 'location_info': 'ceph:/etc/ceph/ceph.conf:a94b63c4e:openstack:volumes', 'backend_state': 'up', 'QoS_support': True, 'volume_backend_name': 'ceph', 'replication_enabled': False, 'allocated_capacity_gb': 1, 'filter_function': None, 'goodness_function': None, 'timestamp': '2025-06-09T13:29:43.286226', }, }) POOL_ZERO_ALLOCATED_CAPACITY = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-1#lvmdriver-1', 'capabilities': { 'allocated_capacity_gb': 0, }, }) POOL_NO_CAPABILITIES = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-1#lvmdriver-1', }) # Test fixture for VirtualFree pollster when provisioned_capacity_gb is # missing. The pollster uses getattr(pool, 'provisioned_capacity_gb', None) # as a guard, so pools missing this attribute are silently skipped # (yield no samples). POOL_NO_PROVISIONED_CAPACITY = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-2#lvmdriver-2', 'capabilities': { 'pool_name': 'lvmdriver-2', 'total_capacity_gb': 28.5, 'free_capacity_gb': 28.39, 'reserved_percentage': 0, 'max_over_subscription_ratio': 20.0, 'thin_provisioning_support': True, 'allocated_capacity_gb': 4, } }) # Test fixture for VirtualFree pollster when thin_provisioning_support is # missing. When provisioned_capacity_gb IS set but thin_provisioning_support # is missing, the pollster attempts to access the attribute and raises # AttributeError. POOL_NO_THIN_PROVISIONING = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-3#lvmdriver-3', 'capabilities': { 'pool_name': 'lvmdriver-3', 'total_capacity_gb': 28.5, 'free_capacity_gb': 28.39, 'reserved_percentage': 0, 'provisioned_capacity_gb': 4.0, 'max_over_subscription_ratio': 20.0, 'allocated_capacity_gb': 4, } }) # Test fixture for VirtualFree pollster with thick provisioning. # When thin_provisioning_support=False, max_over_subscription_ratio defaults to # 1.0 in the calculation: 1.0 * (28.5 - 0) - 4.0 = 24.5 virtual free capacity. POOL_THICK_PROVISIONING = cinder_stats.Pools(connection=None, **{ 'name': 'localhost.localdomain@lvmdriver-4#lvmdriver-4', 'capabilities': { 'pool_name': 'lvmdriver-4', 'total_capacity_gb': 28.5, 'free_capacity_gb': 28.39, 'reserved_percentage': 0, 'provisioned_capacity_gb': 4.0, 'max_over_subscription_ratio': 20.0, 'thin_provisioning_support': False, 'allocated_capacity_gb': 4, } }) # Test fixture for VirtualFree pollster when reserved_percentage is not set POOL_NO_RESERVED_PERCENTAGE = cinder_stats.Pools(connection=None, **{ 'name': 'mypool', 'capabilities': { 'provisioned_capacity_gb': 100, }}) SERVICE_CINDER_VOLUME = cinder_service.Service(connection=None, **{ 'binary': 'cinder-volume', 'host': 'devstack', 'zone': 'nova', 'status': 'enabled', 'state': 'up'}) SERVICE_CINDER_SCHED = cinder_service.Service(connection=None, **{ 'binary': 'cinder-scheduler', 'host': 'devstack', 'zone': 'nova', 'status': 'enabled', 'state': 'up'}) SERVICE_CINDER_BACKUP = cinder_service.Service(connection=None, **{ 'binary': 'cinder-backup', 'host': 'devstack', 'zone': 'nova', 'status': 'enabled', 'state': 'down'}) SERVICE_CINDER_UNKNOWN_STATUS = cinder_service.Service(connection=None, **{ 'binary': 'cinder-volume', 'host': 'devstack', 'zone': 'nova', 'status': 'enabled', 'state': 'unknown'}) VOLUME_LIST = [VOLUME] SNAPSHOT_LIST = [SNAPSHOT] BACKUP_LIST = [BACKUP] POOL_LIST = [POOL_LVM, POOL_CEPH] SERVICE_LIST = [ SERVICE_CINDER_VOLUME, SERVICE_CINDER_SCHED, SERVICE_CINDER_BACKUP] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2651277 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/image/0000775000175000017500000000000015250267567020670 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2083566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/image/__init__.py0000664000175000017500000000000015250267452022760 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2093568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/image/test_glance.py0000664000175000017500000000330015250267452023517 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.image import glance from ceilometer.polling import manager from ceilometer import service import ceilometer.tests.base as base from ceilometer.tests.unit import fakes class TestImagePollsterPageSize(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = glance.ImageSizePollster(conf) def test_image_pollster(self): image_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.IMAGE_LIST)) self.assertEqual(len(fakes.IMAGE_LIST), len(image_samples)) for ix in range(len(image_samples)): self.assertEqual('image.size', image_samples[ix].name) self.assertEqual(fakes.IMAGE_LIST[ix].size, image_samples[ix].volume) self.assertEqual(fakes.IMAGE_LIST[ix].owner, image_samples[ix].project_id) self.assertEqual(fakes.IMAGE_LIST[ix].id, image_samples[ix].resource_id) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2651277 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/0000775000175000017500000000000015250267567020544 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2093568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/__init__.py0000664000175000017500000000000015250267452022634 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2661276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/notifications/0000775000175000017500000000000015250267567023415 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2093568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/notifications/__init__.py0000664000175000017500000000000015250267452025505 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2103567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py0000664000175000017500000010050015250267452026742 0ustar00zuulzuul# # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Sample data for test_ipmi. This data is provided as a sample of the data expected from the ipmitool driver in the Ironic project, which is the publisher of the notifications being tested. """ TEMPERATURE_DATA = { 'DIMM GH VR Temp (0x3b)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '26 (+/- 0.500) degrees C', 'Entity ID': '20.6 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'DIMM GH VR Temp (0x3b)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'CPU1 VR Temp (0x36)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '32 (+/- 0.500) degrees C', 'Entity ID': '20.1 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'CPU1 VR Temp (0x36)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'DIMM EF VR Temp (0x3a)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '26 (+/- 0.500) degrees C', 'Entity ID': '20.5 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'DIMM EF VR Temp (0x3a)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'CPU2 VR Temp (0x37)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '31 (+/- 0.500) degrees C', 'Entity ID': '20.2 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'CPU2 VR Temp (0x37)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'Ambient Temp (0x32)': { 'Status': 'ok', 'Sensor Reading': '25 (+/- 0) degrees C', 'Entity ID': '12.1 (Front Panel Board)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Event Message Control': 'Per-threshold', 'Assertion Events': '', 'Upper non-critical': '43.000', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Upper non-recoverable': '50.000', 'Positive Hysteresis': '4.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '46.000', 'Sensor ID': 'Ambient Temp (0x32)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '25.000' }, 'Mezz Card Temp (0x35)': { 'Status': 'Disabled', 'Sensor Reading': 'Disabled', 'Entity ID': '44.1 (I/O Module)', 'Event Message Control': 'Per-threshold', 'Upper non-critical': '70.000', 'Upper non-recoverable': '85.000', 'Positive Hysteresis': '4.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '80.000', 'Sensor ID': 'Mezz Card Temp (0x35)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '25.000' }, 'PCH Temp (0x3c)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '46 (+/- 0.500) degrees C', 'Entity ID': '45.1 (Processor/IO Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '93.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '103.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '98.000', 'Sensor ID': 'PCH Temp (0x3c)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'DIMM CD VR Temp (0x39)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '27 (+/- 0.500) degrees C', 'Entity ID': '20.4 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'DIMM CD VR Temp (0x39)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'PCI Riser 2 Temp (0x34)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '30 (+/- 0) degrees C', 'Entity ID': '16.2 (System Internal Expansion Board)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '70.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '85.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '80.000', 'Sensor ID': 'PCI Riser 2 Temp (0x34)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'DIMM AB VR Temp (0x38)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '28 (+/- 0.500) degrees C', 'Entity ID': '20.3 (Power Module)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '95.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '105.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '100.000', 'Sensor ID': 'DIMM AB VR Temp (0x38)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, 'PCI Riser 1 Temp (0x33)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': '38 (+/- 0) degrees C', 'Entity ID': '16.1 (System Internal Expansion Board)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '70.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '85.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '80.000', 'Sensor ID': 'PCI Riser 1 Temp (0x33)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, } CURRENT_DATA = { 'Current 1 (0x6b)': { 'Status': 'ok', 'Sensor Reading': '0.800 (+/- 0) Amps', 'Entity ID': '21.0 (Power Management)', 'Assertions Enabled': '', 'Event Message Control': 'Per-threshold', 'Readable Thresholds': 'No Thresholds', 'Positive Hysteresis': 'Unspecified', 'Sensor Type (Analog)': 'Current', 'Negative Hysteresis': 'Unspecified', 'Maximum sensor range': 'Unspecified', 'Sensor ID': 'Current 1 (0x6b)', 'Assertion Events': '', 'Minimum sensor range': '2550.000', 'Settable Thresholds': 'No Thresholds' }, 'Pwr Consumption (0x76)': { 'Entity ID': '7.1 (System Board)', 'Sensor Type (Threshold)': 'Current (0x03)', 'Sensor Reading': '160 (+/- 0) Watts', 'Status': 'ok', 'Nominal Reading': '1034.000', 'Normal Maximum': '1056.000', 'Upper critical': '1914.000', 'Upper non-critical': '1738.000', 'Positive Hysteresis': 'Unspecified', 'Negative Hysteresis': 'Unspecified', 'Minimum sensor range': 'Unspecified', 'Maximum sensor range': '5588.000', 'Sensor ID': 'Pwr Consumption (0x76)', 'Event Message Control': 'Per-threshold', 'Readable Thresholds': 'unc ucr', 'Settable Thresholds': 'unc', 'Assertion Events': '', 'Assertions Enabled': 'unc+ ucr+', 'Deassertions Enabled': 'unc+ ucr+' } } POWER_DATA = { 'Pwr Consumption (0x76)': { 'Entity ID': '7.1 (System Board)', 'Sensor Type (Threshold)': 'Current (0x03)', 'Sensor Reading': '154 (+/- 0) Watts', 'Status': 'ok', 'Nominal Reading': '1034.000', 'Normal Maximum': '1056.000', 'Upper critical': '1914.000', 'Upper non-critical': '1738.000', 'Positive Hysteresis': 'Unspecified', 'Negative Hysteresis': 'Unspecified', 'Minimum sensor range': 'Unspecified', 'Maximum sensor range': '5588.000', 'Sensor ID': 'Pwr Consumption (0x76)', 'Event Message Control': 'Per-threshold', 'Readable Thresholds': 'unc ucr', 'Settable Thresholds': 'unc', 'Assertion Events': '', 'Assertions Enabled': 'unc+ ucr+', 'Deassertions Enabled': 'unc+ ucr+' } } FAN_DATA = { 'Fan 4A Tach (0x46)': { 'Status': 'ok', 'Sensor Reading': '6900 (+/- 0) RPM', 'Entity ID': '29.4 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 4A Tach (0x46)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' }, 'Fan 5A Tach (0x48)': { 'Status': 'ok', 'Sensor Reading': '7140 (+/- 0) RPM', 'Entity ID': '29.5 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 5A Tach (0x48)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' }, 'Fan 3A Tach (0x44)': { 'Status': 'ok', 'Sensor Reading': '6900 (+/- 0) RPM', 'Entity ID': '29.3 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 3A Tach (0x44)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' }, 'Fan 1A Tach (0x40)': { 'Status': 'ok', 'Sensor Reading': '6960 (+/- 0) RPM', 'Entity ID': '29.1 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 1A Tach (0x40)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' }, 'Fan 3B Tach (0x45)': { 'Status': 'ok', 'Sensor Reading': '7104 (+/- 0) RPM', 'Entity ID': '29.3 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 3B Tach (0x45)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 2A Tach (0x42)': { 'Status': 'ok', 'Sensor Reading': '7080 (+/- 0) RPM', 'Entity ID': '29.2 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 2A Tach (0x42)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' }, 'Fan 4B Tach (0x47)': { 'Status': 'ok', 'Sensor Reading': '7488 (+/- 0) RPM', 'Entity ID': '29.4 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 4B Tach (0x47)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 2B Tach (0x43)': { 'Status': 'ok', 'Sensor Reading': '7168 (+/- 0) RPM', 'Entity ID': '29.2 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 2B Tach (0x43)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 5B Tach (0x49)': { 'Status': 'ok', 'Sensor Reading': '7296 (+/- 0) RPM', 'Entity ID': '29.5 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 5B Tach (0x49)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 1B Tach (0x41)': { 'Status': 'ok', 'Sensor Reading': '7296 (+/- 0) RPM', 'Entity ID': '29.1 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 1B Tach (0x41)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 6B Tach (0x4b)': { 'Status': 'ok', 'Sensor Reading': '7616 (+/- 0) RPM', 'Entity ID': '29.6 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2752.000', 'Positive Hysteresis': '128.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '16320.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '128.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 6B Tach (0x4b)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3968.000' }, 'Fan 6A Tach (0x4a)': { 'Status': 'ok', 'Sensor Reading': '7080 (+/- 0) RPM', 'Entity ID': '29.6 (Fan Device)', 'Assertions Enabled': 'lcr-', 'Normal Minimum': '2580.000', 'Positive Hysteresis': '120.000', 'Assertion Events': '', 'Event Message Control': 'Per-threshold', 'Normal Maximum': '15300.000', 'Deassertions Enabled': 'lcr-', 'Sensor Type (Analog)': 'Fan', 'Lower critical': '1920.000', 'Negative Hysteresis': '120.000', 'Threshold Read Mask': 'lcr', 'Maximum sensor range': 'Unspecified', 'Readable Thresholds': 'lcr', 'Sensor ID': 'Fan 6A Tach (0x4a)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4020.000' } } FAN_DATA_PERCENT = { 'Fan 1 (0x23)': { 'Sensor ID': 'Fan 1 (0x23)', 'Entity ID': '7.1 (System Board)', 'Sensor Type (Threshold)': 'Fan (0x04)', 'Sensor Reading': '47.040 (+/- 0) percent', 'Status': 'ok', 'Positive Hysteresis': 'Unspecified', 'Negative Hysteresis': 'Unspecified', 'Minimum sensor range': 'Unspecified', 'Maximum sensor range': 'Unspecified', 'Event Message Control': 'Global Disable Only', 'Readable Thresholds': '', 'Settable Thresholds': '', 'Assertions Enabled': '' } } VOLTAGE_DATA = { 'Planar 12V (0x18)': { 'Status': 'ok', 'Sensor Reading': '12.312 (+/- 0) Volts', 'Entity ID': '7.1 (System Board)', 'Assertions Enabled': 'lcr- ucr+', 'Event Message Control': 'Per-threshold', 'Assertion Events': '', 'Maximum sensor range': 'Unspecified', 'Positive Hysteresis': '0.108', 'Deassertions Enabled': 'lcr- ucr+', 'Sensor Type (Analog)': 'Voltage', 'Lower critical': '10.692', 'Negative Hysteresis': '0.108', 'Threshold Read Mask': 'lcr ucr', 'Upper critical': '13.446', 'Readable Thresholds': 'lcr ucr', 'Sensor ID': 'Planar 12V (0x18)', 'Settable Thresholds': 'lcr ucr', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '12.042' }, 'Planar 3.3V (0x16)': { 'Status': 'ok', 'Sensor Reading': '3.309 (+/- 0) Volts', 'Entity ID': '7.1 (System Board)', 'Assertions Enabled': 'lcr- ucr+', 'Event Message Control': 'Per-threshold', 'Assertion Events': '', 'Maximum sensor range': 'Unspecified', 'Positive Hysteresis': '0.028', 'Deassertions Enabled': 'lcr- ucr+', 'Sensor Type (Analog)': 'Voltage', 'Lower critical': '3.039', 'Negative Hysteresis': '0.028', 'Threshold Read Mask': 'lcr ucr', 'Upper critical': '3.564', 'Readable Thresholds': 'lcr ucr', 'Sensor ID': 'Planar 3.3V (0x16)', 'Settable Thresholds': 'lcr ucr', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3.309' }, 'Planar VBAT (0x1c)': { 'Status': 'ok', 'Sensor Reading': '3.137 (+/- 0) Volts', 'Entity ID': '7.1 (System Board)', 'Assertions Enabled': 'lnc- lcr-', 'Event Message Control': 'Per-threshold', 'Assertion Events': '', 'Readable Thresholds': 'lcr lnc', 'Positive Hysteresis': '0.025', 'Deassertions Enabled': 'lnc- lcr-', 'Sensor Type (Analog)': 'Voltage', 'Lower critical': '2.095', 'Negative Hysteresis': '0.025', 'Lower non-critical': '2.248', 'Maximum sensor range': 'Unspecified', 'Sensor ID': 'Planar VBAT (0x1c)', 'Settable Thresholds': 'lcr lnc', 'Threshold Read Mask': 'lcr lnc', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '3.010' }, 'Planar 5V (0x17)': { 'Status': 'ok', 'Sensor Reading': '5.062 (+/- 0) Volts', 'Entity ID': '7.1 (System Board)', 'Assertions Enabled': 'lcr- ucr+', 'Event Message Control': 'Per-threshold', 'Assertion Events': '', 'Maximum sensor range': 'Unspecified', 'Positive Hysteresis': '0.045', 'Deassertions Enabled': 'lcr- ucr+', 'Sensor Type (Analog)': 'Voltage', 'Lower critical': '4.475', 'Negative Hysteresis': '0.045', 'Threshold Read Mask': 'lcr ucr', 'Upper critical': '5.582', 'Readable Thresholds': 'lcr ucr', 'Sensor ID': 'Planar 5V (0x17)', 'Settable Thresholds': 'lcr ucr', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '4.995' } } SENSOR_DATA = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { 'Temperature': TEMPERATURE_DATA, 'Current': CURRENT_DATA, 'Fan': FAN_DATA, 'Voltage': VOLTAGE_DATA, 'Power': POWER_DATA } } } EMPTY_PAYLOAD = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { } } } MISSING_SENSOR = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { 'Temperature': { 'PCI Riser 1 Temp (0x33)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Entity ID': '16.1 (System Internal Expansion Board)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '70.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '85.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '80.000', 'Sensor ID': 'PCI Riser 1 Temp (0x33)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, } } } } BAD_SENSOR = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { 'Temperature': { 'PCI Riser 1 Temp (0x33)': { 'Status': 'ok', 'Deassertions Enabled': 'unc+ ucr+ unr+', 'Sensor Reading': 'some bad stuff', 'Entity ID': '16.1 (System Internal Expansion Board)', 'Assertions Enabled': 'unc+ ucr+ unr+', 'Positive Hysteresis': '4.000', 'Assertion Events': '', 'Upper non-critical': '70.000', 'Event Message Control': 'Per-threshold', 'Upper non-recoverable': '85.000', 'Normal Maximum': '112.000', 'Maximum sensor range': 'Unspecified', 'Sensor Type (Analog)': 'Temperature', 'Readable Thresholds': 'unc ucr unr', 'Negative Hysteresis': 'Unspecified', 'Threshold Read Mask': 'unc ucr unr', 'Upper critical': '80.000', 'Sensor ID': 'PCI Riser 1 Temp (0x33)', 'Settable Thresholds': '', 'Minimum sensor range': 'Unspecified', 'Nominal Reading': '16.000' }, } } } } NO_SENSOR_ID = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'node_uuid': 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { 'Temperature': { 'PCI Riser 1 Temp (0x33)': { 'Sensor Reading': '26 C', }, } } } } NO_NODE_ID = { 'metadata': {'message_id': 'f22188ca-c068-47ce-a3e5-0e27ffe234c6', 'timestamp': '2015-06-1909:19:35.786893'}, 'publisher_id': 'f23188ca-c068-47ce-a3e5-0e27ffe234c6', 'payload': { 'instance_uuid': 'f11251ax-c568-25ca-4582-0x27add644c6', 'timestamp': '2017-07-07 15:54:12.169510', 'event_type': 'hardware.ipmi.metrics.update', 'payload': { 'Temperature': { 'PCI Riser 1 Temp (0x33)': { 'Sensor Reading': '26 C', 'Sensor ID': 'PCI Riser 1 Temp (0x33)', }, } } } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2103567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/notifications/test_ironic.py0000664000175000017500000002171715250267452026312 0ustar00zuulzuul# # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for producing IPMI sample messages from notification events.""" from unittest import mock from ceilometer.ipmi.notifications import ironic as ipmi from ceilometer import sample from ceilometer.tests import base from ceilometer.tests.unit.ipmi.notifications import ipmi_test_data class TestNotifications(base.BaseTestCase): def test_ipmi_temperature_notification(self): """Test IPMI Temperature sensor data. Based on the above ipmi_testdata the expected sample for a single temperature reading has:: * a resource_id composed from the node_uuid Sensor ID * a name composed from 'hardware.ipmi.' and 'temperature' * a volume from the first chunk of the Sensor Reading * a unit from the last chunk of the Sensor Reading * some readings are skipped if the value is 'Disabled' * metatata with the node id """ processor = ipmi.TemperatureSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(10, len(counters), 'expected 10 temperature readings') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-dimm_gh_vr_temp_(0x3b)' ) test_counter = counters[resource_id] self.assertEqual(26.0, test_counter.volume) self.assertEqual('C', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.temperature', test_counter.name) self.assertEqual('hardware.ipmi.metrics.update', test_counter.resource_metadata['event_type']) self.assertEqual('f4982fd2-2f2b-4bb5-9aff-48aac801d1ad', test_counter.resource_metadata['node']) def test_ipmi_current_notification(self): """Test IPMI Current sensor data. A single current reading is effectively the same as temperature, modulo "current". """ processor = ipmi.CurrentSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(1, len(counters), 'expected 1 current reading') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-current_1_(0x6b)' ) test_counter = counters[resource_id] self.assertEqual(0.800, test_counter.volume) self.assertEqual('Amps', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.current', test_counter.name) def test_ipmi_power_notification(self): """Test IPMI Power sample from Current sensor. A single power reading is effectively the same as temperature, modulo "power". """ processor = ipmi.PowerSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(1, len(counters), 'expected 1 current reading') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pwr_consumption_(0x76)' ) test_counter = counters[resource_id] self.assertEqual(154, test_counter.volume) self.assertEqual('W', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.power', test_counter.name) def test_ipmi_fan_notification(self): """Test IPMI Fan sensor data. A single fan reading is effectively the same as temperature, modulo "fan". """ processor = ipmi.FanSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(12, len(counters), 'expected 12 fan readings') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-fan_4a_tach_(0x46)' ) test_counter = counters[resource_id] self.assertEqual(6900.0, test_counter.volume) self.assertEqual('RPM', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.fan', test_counter.name) def test_ipmi_voltage_notification(self): """Test IPMI Voltage sensor data. A single voltage reading is effectively the same as temperature, modulo "voltage". """ processor = ipmi.VoltageSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(4, len(counters), 'expected 4 volate readings') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-planar_vbat_(0x1c)' ) test_counter = counters[resource_id] self.assertEqual(3.137, test_counter.volume) self.assertEqual('V', test_counter.unit) self.assertEqual(sample.TYPE_GAUGE, test_counter.type) self.assertEqual('hardware.ipmi.voltage', test_counter.name) def test_disabed_skips_metric(self): """Test that a meter which a disabled volume is skipped.""" processor = ipmi.TemperatureSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.SENSOR_DATA)} self.assertEqual(10, len(counters), 'expected 10 temperature readings') resource_id = ( 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-mezz_card_temp_(0x35)' ) self.assertNotIn(resource_id, counters) def test_empty_payload_no_metrics_success(self): processor = ipmi.TemperatureSensorNotification(None, None) counters = {counter.resource_id: counter for counter in processor.build_sample(ipmi_test_data.EMPTY_PAYLOAD)} self.assertEqual(0, len(counters), 'expected 0 readings') @mock.patch('ceilometer.ipmi.notifications.ironic.LOG') def test_missing_sensor_data(self, mylog): processor = ipmi.TemperatureSensorNotification(None, None) messages = [] mylog.warning = lambda *args: messages.extend(args) list(processor.build_sample(ipmi_test_data.MISSING_SENSOR)) self.assertEqual( 'invalid sensor data for ' 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pci_riser_1_temp_(0x33): ' "missing 'Sensor Reading' in payload", messages[0] % messages[1] ) @mock.patch('ceilometer.ipmi.notifications.ironic.LOG') def test_sensor_data_malformed(self, mylog): processor = ipmi.TemperatureSensorNotification(None, None) messages = [] mylog.warning = lambda *args: messages.extend(args) list(processor.build_sample(ipmi_test_data.BAD_SENSOR)) self.assertEqual( 'invalid sensor data for ' 'f4982fd2-2f2b-4bb5-9aff-48aac801d1ad-pci_riser_1_temp_(0x33): ' 'unable to parse sensor reading: some bad stuff', messages[0] % messages[1] ) @mock.patch('ceilometer.ipmi.notifications.ironic.LOG') def test_missing_node_uuid(self, mylog): """Test for desired error message when 'node_uuid' missing. Presumably this will never happen given the way the data is created, but better defensive than dead. """ processor = ipmi.TemperatureSensorNotification(None, None) messages = [] mylog.warning = lambda *args: messages.extend(args) list(processor.build_sample(ipmi_test_data.NO_NODE_ID)) self.assertEqual( 'invalid sensor data for missing id: missing key in payload: ' "'node_uuid'", messages[0] % messages[1] ) @mock.patch('ceilometer.ipmi.notifications.ironic.LOG') def test_missing_sensor_id(self, mylog): """Test for desired error message when 'Sensor ID' missing.""" processor = ipmi.TemperatureSensorNotification(None, None) messages = [] mylog.warning = lambda *args: messages.extend(args) list(processor.build_sample(ipmi_test_data.NO_SENSOR_ID)) self.assertEqual( 'invalid sensor data for missing id: missing key in payload: ' "'Sensor ID'", messages[0] % messages[1] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2671275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/platform/0000775000175000017500000000000015250267567022370 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2103567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/platform/__init__.py0000664000175000017500000000000015250267452024460 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2113566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/platform/fake_utils.py0000664000175000017500000000262515250267452025066 0ustar00zuulzuul# Copyright 2014 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.ipmi.platform import exception as nmexcept from ceilometer.tests.unit.ipmi.platform import ipmitool_test_data as test_data def get_sensor_status_init(parameter=''): return (' 01\n', '') def get_sensor_status_uninit(parameter=''): return (' 00\n', '') def init_sensor_agent(parameter=''): return (' 00\n', '') def execute(*cmd, **kwargs): data = { test_data.sdr_info_cmd: test_data.sdr_info, test_data.read_sensor_temperature_cmd: test_data.sensor_temperature, test_data.read_sensor_voltage_cmd: test_data.sensor_voltage, test_data.read_sensor_current_cmd: test_data.sensor_current, test_data.read_sensor_fan_cmd: test_data.sensor_fan, } cmd_str = "".join(cmd) return data[cmd_str] def execute_without_ipmi(*cmd, **kwargs): raise nmexcept.IPMIException ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2123568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py0000664000175000017500000002751215250267452026626 0ustar00zuulzuul# Copyright 2014 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Sample data for test_ipmi_sensor. This data is provided as a sample of the data expected from the ipmitool binary, which produce Node Manager/IPMI raw data """ sensor_temperature_data = """Sensor ID : SSB Therm Trip (0xd) Entity ID : 7.1 (System Board) Sensor Type (Discrete): Temperature Assertions Enabled : Digital State [State Asserted] Deassertions Enabled : Digital State [State Asserted] Sensor ID : BB P1 VR Temp (0x20) Entity ID : 7.1 (System Board) Sensor Type (Analog) : Temperature Sensor Reading : 25 (+/- 0) degrees C Status : ok Nominal Reading : 58.000 Normal Minimum : 10.000 Normal Maximum : 105.000 Upper critical : 115.000 Upper non-critical : 110.000 Lower critical : 0.000 Lower non-critical : 5.000 Positive Hysteresis : 2.000 Negative Hysteresis : 2.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Assertion Events : Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ Sensor ID : Front Panel Temp (0x21) Entity ID : 12.1 (Front Panel Board) Sensor Type (Analog) : Temperature Sensor Reading : 23 (+/- 0) degrees C Status : ok Nominal Reading : 28.000 Normal Minimum : 10.000 Normal Maximum : 45.000 Upper critical : 55.000 Upper non-critical : 50.000 Lower critical : 0.000 Lower non-critical : 5.000 Positive Hysteresis : 2.000 Negative Hysteresis : 2.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Assertion Events : Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ Sensor ID : SSB Temp (0x22) Entity ID : 7.1 (System Board) Sensor Type (Analog) : Temperature Sensor Reading : 43 (+/- 0) degrees C Status : ok Nominal Reading : 52.000 Normal Minimum : 10.000 Normal Maximum : 93.000 Upper critical : 103.000 Upper non-critical : 98.000 Lower critical : 0.000 Lower non-critical : 5.000 Positive Hysteresis : 2.000 Negative Hysteresis : 2.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Assertion Events : Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ """ sensor_voltage_data = """Sensor ID : VR Watchdog (0xb) Entity ID : 7.1 (System Board) Sensor Type (Discrete): Voltage Assertions Enabled : Digital State [State Asserted] Deassertions Enabled : Digital State [State Asserted] Sensor ID : BB +12.0V (0xd0) Entity ID : 7.1 (System Board) Sensor Type (Analog) : Voltage Sensor Reading : 11.831 (+/- 0) Volts Status : ok Nominal Reading : 11.935 Normal Minimum : 11.363 Normal Maximum : 12.559 Upper critical : 13.391 Upper non-critical : 13.027 Lower critical : 10.635 Lower non-critical : 10.947 Positive Hysteresis : 0.052 Negative Hysteresis : 0.052 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Assertion Events : Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ Sensor ID : BB +1.35 P1LV AB (0xe4) Entity ID : 7.1 (System Board) Sensor Type (Analog) : Voltage Sensor Reading : Disabled Status : Disabled Nominal Reading : 1.342 Normal Minimum : 1.275 Normal Maximum : 1.409 Upper critical : 1.488 Upper non-critical : 1.445 Lower critical : 1.201 Lower non-critical : 1.244 Positive Hysteresis : 0.006 Negative Hysteresis : 0.006 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Event Status : Unavailable Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ Sensor ID : BB +5.0V (0xd1) Entity ID : 7.1 (System Board) Sensor Type (Analog) : Voltage Sensor Reading : 4.959 (+/- 0) Volts Status : ok Nominal Reading : 4.981 Normal Minimum : 4.742 Normal Maximum : 5.241 Upper critical : 5.566 Upper non-critical : 5.415 Lower critical : 4.416 Lower non-critical : 4.546 Positive Hysteresis : 0.022 Negative Hysteresis : 0.022 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc unc ucr Settable Thresholds : lcr lnc unc ucr Threshold Read Mask : lcr lnc unc ucr Assertion Events : Assertions Enabled : lnc- lcr- unc+ ucr+ Deassertions Enabled : lnc- lcr- unc+ ucr+ """ sensor_current_data = """Sensor ID : PS1 Curr Out % (0x58) Entity ID : 10.1 (Power Supply) Sensor Type (Analog) : Current Sensor Reading : 11 (+/- 0) unspecified Status : ok Nominal Reading : 50.000 Normal Minimum : 0.000 Normal Maximum : 100.000 Upper critical : 118.000 Upper non-critical : 100.000 Positive Hysteresis : Unspecified Negative Hysteresis : Unspecified Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : unc ucr Settable Thresholds : unc ucr Threshold Read Mask : unc ucr Assertion Events : Assertions Enabled : unc+ ucr+ Deassertions Enabled : unc+ ucr+ Sensor ID : PS2 Curr Out % (0x59) Entity ID : 10.2 (Power Supply) Sensor Type (Analog) : Current Sensor Reading : 0 (+/- 0) unspecified Status : ok Nominal Reading : 50.000 Normal Minimum : 0.000 Normal Maximum : 100.000 Upper critical : 118.000 Upper non-critical : 100.000 Positive Hysteresis : Unspecified Negative Hysteresis : Unspecified Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : unc ucr Settable Thresholds : unc ucr Threshold Read Mask : unc ucr Assertion Events : Assertions Enabled : unc+ ucr+ Deassertions Enabled : unc+ ucr+ Sensor ID : Pwr Consumption (0x76) Entity ID : 7.1 (System Board) Sensor Type (Threshold) : Current (0x03) Sensor Reading : 154 (+/- 0) Watts Status : ok Nominal Reading : 1034.000 Normal Maximum : 1056.000 Upper critical : 1914.000 Upper non-critical : 1738.000 Positive Hysteresis : Unspecified Negative Hysteresis : Unspecified Minimum sensor range : Unspecified Maximum sensor range : 5588.000 Event Message Control : Per-threshold Readable Thresholds : unc ucr Settable Thresholds : unc Assertion Events : Assertions Enabled : unc+ ucr+ Deassertions Enabled : unc+ ucr+ """ sensor_fan_data = """Sensor ID : System Fan 1 (0x30) Entity ID : 29.1 (Fan Device) Sensor Type (Analog) : Fan Sensor Reading : 4704 (+/- 0) RPM Status : ok Nominal Reading : 7497.000 Normal Minimum : 2499.000 Normal Maximum : 12495.000 Lower critical : 1715.000 Lower non-critical : 1960.000 Positive Hysteresis : 49.000 Negative Hysteresis : 49.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc Settable Thresholds : lcr lnc Threshold Read Mask : lcr lnc Assertion Events : Assertions Enabled : lnc- lcr- Deassertions Enabled : lnc- lcr- Sensor ID : System Fan 2 (0x32) Entity ID : 29.2 (Fan Device) Sensor Type (Analog) : Fan Sensor Reading : 4704 (+/- 0) RPM Status : ok Nominal Reading : 7497.000 Normal Minimum : 2499.000 Normal Maximum : 12495.000 Lower critical : 1715.000 Lower non-critical : 1960.000 Positive Hysteresis : 49.000 Negative Hysteresis : 49.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc Settable Thresholds : lcr lnc Threshold Read Mask : lcr lnc Assertion Events : Assertions Enabled : lnc- lcr- Deassertions Enabled : lnc- lcr- Sensor ID : System Fan 3 (0x34) Entity ID : 29.3 (Fan Device) Sensor Type (Analog) : Fan Sensor Reading : 4704 (+/- 0) RPM Status : ok Nominal Reading : 7497.000 Normal Minimum : 2499.000 Normal Maximum : 12495.000 Lower critical : 1715.000 Lower non-critical : 1960.000 Positive Hysteresis : 49.000 Negative Hysteresis : 49.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc Settable Thresholds : lcr lnc Threshold Read Mask : lcr lnc Assertion Events : Assertions Enabled : lnc- lcr- Deassertions Enabled : lnc- lcr- Sensor ID : System Fan 4 (0x36) Entity ID : 29.4 (Fan Device) Sensor Type (Analog) : Fan Sensor Reading : 4606 (+/- 0) RPM Status : ok Nominal Reading : 7497.000 Normal Minimum : 2499.000 Normal Maximum : 12495.000 Lower critical : 1715.000 Lower non-critical : 1960.000 Positive Hysteresis : 49.000 Negative Hysteresis : 49.000 Minimum sensor range : Unspecified Maximum sensor range : Unspecified Event Message Control : Per-threshold Readable Thresholds : lcr lnc Settable Thresholds : lcr lnc Threshold Read Mask : lcr lnc Assertion Events : Assertions Enabled : lnc- lcr- Deassertions Enabled : lnc- lcr- """ sensor_status_cmd = 'ipmitoolraw0x0a0x2c0x00' init_sensor_cmd = 'ipmitoolraw0x0a0x2c0x01' sdr_info_cmd = 'ipmitoolsdrinfo' read_sensor_all_cmd = 'ipmitoolsdr-v' read_sensor_temperature_cmd = 'ipmitoolsdr-vtypeTemperature' read_sensor_voltage_cmd = 'ipmitoolsdr-vtypeVoltage' read_sensor_current_cmd = 'ipmitoolsdr-vtypeCurrent' read_sensor_fan_cmd = 'ipmitoolsdr-vtypeFan' sdr_info = ('', '') sensor_temperature = (sensor_temperature_data, '') sensor_voltage = (sensor_voltage_data, '') sensor_current = (sensor_current_data, '') sensor_fan = (sensor_fan_data, '') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2123568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py0000664000175000017500000001164315250267452026326 0ustar00zuulzuul# Copyright 2014 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.ipmi.platform import ipmi_sensor from ceilometer.privsep import ipmitool from ceilometer.tests import base from ceilometer.tests.unit.ipmi.platform import fake_utils class TestIPMISensor(base.BaseTestCase): def setUp(self): super().setUp() ipmitool.ipmi = mock.Mock(side_effect=fake_utils.execute) self.ipmi = ipmi_sensor.IPMISensor() @classmethod def tearDownClass(cls): # reset inited to force an initialization of singleton for next test ipmi_sensor.IPMISensor()._inited = False super().tearDownClass() def test_read_sensor_temperature(self): sensors = self.ipmi.read_sensor_any('Temperature') self.assertTrue(self.ipmi.ipmi_support) # only temperature data returned. self.assertIn('Temperature', sensors) self.assertEqual(1, len(sensors)) # 4 sensor data in total, ignore 1 without 'Sensor Reading'. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py self.assertEqual(3, len(sensors['Temperature'])) sensor = sensors['Temperature']['BB P1 VR Temp (0x20)'] self.assertEqual('25 (+/- 0) degrees C', sensor['Sensor Reading']) def test_read_sensor_voltage(self): sensors = self.ipmi.read_sensor_any('Voltage') # only voltage data returned. self.assertIn('Voltage', sensors) self.assertEqual(1, len(sensors)) # 4 sensor data in total, ignore 1 without 'Sensor Reading'. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py self.assertEqual(3, len(sensors['Voltage'])) sensor = sensors['Voltage']['BB +5.0V (0xd1)'] self.assertEqual('4.959 (+/- 0) Volts', sensor['Sensor Reading']) def test_read_sensor_current(self): sensors = self.ipmi.read_sensor_any('Current') # only Current data returned. self.assertIn('Current', sensors) self.assertEqual(1, len(sensors)) # 3 sensor data in total. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py self.assertEqual(3, len(sensors['Current'])) sensor = sensors['Current']['PS1 Curr Out % (0x58)'] self.assertEqual('11 (+/- 0) unspecified', sensor['Sensor Reading']) def test_read_sensor_power(self): sensors = self.ipmi.read_sensor_any('Current') # only Current data returned. self.assertIn('Current', sensors) self.assertEqual(1, len(sensors)) # 3 sensor data in total. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py self.assertEqual(3, len(sensors['Current'])) sensor = sensors['Current']['Pwr Consumption (0x76)'] self.assertEqual('154 (+/- 0) Watts', sensor['Sensor Reading']) def test_read_sensor_fan(self): sensors = self.ipmi.read_sensor_any('Fan') # only Fan data returned. self.assertIn('Fan', sensors) self.assertEqual(1, len(sensors)) # 2 sensor data in total. # Check ceilometer/tests/ipmi/platform/ipmi_test_data.py self.assertEqual(4, len(sensors['Fan'])) sensor = sensors['Fan']['System Fan 2 (0x32)'] self.assertEqual('4704 (+/- 0) RPM', sensor['Sensor Reading']) class TestNonIPMISensor(base.BaseTestCase): def setUp(self): super().setUp() ipmitool.ipmi = mock.Mock(side_effect=fake_utils.execute_without_ipmi) self.ipmi = ipmi_sensor.IPMISensor() @classmethod def tearDownClass(cls): # reset inited to force an initialization of singleton for next test ipmi_sensor.IPMISensor()._inited = False super().tearDownClass() def test_read_sensor_temperature(self): sensors = self.ipmi.read_sensor_any('Temperature') self.assertFalse(self.ipmi.ipmi_support) # Non-IPMI platform return empty data self.assertEqual({}, sensors) def test_read_sensor_voltage(self): sensors = self.ipmi.read_sensor_any('Voltage') # Non-IPMI platform return empty data self.assertEqual({}, sensors) def test_read_sensor_current(self): sensors = self.ipmi.read_sensor_any('Current') # Non-IPMI platform return empty data self.assertEqual({}, sensors) def test_read_sensor_fan(self): sensors = self.ipmi.read_sensor_any('Fan') # Non-IPMI platform return empty data self.assertEqual({}, sensors) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2671275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/pollsters/0000775000175000017500000000000015250267567022573 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2123568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/pollsters/__init__.py0000664000175000017500000000000015250267452024663 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2133567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/pollsters/base.py0000664000175000017500000000454715250267452024062 0ustar00zuulzuul# Copyright 2014 Intel # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc from unittest import mock import fixtures from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class TestPollsterBase(base.BaseTestCase, metaclass=abc.ABCMeta): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def fake_data(self): """Fake data used for test.""" return None def fake_sensor_data(self, sensor_type): """Fake sensor data used for test.""" return None @abc.abstractmethod def make_pollster(self): """Produce right pollster for test.""" def _test_get_samples(self): nm = mock.Mock() nm.read_inlet_temperature.side_effect = self.fake_data nm.read_outlet_temperature.side_effect = self.fake_data nm.read_power_all.side_effect = self.fake_data nm.read_airflow.side_effect = self.fake_data nm.read_cups_index.side_effect = self.fake_data nm.read_cups_utilization.side_effect = self.fake_data nm.read_sensor_any.side_effect = self.fake_sensor_data self.useFixture(fixtures.MockPatch( 'ceilometer.ipmi.platform.ipmi_sensor.IPMISensor', return_value=nm)) self.mgr = manager.AgentManager(0, self.CONF, ['ipmi']) self.pollster = self.make_pollster() def _verify_metering(self, length, expected_vol=None, node=None): cache = {} resources = ['local_host'] samples = list(self.pollster.get_samples(self.mgr, cache, resources)) self.assertEqual(length, len(samples)) if expected_vol: self.assertTrue(any(s.volume == expected_vol for s in samples)) if node: self.assertTrue(any(s.resource_metadata['node'] == node for s in samples)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2143567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py0000664000175000017500000001054115250267452025507 0ustar00zuulzuul# Copyright 2014 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.ipmi.pollsters import sensor from ceilometer.tests.unit.ipmi.notifications import ipmi_test_data from ceilometer.tests.unit.ipmi.pollsters import base TEMPERATURE_SENSOR_DATA = { 'Temperature': ipmi_test_data.TEMPERATURE_DATA } CURRENT_SENSOR_DATA = { 'Current': ipmi_test_data.CURRENT_DATA } FAN_SENSOR_DATA = { 'Fan': ipmi_test_data.FAN_DATA } FAN_SENSOR_DATA_PERCENT = { 'Fan': ipmi_test_data.FAN_DATA_PERCENT } VOLTAGE_SENSOR_DATA = { 'Voltage': ipmi_test_data.VOLTAGE_DATA } POWER_SENSOR_DATA = { 'Current': ipmi_test_data.POWER_DATA } MISSING_SENSOR_DATA = ipmi_test_data.MISSING_SENSOR['payload']['payload'] MALFORMED_SENSOR_DATA = ipmi_test_data.BAD_SENSOR['payload']['payload'] MISSING_ID_SENSOR_DATA = ipmi_test_data.NO_SENSOR_ID['payload']['payload'] class TestTemperatureSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return TEMPERATURE_SENSOR_DATA def make_pollster(self): return sensor.TemperatureSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(10, float(32), self.CONF.host) class TestMissingSensorData(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return MISSING_SENSOR_DATA def make_pollster(self): return sensor.TemperatureSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(0) class TestMalformedSensorData(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return MALFORMED_SENSOR_DATA def make_pollster(self): return sensor.TemperatureSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(0) class TestMissingSensorId(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return MISSING_ID_SENSOR_DATA def make_pollster(self): return sensor.TemperatureSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(0) class TestFanSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return FAN_SENSOR_DATA def make_pollster(self): return sensor.FanSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(12, float(7140), self.CONF.host) class TestFanPercentSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return FAN_SENSOR_DATA_PERCENT def make_pollster(self): return sensor.FanSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(1, 47.04, self.CONF.host) class TestCurrentSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return CURRENT_SENSOR_DATA def make_pollster(self): return sensor.CurrentSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(1, 0.800, self.CONF.host) class TestVoltageSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return VOLTAGE_SENSOR_DATA def make_pollster(self): return sensor.VoltageSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(4, 3.309, self.CONF.host) class TestPowerSensorPollster(base.TestPollsterBase): def fake_sensor_data(self, sensor_type): return POWER_SENSOR_DATA def make_pollster(self): return sensor.PowerSensorPollster(self.CONF) def test_get_samples(self): self._test_get_samples() self._verify_metering(1, 154, self.CONF.host) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2681277 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/load_balancer/0000775000175000017500000000000015250267567022354 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2143567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/load_balancer/__init__.py0000664000175000017500000000000015250267452024444 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2143567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/load_balancer/test_octavia.py0000664000175000017500000001074115250267452025407 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.load_balancer import discovery from ceilometer.load_balancer import octavia from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes class _BaseTestLBPollster(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestLoadBalancerOperatingStatusPollster(_BaseTestLBPollster): def setUp(self): super().setUp() self.pollster = octavia.LoadBalancerOperatingStatusPollster(self.CONF) self.lbs = fakes.FakeSDKOctaviaClient.default_load_balancers def test_lb_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) self.assertEqual(len(self.lbs), len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(self.lbs[0], field), samples[0].resource_metadata[field]) def test_lb_operating_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) # ONLINE = 1, OFFLINE = 3, ERROR = 5, DEGRADED = 4 self.assertEqual(1, samples[0].volume) self.assertEqual(3, samples[1].volume) self.assertEqual(5, samples[2].volume) self.assertEqual(4, samples[3].volume) def test_get_lb_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) self.assertEqual({'loadbalancer.operating'}, {s.name for s in samples}) def test_lb_discovery(self): discovered_lbs = discovery.LoadBalancerDiscovery( self.CONF).discover(self.manager) expected = len(fakes.FakeSDKOctaviaClient.default_load_balancers) self.assertEqual(expected, len(list(discovered_lbs))) class TestLoadBalancerProvisioningStatusPollster(_BaseTestLBPollster): def setUp(self): super().setUp() self.pollster = octavia.LoadBalancerProvisioningStatusPollster( self.CONF) self.lbs = fakes.FakeSDKOctaviaClient.default_load_balancers def test_lb_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) self.assertEqual(len(self.lbs), len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(self.lbs[0], field), samples[0].resource_metadata[field]) def test_lb_provisioning_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) # ACTIVE = 1, PENDING_UPDATE = 5, ERROR = 3, PENDING_DELETE = 6 self.assertEqual(1, samples[0].volume) self.assertEqual(5, samples[1].volume) self.assertEqual(3, samples[2].volume) self.assertEqual(6, samples[3].volume) def test_get_lb_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.lbs)) self.assertEqual({'loadbalancer.provisioning'}, {s.name for s in samples}) class TestLoadBalancerPollsterUnknownStatus(_BaseTestLBPollster): def test_unknown_operating_status(self): pollster = octavia.LoadBalancerOperatingStatusPollster(self.CONF) samples = list(pollster.get_samples( self.manager, {}, resources=[fakes.LB_UNKNOWN_OPERATING_STATUS])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) def test_unknown_provisioning_status(self): pollster = octavia.LoadBalancerProvisioningStatusPollster(self.CONF) samples = list(pollster.get_samples( self.manager, {}, resources=[fakes.LB_UNKNOWN_PROVISIONING_STATUS])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2681277 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/meter/0000775000175000017500000000000015250267567020722 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2143567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/meter/__init__.py0000664000175000017500000000000015250267452023012 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2143567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/meter/test_meter_plugins.py0000664000175000017500000000623415250267452025206 0ustar00zuulzuul# # Copyright 2016 Mirantis Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.event import trait_plugins from ceilometer.tests import base class TestTimedeltaPlugin(base.BaseTestCase): def setUp(self): super().setUp() self.plugin = trait_plugins.TimedeltaPlugin() def test_timedelta_transformation(self): match_list = [('test.timestamp1', '2016-03-02T15:04:32'), ('test.timestamp2', '2016-03-02T16:04:32')] value = self.plugin.trait_values(match_list) self.assertEqual([3600], value) def test_timedelta_missing_field(self): match_list = [('test.timestamp1', '2016-03-02T15:04:32')] with mock.patch('%s.LOG' % self.plugin.trait_values.__module__) as log: self.assertEqual([None], self.plugin.trait_values(match_list)) log.warning.assert_called_once_with( 'Timedelta plugin is required two timestamp fields to create ' 'timedelta value.') def test_timedelta_exceed_field(self): match_list = [('test.timestamp1', '2016-03-02T15:04:32'), ('test.timestamp2', '2016-03-02T16:04:32'), ('test.timestamp3', '2016-03-02T16:10:32')] with mock.patch('%s.LOG' % self.plugin.trait_values.__module__) as log: self.assertEqual([None], self.plugin.trait_values(match_list)) log.warning.assert_called_once_with( 'Timedelta plugin is required two timestamp fields to create ' 'timedelta value.') def test_timedelta_invalid_timestamp(self): match_list = [('test.timestamp1', '2016-03-02T15:04:32'), ('test.timestamp2', '2016-03-02T15:004:32')] with mock.patch('%s.LOG' % self.plugin.trait_values.__module__) as log: self.assertEqual([None], self.plugin.trait_values(match_list)) msg = log.warning._mock_call_args[0][0] self.assertTrue(msg.startswith('Failed to parse date from set ' 'fields, both fields ') ) def test_timedelta_reverse_timestamp_order(self): match_list = [('test.timestamp1', '2016-03-02T15:15:32'), ('test.timestamp2', '2016-03-02T15:10:32')] value = self.plugin.trait_values(match_list) self.assertEqual([300], value) def test_timedelta_precise_difference(self): match_list = [('test.timestamp1', '2016-03-02T15:10:32.786893'), ('test.timestamp2', '2016-03-02T15:10:32.786899')] value = self.plugin.trait_values(match_list) self.assertEqual([0.000006], value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2153566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/meter/test_notifications.py0000664000175000017500000012173615250267452025207 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer.meter.notifications""" import copy from unittest import mock import fixtures from oslo_cache import core as cache from oslo_config import fixture as config_fixture from oslo_utils import fileutils import yaml from ceilometer import declarative from ceilometer.meter import notifications from ceilometer import service as ceilometer_service from ceilometer.tests import base as test NOTIFICATION = { 'event_type': 'test.create', 'metadata': {'timestamp': '2015-06-19T09:19:35.786893', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': {'user_id': 'e1d870e51c7340cb9d555b15cbfcaec2', 'resource_id': 'bea70e51c7340cb9d555b15cbfcaec23', 'timestamp': '2015-06-19T09:19:35.785330', 'created_at': '2015-06-19T09:25:35.785330', 'launched_at': '2015-06-19T09:25:40.785330', 'message_signature': 'fake_signature1', 'resource_metadata': {'foo': 'bar'}, 'source': '30be1fc9a03c4e94ab05c403a8a377f2: openstack', 'volume': 1.0, 'project_id': '30be1fc9a03c4e94ab05c403a8a377f2', }, 'ctxt': {'tenant': '30be1fc9a03c4e94ab05c403a8a377f2', 'request_id': 'req-da91b4bf-d2b5-43ae-8b66-c7752e72726d', 'user': 'e1d870e51c7340cb9d555b15cbfcaec2'}, 'publisher_id': "foo123" } USER_META = { 'event_type': 'test.create', 'metadata': {'timestamp': '2015-06-19T09:19:35.786893', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': {'user_id': 'e1d870e51c7340cb9d555b15cbfcaec2', 'resource_id': 'bea70e51c7340cb9d555b15cbfcaec23', 'timestamp': '2015-06-19T09:19:35.785330', 'created_at': '2015-06-19T09:25:35.785330', 'launched_at': '2015-06-19T09:25:40.785330', 'message_signature': 'fake_signature1', 'resource_metadata': {'foo': 'bar'}, 'source': '30be1fc9a03c4e94ab05c403a8a377f2: openstack', 'volume': 1.0, 'project_id': '30be1fc9a03c4e94ab05c403a8a377f2', 'metadata': {'metering.xyz': 'abc', 'ignore': 'this'}, }, 'ctxt': {'tenant': '30be1fc9a03c4e94ab05c403a8a377f2', 'request_id': 'req-da91b4bf-d2b5-43ae-8b66-c7752e72726d', 'user': 'e1d870e51c7340cb9d555b15cbfcaec2'}, 'publisher_id': "foo123" } MIDDLEWARE_EVENT = { 'ctxt': {'request_id': 'req-a8bfa89b-d28b-4b95-9e4b-7d7875275650', 'quota_class': None, 'service_catalog': [], 'auth_token': None, 'user_id': None, 'is_admin': True, 'user': None, 'remote_address': None, 'roles': [], 'timestamp': '2013-07-29T06:51:34.348091', 'project_name': None, 'read_deleted': 'no', 'tenant': None, 'instance_lock_checked': False, 'project_id': None, 'user_name': None}, 'event_type': 'objectstore.http.request', 'publisher_id': 'ceilometermiddleware', 'metadata': {'message_id': '6eccedba-120e-4db8-9735-2ad5f061e5ee', 'timestamp': '2013-07-29T06:51:34.474815+00:00', '_unique_id': '0ee26117077648e18d88ac76e28a72e2'}, 'payload': { 'typeURI': 'http: //schemas.dmtf.org/cloud/audit/1.0/event', 'eventTime': '2013-07-29T06:51:34.474815+00:00', 'target': { 'action': 'get', 'typeURI': 'service/storage/object', 'id': 'account', 'metadata': { 'path': '/1.0/CUSTOM_account/container/obj', 'version': '1.0', 'container': 'container', 'object': 'obj' } }, 'observer': { 'id': 'target' }, 'eventType': 'activity', 'measurements': [ { 'metric': { 'metricId': 'openstack: uuid', 'name': 'storage.objects.outgoing.bytes', 'unit': 'B' }, 'result': 28 }, { 'metric': { 'metricId': 'openstack: uuid2', 'name': 'storage.objects.incoming.bytes', 'unit': 'B' }, 'result': 1 } ], 'initiator': { 'typeURI': 'service/security/account/user', 'project_id': None, 'id': 'openstack: 288f6260-bf37-4737-a178-5038c84ba244' }, 'action': 'read', 'outcome': 'success', 'id': 'openstack: 69972bb6-14dd-46e4-bdaf-3148014363dc' } } FULL_MULTI_MSG = { 'event_type': 'full.sample', 'payload': [{ 'counter_name': 'instance1', 'user_id': 'user1', 'user_name': 'fake-name', 'resource_id': 'res1', 'counter_unit': 'ns', 'counter_volume': 28.0, 'project_id': 'proj1', 'project_name': 'fake-name', 'counter_type': 'gauge' }, { 'counter_name': 'instance2', 'user_id': 'user2', 'user_name': 'fake-name', 'resource_id': 'res2', 'counter_unit': '%', 'counter_volume': 1.0, 'project_id': 'proj2', 'project_name': 'fake-name', 'counter_type': 'delta' }], 'ctxt': {'domain': None, 'request_id': 'req-da91b4bf-d2b5-43ae-8b66-c7752e72726d', 'auth_token': None, 'read_only': False, 'resource_uuid': None, 'user_identity': 'fake_user_identity---', 'show_deleted': False, 'tenant': '30be1fc9a03c4e94ab05c403a8a377f2', 'is_admin': True, 'project_domain': None, 'user': 'e1d870e51c7340cb9d555b15cbfcaec2', 'user_domain': None}, 'publisher_id': 'ceilometer.api', 'metadata': {'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e', 'timestamp': '2015-06-19T09:19:35.786893'}, } METRICS_UPDATE = { 'event_type': 'compute.metrics.update', 'payload': { 'metrics': [ {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.frequency', 'value': 1600, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.user.time', 'value': 17421440000000, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.kernel.time', 'value': 7852600000000, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.idle.time', 'value': 1307374400000000, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.iowait.time', 'value': 11697470000000, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.user.percent', 'value': 0.012959045637294348, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.kernel.percent', 'value': 0.005841204961898534, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.idle.percent', 'value': 0.9724985141658965, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.iowait.percent', 'value': 0.008701235234910634, 'source': 'libvirt.LibvirtDriver'}, {'timestamp': '2013-07-29T06:51:34.472416', 'name': 'cpu.percent', 'value': 0.027501485834103515, 'source': 'libvirt.LibvirtDriver'}], 'nodename': 'tianst.sh.intel.com', 'host': 'tianst', 'host_id': '10.0.1.1'}, 'publisher_id': 'compute.tianst.sh.intel.com', 'metadata': {'message_id': '6eccedba-120e-4db8-9735-2ad5f061e5ee', 'timestamp': '2013-07-29 06:51:34.474815', '_unique_id': '0ee26117077648e18d88ac76e28a72e2'}, 'ctxt': {'request_id': 'req-a8bfa89b-d28b-4b95-9e4b-7d7875275650', 'quota_class': None, 'service_catalog': [], 'auth_token': None, 'user_id': None, 'is_admin': True, 'user': None, 'remote_address': None, 'roles': [], 'timestamp': '2013-07-29T06:51:34.348091', 'project_name': None, 'read_deleted': 'no', 'tenant': None, 'instance_lock_checked': False, 'project_id': None, 'user_name': None} } class TestMeterDefinition(test.BaseTestCase): def test_config_definition(self): cfg = dict(name="test", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id") conf = ceilometer_service.prepare_service([], []) handler = notifications.MeterDefinition(cfg, conf, mock.Mock()) self.assertTrue(handler.match_type("test.create")) sample = list(handler.to_samples(NOTIFICATION))[0] self.assertEqual(1.0, sample["volume"]) self.assertEqual("bea70e51c7340cb9d555b15cbfcaec23", sample["resource_id"]) self.assertEqual("30be1fc9a03c4e94ab05c403a8a377f2", sample["project_id"]) def test_config_required_missing_fields(self): cfg = dict() conf = ceilometer_service.prepare_service([], []) try: notifications.MeterDefinition(cfg, conf, mock.Mock()) except declarative.DefinitionException as e: self.assertIn("Required fields ['name', 'type', 'event_type'," " 'unit', 'volume', 'resource_id']" " not specified", str(e)) def test_bad_type_cfg_definition(self): cfg = dict(name="test", type="foo", event_type="bar.create", unit="foo", volume="bar", resource_id="bea70e51c7340cb9d555b15cbfcaec23") conf = ceilometer_service.prepare_service([], []) try: notifications.MeterDefinition(cfg, conf, mock.Mock()) except declarative.DefinitionException as e: self.assertIn("Invalid type foo specified", str(e)) class CacheConfFixture(config_fixture.Config): def setUp(self): super().setUp() self.conf = ceilometer_service.\ prepare_service(argv=[], config_files=[]) cache.configure(self.conf) class TestMeterProcessing(test.BaseTestCase): def setUp(self): super().setUp() self.CONF = ceilometer_service.prepare_service([], []) dict_conf_fixture = CacheConfFixture(self.CONF) self.useFixture(dict_conf_fixture) dict_conf_fixture.config(enabled=True, group='cache') dict_conf_fixture.config(expiration_time=600, backend='oslo_cache.dict', group='cache') dict_conf_fixture.config(identity_name_discovery=True, group='polling') self.CONF = dict_conf_fixture.conf self.path = self.useFixture(fixtures.TempDir()).path self.handler = notifications.ProcessMeterNotifications( self.CONF, mock.Mock()) def _load_meter_def_file(self, cfgs=None): self.CONF.set_override('meter_definitions_dirs', [self.path], group='meter') cfgs = cfgs or [] if not isinstance(cfgs, list): cfgs = [cfgs] meter_cfg_files = list() for cfg in cfgs: cfg = cfg.encode('utf-8') meter_cfg_files.append(fileutils.write_to_tempfile(content=cfg, path=self.path, prefix="meters", suffix=".yaml")) self.handler.definitions = self.handler._load_definitions() @mock.patch('ceilometer.meter.notifications.LOG') def test_bad_meter_definition_skip(self, LOG): cfg = yaml.dump( {'metric': [dict(name="good_test_1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id"), dict(name="bad_test_2", type="bad_type", event_type="bar.create", unit="foo", volume="bar", resource_id="bea70e51c7340cb9d555b15cbfcaec23"), dict(name="good_test_3", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) self.assertEqual(2, len(self.handler.definitions)) args, kwargs = LOG.error.call_args_list[0] self.assertEqual("Error loading meter definition: %s", args[0]) self.assertTrue( str(args[1]).endswith("Invalid type bad_type specified")) def test_jsonpath_values_parsed(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('test1', s1['name']) self.assertEqual(1.0, s1['volume']) self.assertEqual('bea70e51c7340cb9d555b15cbfcaec23', s1['resource_id']) self.assertEqual('30be1fc9a03c4e94ab05c403a8a377f2', s1['project_id']) def test_multiple_meter(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id"), dict(name="test2", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) data = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(2, len(data)) expected_names = ['test1', 'test2'] for s in data: self.assertIn(s.as_dict()['name'], expected_names) def test_unmatched_meter(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.update", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(0, len(c)) def test_regex_match_meter(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) def test_default_timestamp(self): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'][1] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", multi="name")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual(MIDDLEWARE_EVENT['metadata']['timestamp'], s1['timestamp']) def test_custom_timestamp(self): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'][1] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", multi="name", timestamp='$.payload.eventTime')]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual(MIDDLEWARE_EVENT['payload']['eventTime'], s1['timestamp']) def test_custom_timestamp_expr_meter(self): cfg = yaml.dump( {'metric': [dict(name='compute.node.cpu.frequency', event_type="compute.metrics.update", type='gauge', unit="ns", volume="$.payload.metrics[?(@.name='cpu.frequency')]" ".value", resource_id="'prefix-' + $.payload.nodename", timestamp="$.payload.metrics" "[?(@.name='cpu.frequency')].timestamp")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(METRICS_UPDATE)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('compute.node.cpu.frequency', s1['name']) self.assertEqual("2013-07-29T06:51:34.472416+00:00", s1['timestamp']) def test_default_metadata(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() meta = NOTIFICATION['payload'].copy() meta['host'] = NOTIFICATION['publisher_id'] meta['event_type'] = NOTIFICATION['event_type'] self.assertEqual(meta, s1['resource_metadata']) def test_datetime_plugin(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="gauge", unit="sec", volume={"fields": ["$.payload.created_at", "$.payload.launched_at"], "plugin": "timedelta"}, resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual(5.0, s1['volume']) def test_custom_metadata(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id", metadata={'proj': '$.payload.project_id', 'dict': '$.payload.resource_metadata'})]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() meta = {'proj': s1['project_id'], 'dict': NOTIFICATION['payload']['resource_metadata']} self.assertEqual(meta, s1['resource_metadata']) def test_user_meta(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id", user_metadata="$.payload.metadata",)]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(USER_META)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() meta = {'user_metadata': {'xyz': 'abc'}} self.assertEqual(meta, s1['resource_metadata']) def test_user_meta_and_custom(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.*", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id", user_metadata="$.payload.metadata", metadata={'proj': '$.payload.project_id'})]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(USER_META)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() meta = {'user_metadata': {'xyz': 'abc'}, 'proj': s1['project_id']} self.assertEqual(meta, s1['resource_metadata']) def test_multi_match_event_meter(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id"), dict(name="test2", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(2, len(c)) def test_multi_meter_payload(self): cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup=["name", "volume", "unit"])]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(MIDDLEWARE_EVENT)) self.assertEqual(2, len(c)) s1 = c[0].as_dict() self.assertEqual('storage.objects.outgoing.bytes', s1['name']) self.assertEqual(28, s1['volume']) self.assertEqual('B', s1['unit']) s2 = c[1].as_dict() self.assertEqual('storage.objects.incoming.bytes', s2['name']) self.assertEqual(1, s2['volume']) self.assertEqual('B', s2['unit']) def test_multi_meter_payload_single(self): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'][1] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup=["name", "unit"])]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('storage.objects.outgoing.bytes', s1['name']) self.assertEqual(28, s1['volume']) self.assertEqual('B', s1['unit']) def test_multi_meter_payload_none(self): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup="name")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(0, len(c)) @mock.patch( 'ceilometer.cache_utils.CacheClient._resolve_uuid_from_keystone' ) def test_multi_meter_payload_all_multi(self, resolved_uuid): resolved_uuid.return_value = "fake-name" cfg = yaml.dump( {'metric': [dict(name="$.payload.[*].counter_name", event_type="full.sample", type="$.payload.[*].counter_type", unit="$.payload.[*].counter_unit", volume="$.payload.[*].counter_volume", resource_id="$.payload.[*].resource_id", project_id="$.payload.[*].project_id", user_id="$.payload.[*].user_id", lookup=['name', 'type', 'unit', 'volume', 'resource_id', 'project_id', 'user_id'])]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(FULL_MULTI_MSG)) self.assertEqual(2, len(c)) msg = FULL_MULTI_MSG['payload'] for idx, val in enumerate(c): s1 = val.as_dict() self.assertEqual(msg[idx]['counter_name'], s1['name']) self.assertEqual(msg[idx]['counter_volume'], s1['volume']) self.assertEqual(msg[idx]['counter_unit'], s1['unit']) self.assertEqual(msg[idx]['counter_type'], s1['type']) self.assertEqual(msg[idx]['resource_id'], s1['resource_id']) self.assertEqual(msg[idx]['project_id'], s1['project_id']) self.assertEqual(msg[idx]['user_id'], s1['user_id']) self.assertEqual(msg[idx]['project_name'], s1['project_name']) self.assertEqual(msg[idx]['user_name'], s1['user_name']) @mock.patch('ceilometer.meter.notifications.LOG') def test_multi_meter_payload_invalid_missing(self, LOG): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'][0]['result'] del event['payload']['measurements'][1]['result'] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup=["name", "unit", "volume"])]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(0, len(c)) log_called_args = LOG.warning.call_args_list self.assertEqual( 'Only 0 fetched meters contain "volume" field instead of 2.', log_called_args[0][0][0] % log_called_args[0][0][1]) @mock.patch('ceilometer.meter.notifications.LOG') def test_multi_meter_payload_invalid_short(self, LOG): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'][0]['result'] cfg = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup=["name", "unit", "volume"])]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(event)) self.assertEqual(0, len(c)) log_called_args = LOG.warning.call_args_list self.assertEqual( 'Only 1 fetched meters contain "volume" field instead of 2.', log_called_args[0][0][0] % log_called_args[0][0][1]) def test_arithmetic_expr_meter(self): cfg = yaml.dump( {'metric': [dict(name='compute.node.cpu.percent', event_type="compute.metrics.update", type='gauge', unit="percent", volume="$.payload.metrics[" "?(@.name='cpu.percent')].value" " * 100", resource_id="$.payload.host + '_'" " + $.payload.nodename")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(METRICS_UPDATE)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('compute.node.cpu.percent', s1['name']) self.assertEqual(2.7501485834103514, s1['volume']) self.assertEqual("tianst_tianst.sh.intel.com", s1['resource_id']) def test_string_expr_meter(self): cfg = yaml.dump( {'metric': [dict(name='compute.node.cpu.frequency', event_type="compute.metrics.update", type='gauge', unit="ns", volume="$.payload.metrics[?(@.name='cpu.frequency')]" ".value", resource_id="$.payload.host + '_'" " + $.payload.nodename")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(METRICS_UPDATE)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('compute.node.cpu.frequency', s1['name']) self.assertEqual(1600, s1['volume']) self.assertEqual("tianst_tianst.sh.intel.com", s1['resource_id']) def test_prefix_expr_meter(self): cfg = yaml.dump( {'metric': [dict(name='compute.node.cpu.frequency', event_type="compute.metrics.update", type='gauge', unit="ns", volume="$.payload.metrics[?(@.name='cpu.frequency')]" ".value", resource_id="'prefix-' + $.payload.nodename")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(METRICS_UPDATE)) self.assertEqual(1, len(c)) s1 = c[0].as_dict() self.assertEqual('compute.node.cpu.frequency', s1['name']) self.assertEqual(1600, s1['volume']) self.assertEqual("prefix-tianst.sh.intel.com", s1['resource_id']) def test_duplicate_meter(self): cfg = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id"), dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file(cfg) c = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(c)) def test_multi_files_multi_meters(self): cfg1 = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) cfg2 = yaml.dump( {'metric': [dict(name="test2", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file([cfg1, cfg2]) data = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(2, len(data)) expected_names = ['test1', 'test2'] for s in data: self.assertIn(s.as_dict()['name'], expected_names) def test_multi_files_duplicate_meter(self): cfg1 = yaml.dump( {'metric': [dict(name="test", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) cfg2 = yaml.dump( {'metric': [dict(name="test", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file([cfg1, cfg2]) data = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(data)) self.assertEqual(data[0].as_dict()['name'], 'test') def test_multi_files_empty_payload(self): event = copy.deepcopy(MIDDLEWARE_EVENT) del event['payload']['measurements'] cfg1 = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup="name")]}) cfg2 = yaml.dump( {'metric': [dict(name="$.payload.measurements.[*].metric.[*].name", event_type="objectstore.http.request", type="delta", unit="$.payload.measurements.[*].metric.[*].unit", volume="$.payload.measurements.[*].result", resource_id="$.payload.target_id", project_id="$.payload.initiator.project_id", lookup="name")]}) self._load_meter_def_file([cfg1, cfg2]) data = list(self.handler.build_sample(event)) self.assertEqual(0, len(data)) def test_multi_files_unmatched_meter(self): cfg1 = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) cfg2 = yaml.dump( {'metric': [dict(name="test2", event_type="test.update", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file([cfg1, cfg2]) data = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(1, len(data)) self.assertEqual(data[0].as_dict()['name'], 'test1') @mock.patch('ceilometer.meter.notifications.LOG') def test_multi_files_bad_meter(self, LOG): cfg1 = yaml.dump( {'metric': [dict(name="test1", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id"), dict(name="bad_test", type="bad_type", event_type="bar.create", unit="foo", volume="bar", resource_id="bea70e51c7340cb9d555b15cbfcaec23")]}) cfg2 = yaml.dump( {'metric': [dict(name="test2", event_type="test.create", type="delta", unit="B", volume="$.payload.volume", resource_id="$.payload.resource_id", project_id="$.payload.project_id")]}) self._load_meter_def_file([cfg1, cfg2]) data = list(self.handler.build_sample(NOTIFICATION)) self.assertEqual(2, len(data)) expected_names = ['test1', 'test2'] for s in data: self.assertIn(s.as_dict()['name'], expected_names) args, kwargs = LOG.error.call_args_list[0] self.assertEqual("Error loading meter definition: %s", args[0]) self.assertTrue( str(args[1]).endswith("Invalid type bad_type specified")) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2691276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/0000775000175000017500000000000015250267567021277 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2163568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/__init__.py0000664000175000017500000000000015250267452023367 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2691276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/services/0000775000175000017500000000000015250267567023122 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2163568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/services/__init__.py0000664000175000017500000000000015250267452025212 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2163568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/services/test_fwaas.py0000664000175000017500000002033115250267452025624 0ustar00zuulzuul# # Copyright 2014 Cisco Systems,Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from ceilometer.network.services import discovery from ceilometer.network.services import fwaas from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class _BaseTestFWPollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestFirewallPollster(_BaseTestFWPollster): def setUp(self): super().setUp() self.pollster = fwaas.FirewallPollster(self.CONF) self.fake_fw = self.fake_get_fw_service() self.useFixture(fixtures.MockPatch('ceilometer.neutron_client.Client.' 'firewall_get_all', return_value=self.fake_fw)) @staticmethod def fake_get_fw_service(): return [{'status': 'ACTIVE', 'name': 'myfw1', 'description': '', 'admin_state_up': True, 'id': 'fdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'ingress_firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'egress_firewall_policy_id': 'cce3d818-bdcb-4e4b-b47f-5650dc8a9d7b', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'INACTIVE', 'name': 'myfw2', 'description': '', 'admin_state_up': True, 'id': 'e0d707dc-6194-4471-8286-0635bf65a055', 'ingress_firewall_policy_id': 'e0d707dc-6194-4471-8286-0635bf65a055', 'egress_firewall_policy_id': None, 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'PENDING_CREATE', 'name': 'myfw3', 'description': '', 'admin_state_up': True, 'id': 'e538d353-31e9-4581-a511-0a487ff71d0d', 'ingress_firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'egress_firewall_policy_id': 'dde3d818-bdcb-4e4b-b47f-5650dc8a9d7c', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'ERROR', 'name': 'myfw4', 'description': '', 'admin_state_up': True, 'id': '06f698c4-dc63-43c4-a2d9-7b978e80f09a', 'ingress_firewall_policy_id': 'bef98f97-789f-418e-82ad-3e5d69618916', 'egress_firewall_policy_id': None, 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'UNKNOWN', 'name': 'myfw5', 'description': '', 'admin_state_up': True, 'id': 'c65a1bec-ab59-44ce-b784-1c725f427998', 'ingress_firewall_policy_id': 'd45b975e-738f-42c3-a4b3-760d3a58ab51', 'egress_firewall_policy_id': 'e45b975e-738f-42c3-a4b3-760d3a58ab52', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': None, 'name': 'myfw6', 'description': '', 'admin_state_up': True, 'id': 'ab5d19ff-32a8-49e5-aa2b-d008157359d9', 'ingress_firewall_policy_id': '79b9c933-2a7c-4f93-bbf9-d165f0326581', 'egress_firewall_policy_id': None, 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, ] def test_fw_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fw)) self.assertEqual(len(self.fake_fw), len(samples)) self.assertEqual({fw['id'] for fw in self.fake_fw}, {sample.resource_id for sample in samples}) samples_dict = {sample.resource_id: sample for sample in samples} for fw in self.fake_fw: sample = samples_dict[fw['id']] for field in self.pollster.FIELDS: self.assertEqual(fw[field], sample.resource_metadata[field]) def test_vpn_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fw)) self.assertEqual(1, samples[0].volume) self.assertEqual(0, samples[1].volume) self.assertEqual(2, samples[2].volume) self.assertEqual(7, samples[3].volume) self.assertEqual(-1, samples[4].volume) self.assertEqual(-1, samples[5].volume) def test_get_vpn_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fw)) self.assertEqual({'network.services.firewall'}, {s.name for s in samples}) def test_vpn_discovery(self): discovered_fws = discovery.FirewallDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(self.fake_fw), len(discovered_fws)) for vpn in self.fake_fw: self.assertIn(vpn, discovered_fws) class TestIPSecConnectionsPollster(_BaseTestFWPollster): def setUp(self): super().setUp() self.pollster = fwaas.FirewallPolicyPollster(self.CONF) fake_fw_policy = self.fake_get_fw_policy() self.useFixture(fixtures.MockPatch('ceilometer.neutron_client.Client.' 'fw_policy_get_all', return_value=fake_fw_policy)) @staticmethod def fake_get_fw_policy(): return [{'name': 'my_fw_policy', 'description': 'fw_policy', 'admin_state_up': True, 'project_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a', 'firewall_rules': [{'enabled': True, 'action': 'allow', 'ip_version': 4, 'protocol': 'tcp', 'destination_port': '80', 'source_ip_address': '10.24.4.2'}, {'enabled': True, 'action': 'deny', 'ip_version': 4, 'protocol': 'tcp', 'destination_port': '22'}], 'shared': True, 'audited': True, 'id': 'fdfbcec-fdcb-fg4b-de7f-6650dc8a9d7a'} ] def test_policy_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_fw_policy())) self.assertEqual(1, len(samples)) for field in self.pollster.FIELDS: self.assertEqual(self.fake_get_fw_policy()[0][field], samples[0].resource_metadata[field]) def test_get_policy_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_fw_policy())) self.assertEqual({'network.services.firewall.policy'}, {s.name for s in samples}) def test_fw_policy_discovery(self): discovered_policy = discovery.FirewallPolicyDiscovery( self.CONF).discover(self.manager) self.assertEqual(1, len(discovered_policy)) self.assertEqual(self.fake_get_fw_policy(), discovered_policy) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2163568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/services/test_vpnaas.py0000664000175000017500000002243515250267452026022 0ustar00zuulzuul# # Copyright 2014 Cisco Systems,Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from ceilometer.network.services import discovery from ceilometer.network.services import vpnaas from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class _BaseTestVPNPollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestVPNServicesPollster(_BaseTestVPNPollster): def setUp(self): super().setUp() self.pollster = vpnaas.VPNServicesPollster(self.CONF) self.fake_vpn = self.fake_get_vpn_service() self.useFixture(fixtures.MockPatch('ceilometer.neutron_client.Client.' 'vpn_get_all', return_value=self.fake_vpn)) @staticmethod def fake_get_vpn_service(): return [{'status': 'ACTIVE', 'name': 'myvpn1', 'description': '', 'is_admin_state_up': True, 'id': 'fdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'flavor_id': None, 'external_v4_ip': '192.168.1.232', 'external_v6_ip': '2001:db8::158'}, {'status': 'INACTIVE', 'name': 'myvpn2', 'description': '', 'is_admin_state_up': True, 'id': 'cdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'flavor_id': None, 'external_v4_ip': '192.168.1.233', 'external_v6_ip': '2001:db8::159'}, {'status': 'PENDING_CREATE', 'name': 'myvpn3', 'description': '', 'id': 'bdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'is_admin_state_up': True, 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'flavor_id': 'flavor-123', 'external_v4_ip': '192.168.1.234', 'external_v6_ip': '2001:db8::160'}, {'status': 'error', 'name': 'myvpn4', 'description': '', 'id': 'edde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'is_admin_state_up': False, 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'flavor_id': None, 'external_v4_ip': '192.168.1.235', 'external_v6_ip': '2001:db8::161'}, {'status': 'UNKNOWN', 'name': 'myvpn5', 'description': '', 'id': '34e6383a-b1ab-4602-b26a-a1ae7b759212', 'is_admin_state_up': False, 'subnet_id': '8c20bbbf-1409-4bc4-b652-3aeda66746c1', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': '0e5c9333-2ef5-4c90-9cca-5cc898515da4', 'flavor_id': None, 'external_v4_ip': '192.168.1.236', 'external_v6_ip': '2001:db8::162'}, {'status': None, 'name': 'myvpn6', 'description': '', 'id': '6e94ff61-8dea-4154-98f1-4020e4b2cecd', 'is_admin_state_up': False, 'subnet_id': '5e2a20c3-547a-43e4-90c5-26d32ea42d10', 'project_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': '5b14df87-60c1-4fc7-8ad5-7811b2199c7f', 'flavor_id': None, 'external_v4_ip': '192.168.1.237', 'external_v6_ip': '2001:db8::163'}, ] def test_vpn_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_vpn)) self.assertEqual(len(self.fake_vpn), len(samples)) self.assertEqual({vpn['id'] for vpn in self.fake_vpn}, {sample.resource_id for sample in samples}) samples_dict = {sample.resource_id: sample for sample in samples} for vpn in self.fake_vpn: sample = samples_dict[vpn['id']] for field in self.pollster.FIELDS: self.assertEqual(vpn[field], sample.resource_metadata[field]) def test_vpn_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_vpn)) self.assertEqual(1, samples[0].volume) self.assertEqual(0, samples[1].volume) self.assertEqual(2, samples[2].volume) self.assertEqual(7, samples[3].volume) self.assertEqual(-1, samples[4].volume) self.assertEqual(-1, samples[5].volume) def test_get_vpn_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_vpn)) self.assertEqual({'network.services.vpn'}, {s.name for s in samples}) def test_vpn_discovery(self): discovered_vpns = discovery.VPNServicesDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(self.fake_vpn), len(discovered_vpns)) for vpn in self.fake_get_vpn_service(): self.assertIn(vpn, discovered_vpns) class TestIPSecConnectionsPollster(_BaseTestVPNPollster): def setUp(self): super().setUp() self.pollster = vpnaas.IPSecConnectionsPollster(self.CONF) fake_conns = self.fake_get_ipsec_connections() self.useFixture(fixtures.MockPatch('ceilometer.neutron_client.Client.' 'ipsec_site_connections_get_all', return_value=fake_conns)) @staticmethod def fake_get_ipsec_connections(): return [{'name': 'connection1', 'description': 'Remote-connection1', 'peer_address': '192.168.1.10', 'peer_id': '192.168.1.10', 'peer_cidrs': ['192.168.2.0/24', '192.168.3.0/24'], 'mtu': 1500, 'psk': 'abcd', 'initiator': 'bi-directional', 'ikepolicy_id': 'ade3d818-fdcb-fg4b-de7f-4550dc8a9d7a', 'ipsecpolicy_id': 'fce3d818-fdcb-fg4b-de7f-7850dc8a9d7a', 'vpnservice_id': 'dce3d818-fdcb-fg4b-de7f-5650dc8a9d7a', 'peer_ep_group_id': '5cd73c38-1292-461e-9ff4-705b1c13a31c', 'is_admin_state_up': True, 'status': 'ACTIVE', 'project_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a', 'tenant_id': 'abe3d818-fdcb-fg4b-de7f-6650dc8a9d7a', 'id': 'fdfbcec-fdcb-fg4b-de7f-6650dc8a9d7a'} ] def test_conns_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_ipsec_connections())) self.assertEqual(1, len(samples)) for field in self.pollster.FIELDS: self.assertEqual(self.fake_get_ipsec_connections()[0][field], samples[0].resource_metadata[field]) def test_get_conns_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_ipsec_connections())) self.assertEqual({'network.services.vpn.connections'}, {s.name for s in samples}) def test_conns_discovery(self): discovered_conns = discovery.IPSecConnectionsDiscovery( self.CONF).discover(self.manager) self.assertEqual(1, len(discovered_conns)) self.assertEqual(self.fake_get_ipsec_connections(), discovered_conns) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2163568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/network/test_floating_ip.py0000664000175000017500000001317515250267452025203 0ustar00zuulzuul# Copyright 2016 Sungard Availability Services # Copyright 2016 Red Hat # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fixtures from ceilometer.network import floatingip from ceilometer.network.services import discovery from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class _BaseTestFloatingIPPollster(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestFloatingIPPollster(_BaseTestFloatingIPPollster): def setUp(self): super().setUp() self.pollster = floatingip.FloatingIPPollster(self.CONF) self.fake_fip = self.fake_get_fip_service() self.useFixture(fixtures.MockPatch('ceilometer.neutron_client.Client.' 'fip_get_all', return_value=self.fake_fip)) @staticmethod def fake_get_fip_service(): return [{'router_id': 'e24f8a37-1bb7-49e4-833c-049bb21986d2', 'status': 'ACTIVE', 'tenant_id': '54a00c50ee4c4396b2f8dc220a2bed57', 'floating_network_id': 'f41f399e-d63e-47c6-9a19-21c4e4fbbba0', 'fixed_ip_address': '10.0.0.6', 'floating_ip_address': '65.79.162.11', 'port_id': '93a0d2c7-a397-444c-9d75-d2ac89b6f209', 'id': '18ca27bf-72bc-40c8-9c13-414d564ea367'}, {'router_id': 'astf8a37-1bb7-49e4-833c-049bb21986d2', 'status': 'DOWN', 'tenant_id': '34a00c50ee4c4396b2f8dc220a2bed57', 'floating_network_id': 'gh1f399e-d63e-47c6-9a19-21c4e4fbbba0', 'fixed_ip_address': '10.0.0.7', 'floating_ip_address': '65.79.162.12', 'port_id': '453a0d2c7-a397-444c-9d75-d2ac89b6f209', 'id': 'jkca27bf-72bc-40c8-9c13-414d564ea367'}, {'router_id': 'e2478937-1bb7-49e4-833c-049bb21986d2', 'status': 'error', 'tenant_id': '54a0gggg50ee4c4396b2f8dc220a2bed57', 'floating_network_id': 'po1f399e-d63e-47c6-9a19-21c4e4fbbba0', 'fixed_ip_address': '10.0.0.8', 'floating_ip_address': '65.79.162.13', 'port_id': '67a0d2c7-a397-444c-9d75-d2ac89b6f209', 'id': '90ca27bf-72bc-40c8-9c13-414d564ea367'}, {'router_id': 'a27ac630-939f-4e2e-bbc3-09a6b4f19a77', 'status': 'UNKNOWN', 'tenant_id': '54a0gggg50ee4c4396b2f8dc220a2bed57', 'floating_network_id': '4d0c3f4f-79c7-40ff-9b0d-6e3a396547db', 'fixed_ip_address': '10.0.0.9', 'floating_ip_address': '65.79.162.14', 'port_id': '59cc6efa-7c89-4730-b051-b15f594e6728', 'id': 'a8a11884-7666-4f35-901e-dbb84e7111b5'}, {'router_id': '7eb0adde-6c3b-4a77-9714-f718a17afb83', 'status': None, 'tenant_id': '54a0gggg50ee4c4396b2f8dc220a2bed57', 'floating_network_id': 'bd6290e6-b014-4cd3-91f0-7e8a1b4c26ab', 'fixed_ip_address': '10.0.0.10', 'floating_ip_address': '65.79.162.15', 'port_id': 'd3b9436d-4b2b-4832-852b-34df7513c935', 'id': '27c539ca-94ce-42fc-a639-1bf2c8690d76'}] def test_fip_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fip)) self.assertEqual(len(self.fake_fip), len(samples)) self.assertEqual({fip['id'] for fip in self.fake_fip}, {sample.resource_id for sample in samples}) samples_dict = {sample.resource_id: sample for sample in samples} for fip in self.fake_fip: sample = samples_dict[fip['id']] for field in self.pollster.FIELDS: self.assertEqual(fip[field], sample.resource_metadata[field]) def test_fip_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fip)) self.assertEqual(1, samples[0].volume) self.assertEqual(3, samples[1].volume) self.assertEqual(7, samples[2].volume) self.assertEqual(-1, samples[3].volume) self.assertEqual(-1, samples[4].volume) def test_get_fip_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_fip)) self.assertEqual({'ip.floating'}, {s.name for s in samples}) def test_fip_discovery(self): discovered_fips = discovery.FloatingIPDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(self.fake_fip), len(discovered_fips)) for fip in self.fake_fip: self.assertIn(fip, discovered_fips) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2701275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/objectstore/0000775000175000017500000000000015250267567022131 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2173567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/objectstore/__init__.py0000664000175000017500000000000015250267452024221 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2173567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/objectstore/test_rgw.py0000664000175000017500000002574515250267452024347 0ustar00zuulzuul# Copyright 2015 Reliance Jio Infocomm Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections from unittest import mock import fixtures from keystoneauth1 import exceptions import testscenarios.testcase from ceilometer.objectstore import rgw from ceilometer.objectstore import rgw_client from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base bucket_list1 = [rgw_client.RGWAdminClient.Bucket('somefoo1', 10, 7)] bucket_list2 = [rgw_client.RGWAdminClient.Bucket('somefoo2', 2, 9)] bucket_list3 = [rgw_client.RGWAdminClient.Bucket('unlisted', 100, 100)] GET_BUCKETS = [('tenant-000', {'num_buckets': 2, 'size': 1042, 'num_objects': 1001, 'buckets': bucket_list1}), ('tenant-001', {'num_buckets': 2, 'size': 1042, 'num_objects': 1001, 'buckets': bucket_list2}), ('tenant-002-ignored', {'num_buckets': 2, 'size': 1042, 'num_objects': 1001, 'buckets': bucket_list3})] GET_USAGE = [('tenant-000', 10), ('tenant-001', 11), ('tenant-002-ignored', 12)] Tenant = collections.namedtuple('Tenant', 'id') ASSIGNED_TENANTS = [Tenant('tenant-000'), Tenant('tenant-001')] class TestManager(manager.AgentManager): def __init__(self, worker_id, conf): super().__init__(worker_id, conf) self._keystone = mock.Mock() self._catalog = (self._keystone.session.auth.get_access. return_value.service_catalog) self._catalog.url_for.return_value = 'http://foobar/endpoint' class TestRgwPollster(testscenarios.testcase.WithScenarios, base.BaseTestCase): # Define scenarios to run all of the tests against all of the # pollsters. scenarios = [ ('radosgw.objects', {'factory': rgw.ObjectsPollster}), ('radosgw.objects.size', {'factory': rgw.ObjectsSizePollster}), ('radosgw.objects.containers', {'factory': rgw.ObjectsContainersPollster}), ('radosgw.containers.objects', {'factory': rgw.ContainersObjectsPollster}), ('radosgw.containers.objects.size', {'factory': rgw.ContainersSizePollster}), ('radosgw.api.request', {'factory': rgw.UsagePollster}), ] @staticmethod def fake_ks_service_catalog_url_for(*args, **kwargs): raise exceptions.EndpointNotFound("Fake keystone exception") def fake_iter_accounts(self, ksclient, cache, tenants): tenant_ids = [t.id for t in tenants] for i in self.ACCOUNTS: if i[0] in tenant_ids: yield i def setUp(self): super().setUp() conf = service.prepare_service([], []) conf.set_override('radosgw', 'object-store', group='service_types') self.pollster = self.factory(conf) self.manager = TestManager(0, conf) if self.pollster.CACHE_KEY_METHOD == 'rgw.get_bucket': self.ACCOUNTS = GET_BUCKETS else: self.ACCOUNTS = GET_USAGE def tearDown(self): super().tearDown() rgw._Base._ENDPOINT = None def test_iter_accounts_no_cache(self): cache = {} with fixtures.MockPatchObject(self.factory, '_get_account_info', return_value=[]): data = list(self.pollster._iter_accounts(mock.Mock(), cache, ASSIGNED_TENANTS)) self.assertIn(self.pollster.CACHE_KEY_METHOD, cache) self.assertEqual([], data) def test_iter_accounts_cached(self): # Verify that if a method has already been called, _iter_accounts # uses the cached version and doesn't call rgw_clinet. mock_method = mock.Mock() mock_method.side_effect = AssertionError( 'should not be called', ) api_method = 'get_%s' % self.pollster.METHOD with fixtures.MockPatchObject(rgw_client.RGWAdminClient, api_method, new=mock_method): cache = {self.pollster.CACHE_KEY_METHOD: [self.ACCOUNTS[0]]} data = list(self.pollster._iter_accounts(mock.Mock(), cache, ASSIGNED_TENANTS)) self.assertEqual([self.ACCOUNTS[0]], data) def test_metering(self): with fixtures.MockPatchObject(self.factory, '_iter_accounts', side_effect=self.fake_iter_accounts): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(2, len(samples), self.pollster.__class__) def test_get_meter_names(self): with fixtures.MockPatchObject(self.factory, '_iter_accounts', side_effect=self.fake_iter_accounts): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual({samples[0].name}, {s.name for s in samples}) def test_only_poll_assigned(self): mock_method = mock.MagicMock() endpoint = 'http://127.0.0.1:8000/admin' api_method = 'get_%s' % self.pollster.METHOD with fixtures.MockPatchObject(rgw_client.RGWAdminClient, api_method, new=mock_method): with fixtures.MockPatchObject( self.manager._catalog, 'url_for', return_value=endpoint): list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) expected = [mock.call(t.id) for t in ASSIGNED_TENANTS] self.assertEqual(expected, mock_method.call_args_list) def test_get_endpoint_only_once(self): mock_url_for = mock.MagicMock() mock_url_for.return_value = '/endpoint' api_method = 'get_%s' % self.pollster.METHOD with fixtures.MockPatchObject(rgw_client.RGWAdminClient, api_method, new=mock.MagicMock()): with fixtures.MockPatchObject( self.manager._catalog, 'url_for', new=mock_url_for): list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(1, mock_url_for.call_count) def test_endpoint_notfound(self): with fixtures.MockPatchObject( self.manager._catalog, 'url_for', side_effect=self.fake_ks_service_catalog_url_for): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(0, len(samples)) class TestRgwPollsterTLS(base.BaseTestCase): """Tests for the RGW pollster TLS configuration options.""" def setUp(self): super().setUp() self.conf = service.prepare_service([], []) self.conf.set_override('radosgw', 'object-store', group='service_types') def test_verify_default_insecure(self): """Default insecure=True should set verify=False.""" pollster = rgw.ObjectsPollster(self.conf) self.assertFalse(pollster.verify) def test_verify_insecure_false(self): """insecure=False with no cafile should set verify=True.""" self.conf.set_override('insecure', False, group='rgw_client') pollster = rgw.ObjectsPollster(self.conf) self.assertTrue(pollster.verify) def test_verify_cafile(self): """insecure=False with cafile should set verify to the path.""" self.conf.set_override('insecure', False, group='rgw_client') self.conf.set_override('cafile', '/path/to/ca.pem', group='rgw_client') pollster = rgw.ObjectsPollster(self.conf) self.assertEqual('/path/to/ca.pem', pollster.verify) def test_verify_insecure_overrides_cafile(self): """insecure=True should set verify=False even if cafile is set.""" self.conf.set_override('insecure', True, group='rgw_client') self.conf.set_override('cafile', '/path/to/ca.pem', group='rgw_client') pollster = rgw.ObjectsPollster(self.conf) self.assertFalse(pollster.verify) def test_tls_versions_from_config(self): self.conf.set_override('tls_min_version', '1.2', group='rgw_client') self.conf.set_override('tls_max_version', '1.3', group='rgw_client') pollster = rgw.ObjectsPollster(self.conf) self.assertEqual('1.2', pollster.tls_min_version) self.assertEqual('1.3', pollster.tls_max_version) def test_tls_versions_default_unset(self): """Unset tls_*_version options use awscurl defaults (None).""" pollster = rgw.ObjectsPollster(self.conf) self.assertIsNone(pollster.tls_min_version) self.assertIsNone(pollster.tls_max_version) def test_none_tls_min_version_set(self): with self.assertRaisesRegex( ValueError, r"Valid values are \[1.2, 1.3\], but found ''"): self.conf.set_override('tls_min_version', '', group='rgw_client') class TestRgwPollsterMissingConfig(base.BaseTestCase): def setUp(self): super().setUp() self.conf = service.prepare_service([], []) self.manager = TestManager(0, self.conf) def tearDown(self): super().tearDown() rgw._Base._ENDPOINT = None def test_get_endpoint_does_not_raise_when_config_missing(self): result = rgw._Base._get_endpoint(self.conf, self.manager.keystone) self.assertIsNone(result) def test_get_endpoint_logs_error_when_config_missing(self): with mock.patch.object(rgw.LOG, 'error') as mock_log: rgw._Base._get_endpoint(self.conf, self.manager.keystone) mock_log.assert_called_once() self.assertIn('Skipping radosgw polling', mock_log.call_args[0][0]) def test_pollster_yields_no_samples_when_config_missing(self): pollster = rgw.ObjectsPollster(self.conf) samples = list(pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(0, len(samples)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2173567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/objectstore/test_rgw_client.py0000664000175000017500000002460415250267452025676 0ustar00zuulzuul# Copyright (C) 2015 Reliance Jio Infocomm Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from unittest import mock from ceilometer.objectstore import rgw_client from ceilometer.tests import base RGW_ADMIN_BUCKETS = ''' [ { "max_marker": "", "ver": 2001, "usage": { "rgw.main": { "size_kb_actual": 16000, "num_objects": 1000, "size_kb": 1000 } }, "bucket": "somefoo", "owner": "admin", "master_ver": 0, "mtime": 1420176126, "marker": "default.4126.1", "bucket_quota": { "max_objects": -1, "enabled": false, "max_size_kb": -1 }, "id": "default.4126.1", "pool": ".rgw.buckets", "index_pool": ".rgw.buckets.index" }, { "max_marker": "", "ver": 3, "usage": { "rgw.main": { "size_kb_actual": 43, "num_objects": 1, "size_kb": 42 } }, "bucket": "somefoo31", "owner": "admin", "master_ver": 0, "mtime": 1420176134, "marker": "default.4126.5", "bucket_quota": { "max_objects": -1, "enabled": false, "max_size_kb": -1 }, "id": "default.4126.5", "pool": ".rgw.buckets", "index_pool": ".rgw.buckets.index" } ]''' RGW_ADMIN_USAGE = ''' { "entries": [ { "owner": "5f7fe2d5352e466f948f49341e33d107", "buckets": [ { "bucket": "", "time": "2015-01-23 09:00:00.000000Z", "epoch": 1422003600, "categories": [ { "category": "list_buckets", "bytes_sent": 46, "bytes_received": 0, "ops": 3, "successful_ops": 3}, { "category": "stat_account", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 1}]}, { "bucket": "foodsgh", "time": "2015-01-23 09:00:00.000000Z", "epoch": 1422003600, "categories": [ { "category": "create_bucket", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 1}, { "category": "get_obj", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 0}, { "category": "put_obj", "bytes_sent": 0, "bytes_received": 238, "ops": 1, "successful_ops": 1}]}]}], "summary": [ { "user": "5f7fe2d5352e466f948f49341e33d107", "categories": [ { "category": "create_bucket", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 1}, { "category": "get_obj", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 0}, { "category": "list_buckets", "bytes_sent": 46, "bytes_received": 0, "ops": 3, "successful_ops": 3}, { "category": "put_obj", "bytes_sent": 0, "bytes_received": 238, "ops": 1, "successful_ops": 1}, { "category": "stat_account", "bytes_sent": 0, "bytes_received": 0, "ops": 1, "successful_ops": 1}], "total": { "bytes_sent": 46, "bytes_received": 238, "ops": 7, "successful_ops": 6}}]} ''' buckets_json = json.loads(RGW_ADMIN_BUCKETS) usage_json = json.loads(RGW_ADMIN_USAGE) class TestRGWAdminClient(base.BaseTestCase): def setUp(self): super().setUp() self.client = rgw_client.RGWAdminClient('http://127.0.0.1:8080/admin', 'abcde', 'secret', False) self.resp = mock.MagicMock() self.make_request = mock.patch('awscurl.awscurl.make_request', return_value=self.resp).start() def test_make_request_exception(self): self.resp.status_code = 403 self.assertRaises(rgw_client.RGWAdminAPIFailed, self.client._make_request, *('foo', {})) def test_make_request(self): self.resp.status_code = 200 self.resp.json.return_value = buckets_json actual = self.client._make_request('foo', []) self.assertEqual(buckets_json, actual) def test_get_buckets(self): self.resp.status_code = 200 self.resp.json.return_value = buckets_json actual = self.client.get_bucket('foo') bucket_list = [rgw_client.RGWAdminClient.Bucket('somefoo', 1000, 1000), rgw_client.RGWAdminClient.Bucket('somefoo31', 1, 42), ] expected = {'num_buckets': 2, 'size': 1042, 'num_objects': 1001, 'buckets': bucket_list} self.assertEqual(expected, actual) self.make_request.assert_called_once_with( 'GET', 's3', 'us-east-1', 'http://127.0.0.1:8080/admin/bucket?uid=foo&stats=true', {'Accept': 'application/json'}, '', 'abcde', 'secret', None, False, True, tls_min=None, tls_max=None) def test_get_buckets_implicit_tenants(self): self.resp.status_code = 200 self.resp.json.return_value = buckets_json self.client.implicit_tenants = True actual = self.client.get_bucket('foo') bucket_list = [rgw_client.RGWAdminClient.Bucket('somefoo', 1000, 1000), rgw_client.RGWAdminClient.Bucket('somefoo31', 1, 42), ] expected = {'num_buckets': 2, 'size': 1042, 'num_objects': 1001, 'buckets': bucket_list} self.assertEqual(expected, actual) self.make_request.assert_called_once_with( 'GET', 's3', 'us-east-1', 'http://127.0.0.1:8080/admin/bucket?uid=foo%24foo&stats=true', {'Accept': 'application/json'}, '', 'abcde', 'secret', None, False, True, tls_min=None, tls_max=None) def test_get_usage(self): self.resp.status_code = 200 self.resp.json.return_value = usage_json actual = self.client.get_usage('foo') expected = 7 self.assertEqual(expected, actual) self.make_request.assert_called_once_with( 'GET', 's3', 'us-east-1', 'http://127.0.0.1:8080/admin/usage?uid=foo', {'Accept': 'application/json'}, '', 'abcde', 'secret', None, False, True, tls_min=None, tls_max=None) def test_get_usage_implicit_tenants(self): self.resp.status_code = 200 self.resp.json.return_value = usage_json self.client.implicit_tenants = True actual = self.client.get_usage('foo') expected = 7 self.assertEqual(expected, actual) self.make_request.assert_called_once_with( 'GET', 's3', 'us-east-1', 'http://127.0.0.1:8080/admin/usage?uid=foo%24foo', {'Accept': 'application/json'}, '', 'abcde', 'secret', None, False, True, tls_min=None, tls_max=None) def test_verify_default(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False) self.assertTrue(client.verify) def test_verify_false(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False, verify=False) self.assertFalse(client.verify) def test_verify_cafile(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False, verify='/path/to/ca.pem') self.assertEqual('/path/to/ca.pem', client.verify) def test_verify_passed_to_make_request(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False, verify='/path/to/ca.pem') self.resp.status_code = 200 self.resp.json.return_value = buckets_json with mock.patch('awscurl.awscurl.make_request', return_value=self.resp) as mock_req: client._make_request('bucket', {'uid': 'foo', 'stats': 'true'}) mock_req.assert_called_once() # verify is the 11th positional arg (index 10) self.assertEqual('/path/to/ca.pem', mock_req.call_args[0][10]) def test_tls_version_defaults(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False) self.assertIsNone(client.tls_min_version) self.assertIsNone(client.tls_max_version) def test_tls_versions_passed_to_make_request(self): client = rgw_client.RGWAdminClient( 'http://127.0.0.1:8080/admin', 'key', 'secret', False, verify=True, tls_min_version='1.2', tls_max_version='1.3') self.resp.status_code = 200 self.resp.json.return_value = buckets_json with mock.patch('awscurl.awscurl.make_request', return_value=self.resp) as mock_req: client._make_request('bucket', {'uid': 'foo', 'stats': 'true'}) mock_req.assert_called_once_with( 'GET', 's3', 'us-east-1', 'http://127.0.0.1:8080/admin/bucket?uid=foo&stats=true', {'Accept': 'application/json'}, '', 'key', 'secret', None, False, True, tls_min='1.2', tls_max='1.3') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2183566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/objectstore/test_swift.py0000664000175000017500000003001115250267452024662 0ustar00zuulzuul# Copyright 2012 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import itertools from unittest import mock import fixtures from keystoneauth1 import exceptions from swiftclient import client as swift_client import testscenarios.testcase from ceilometer.objectstore import swift from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base HEAD_ACCOUNTS = [('tenant-000', {'x-account-object-count': 12, 'x-account-bytes-used': 321321321, 'x-account-container-count': 7, }), ('tenant-001', {'x-account-object-count': 34, 'x-account-bytes-used': 9898989898, 'x-account-container-count': 17, }), ('tenant-002-ignored', {'x-account-object-count': 34, 'x-account-bytes-used': 9898989898, 'x-account-container-count': 17, })] GET_ACCOUNTS = [('tenant-000', ({'x-account-object-count': 10, 'x-account-bytes-used': 123123, 'x-account-container-count': 2, }, [{'count': 10, 'bytes': 123123, 'name': 'my_container', 'storage_policy': 'Policy-0', }, {'count': 0, 'bytes': 0, 'name': 'new_container', # NOTE(callumdickinson): No storage policy, # to test backwards compatibility with older # versions of Swift. }])), ('tenant-001', ({'x-account-object-count': 0, 'x-account-bytes-used': 0, 'x-account-container-count': 0, }, [])), ('tenant-002-ignored', ({'x-account-object-count': 0, 'x-account-bytes-used': 0, 'x-account-container-count': 0, }, []))] Tenant = collections.namedtuple('Tenant', 'id') ASSIGNED_TENANTS = [Tenant('tenant-000'), Tenant('tenant-001')] class TestManager(manager.AgentManager): def __init__(self, worker_id, conf): super().__init__(worker_id, conf) self._keystone = mock.MagicMock() self._keystone_last_exception = None self._service_catalog = (self._keystone.session.auth. get_access.return_value.service_catalog) self._auth_token = (self._keystone.session.auth. get_access.return_value.auth_token) class TestSwiftPollster(testscenarios.testcase.WithScenarios, base.BaseTestCase): # Define scenarios to run all of the tests against all of the # pollsters. scenarios = [ ('storage.objects', {'factory': swift.ObjectsPollster, 'resources': {}}), ('storage.objects.size', {'factory': swift.ObjectsSizePollster, 'resources': {}}), ('storage.objects.containers', {'factory': swift.ObjectsContainersPollster, 'resources': {}}), ('storage.containers.objects', {'factory': swift.ContainersObjectsPollster, 'resources': { f"{project_id}/{container['name']}": container for project_id, container in itertools.chain.from_iterable( itertools.product([acc[0]], acc[1][1]) for acc in GET_ACCOUNTS) }}), ('storage.containers.objects.size', {'factory': swift.ContainersSizePollster, 'resources': { f"{project_id}/{container['name']}": container for project_id, container in itertools.chain.from_iterable( itertools.product([acc[0]], acc[1][1]) for acc in GET_ACCOUNTS) }}), ] @staticmethod def fake_ks_service_catalog_url_for(*args, **kwargs): raise exceptions.EndpointNotFound("Fake keystone exception") def fake_iter_accounts(self, ksclient, cache, tenants): tenant_ids = [t.id for t in tenants] for i in self.ACCOUNTS: if i[0] in tenant_ids: yield i def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.pollster = self.factory(self.CONF) self.manager = TestManager(0, self.CONF) if self.pollster.CACHE_KEY_METHOD == 'swift.head_account': self.ACCOUNTS = HEAD_ACCOUNTS else: self.ACCOUNTS = GET_ACCOUNTS def tearDown(self): super().tearDown() swift._Base._ENDPOINT = None def test_iter_accounts_no_cache(self): cache = {} with fixtures.MockPatchObject(self.factory, '_get_account_info', return_value=[]): data = list(self.pollster._iter_accounts(mock.Mock(), cache, ASSIGNED_TENANTS)) self.assertIn(self.pollster.CACHE_KEY_METHOD, cache) self.assertEqual([], data) def test_iter_accounts_cached(self): # Verify that if a method has already been called, _iter_accounts # uses the cached version and doesn't call swiftclient. mock_method = mock.Mock() mock_method.side_effect = AssertionError( 'should not be called', ) api_method = '%s_account' % self.pollster.METHOD with fixtures.MockPatchObject(swift_client, api_method, new=mock_method): with fixtures.MockPatchObject(self.factory, '_neaten_url'): cache = {self.pollster.CACHE_KEY_METHOD: [self.ACCOUNTS[0]]} data = list(self.pollster._iter_accounts(mock.Mock(), cache, ASSIGNED_TENANTS)) self.assertEqual([self.ACCOUNTS[0]], data) def test_neaten_url(self): reseller_prefix = self.CONF.reseller_prefix test_endpoints = ['http://127.0.0.1:8080', 'http://127.0.0.1:8080/swift'] test_tenant_id = 'a7fd1695fa154486a647e44aa99a1b9b' for test_endpoint in test_endpoints: standard_url = test_endpoint + '/v1/AUTH_' + test_tenant_id url = swift._Base._neaten_url(test_endpoint, test_tenant_id, reseller_prefix) self.assertEqual(standard_url, url) url = swift._Base._neaten_url(test_endpoint + '/', test_tenant_id, reseller_prefix) self.assertEqual(standard_url, url) url = swift._Base._neaten_url(test_endpoint + '/v1', test_tenant_id, reseller_prefix) self.assertEqual(standard_url, url) url = swift._Base._neaten_url(standard_url, test_tenant_id, reseller_prefix) self.assertEqual(standard_url, url) def test_metering(self): with fixtures.MockPatchObject(self.factory, '_iter_accounts', side_effect=self.fake_iter_accounts): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(2, len(samples), self.pollster.__class__) for resource_id, resource in self.resources.items(): for field in getattr(self.pollster, 'FIELDS', []): with self.subTest(f'{resource_id}-{field}'): sample = next(s for s in samples if s.resource_id == resource_id) if field in resource: self.assertEqual(resource[field], sample.resource_metadata[field]) else: self.assertIsNone(sample.resource_metadata[field]) def test_get_meter_names(self): with fixtures.MockPatchObject(self.factory, '_iter_accounts', side_effect=self.fake_iter_accounts): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual({samples[0].name}, {s.name for s in samples}) def test_only_poll_assigned(self): mock_method = mock.MagicMock() endpoint = 'end://point/' api_method = '%s_account' % self.pollster.METHOD mock_connection = mock.MagicMock() with fixtures.MockPatchObject(swift_client, api_method, new=mock_method): with fixtures.MockPatchObject(swift_client, 'http_connection', new=mock_connection): with fixtures.MockPatchObject( self.manager._service_catalog, 'url_for', return_value=endpoint): list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) expected = [mock.call(self.pollster._neaten_url( endpoint, t.id, self.CONF.reseller_prefix), cacert=None) for t in ASSIGNED_TENANTS] self.assertEqual(expected, mock_connection.call_args_list) expected = [mock.call(None, self.manager._auth_token, http_conn=mock_connection.return_value) for t in ASSIGNED_TENANTS] self.assertEqual(expected, mock_method.call_args_list) def test_get_endpoint_only_once(self): endpoint = 'end://point/' mock_url_for = mock.MagicMock(return_value=endpoint) api_method = '%s_account' % self.pollster.METHOD with fixtures.MockPatchObject(swift_client, api_method, new=mock.MagicMock()): with fixtures.MockPatchObject(swift_client, 'http_connection', new=mock.MagicMock()): with fixtures.MockPatchObject( self.manager._service_catalog, 'url_for', new=mock_url_for): list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(1, mock_url_for.call_count) def test_endpoint_notfound(self): with fixtures.MockPatchObject( self.manager._service_catalog, 'url_for', side_effect=self.fake_ks_service_catalog_url_for): samples = list(self.pollster.get_samples(self.manager, {}, ASSIGNED_TENANTS)) self.assertEqual(0, len(samples)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2183566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/pipeline_base.py0000664000175000017500000004275215250267452022762 0ustar00zuulzuul# # Copyright 2013 Intel Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import abc import traceback from unittest import mock import fixtures from oslo_utils import timeutils from ceilometer.pipeline import base as pipe_base from ceilometer.pipeline import sample as pipeline from ceilometer import publisher from ceilometer.publisher import test as test_publisher from ceilometer import sample from ceilometer import service from ceilometer.tests import base class BasePipelineTestCase(base.BaseTestCase, metaclass=abc.ABCMeta): def get_publisher(self, conf, url, namespace=''): fake_drivers = {'test://': test_publisher.TestPublisher, 'new://': test_publisher.TestPublisher, 'except://': self.PublisherClassException} return fake_drivers[url](conf, url) class PublisherClassException(publisher.ConfigPublisherBase): def publish_samples(self, samples): raise Exception() def publish_events(self, events): raise Exception() def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.test_counter = sample.Sample( name='a', type=sample.TYPE_GAUGE, volume=1, unit='B', user_id="test_user", project_id="test_proj", resource_id="test_resource", timestamp=timeutils.utcnow().isoformat(), resource_metadata={} ) self.useFixture(fixtures.MockPatchObject( publisher, 'get_publisher', side_effect=self.get_publisher)) self._setup_pipeline_cfg() self._reraise_exception = True self.useFixture(fixtures.MockPatch( 'ceilometer.pipeline.base.LOG.exception', side_effect=self._handle_reraise_exception)) def _handle_reraise_exception(self, *args, **kwargs): if self._reraise_exception: raise Exception(traceback.format_exc()) @abc.abstractmethod def _setup_pipeline_cfg(self): """Setup the appropriate form of pipeline config.""" @abc.abstractmethod def _augment_pipeline_cfg(self): """Augment the pipeline config with an additional element.""" @abc.abstractmethod def _break_pipeline_cfg(self): """Break the pipeline config with a malformed element.""" @abc.abstractmethod def _dup_pipeline_name_cfg(self): """Break the pipeline config with duplicate pipeline name.""" @abc.abstractmethod def _set_pipeline_cfg(self, field, value): """Set a field to a value in the pipeline config.""" @abc.abstractmethod def _extend_pipeline_cfg(self, field, value): """Extend an existing field in the pipeline config with a value.""" @abc.abstractmethod def _unset_pipeline_cfg(self, field): """Clear an existing field in the pipeline config.""" def _build_and_set_new_pipeline(self): name = self.cfg2file(self.pipeline_cfg) self.CONF.set_override('pipeline_cfg_file', name) def _exception_create_pipelinemanager(self): self._build_and_set_new_pipeline() self.assertRaises(pipe_base.PipelineException, pipeline.SamplePipelineManager, self.CONF) def test_no_meters(self): self._unset_pipeline_cfg('meters') self._exception_create_pipelinemanager() def test_no_name(self): self._unset_pipeline_cfg('name') self._exception_create_pipelinemanager() def test_no_publishers(self): self._unset_pipeline_cfg('publishers') self._exception_create_pipelinemanager() def test_check_counters_include_exclude_same(self): counter_cfg = ['a', '!a'] self._set_pipeline_cfg('meters', counter_cfg) self._exception_create_pipelinemanager() def test_check_counters_include_exclude(self): counter_cfg = ['a', '!b'] self._set_pipeline_cfg('meters', counter_cfg) self._exception_create_pipelinemanager() def test_check_counters_wildcard_included(self): counter_cfg = ['a', '*'] self._set_pipeline_cfg('meters', counter_cfg) self._exception_create_pipelinemanager() def test_check_publishers_invalid_publisher(self): publisher_cfg = ['test_invalid'] self._set_pipeline_cfg('publishers', publisher_cfg) def test_multiple_included_counters(self): counter_cfg = ['a', 'b'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.samples)) self.test_counter = sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([self.test_counter]) self.assertEqual(2, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], "name")) self.assertEqual('b', getattr(publisher.samples[1], "name")) @mock.patch('ceilometer.pipeline.sample.LOG') def test_none_volume_counter(self, LOG): self._set_pipeline_cfg('meters', ['empty_volume']) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) publisher = pipeline_manager.pipelines[0].publishers[0] test_s = sample.Sample( name='empty_volume', type=self.test_counter.type, volume=None, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([test_s]) LOG.warning.assert_called_once_with( 'metering data %(counter_name)s for %(resource_id)s ' '@ %(timestamp)s has no volume (volume: None), the ' 'sample will be dropped', {'counter_name': test_s.name, 'resource_id': test_s.resource_id, 'timestamp': test_s.timestamp}) self.assertEqual(0, len(publisher.samples)) @mock.patch('ceilometer.pipeline.sample.LOG') def test_fake_volume_counter(self, LOG): self._set_pipeline_cfg('meters', ['fake_volume']) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) publisher = pipeline_manager.pipelines[0].publishers[0] test_s = sample.Sample( name='fake_volume', type=self.test_counter.type, volume='fake_value', unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([test_s]) LOG.warning.assert_called_once_with( 'metering data %(counter_name)s for %(resource_id)s ' '@ %(timestamp)s has volume which is not a number ' '(volume: %(counter_volume)s), the sample will be dropped', {'counter_name': test_s.name, 'resource_id': test_s.resource_id, 'timestamp': test_s.timestamp, 'counter_volume': test_s.volume}) self.assertEqual(0, len(publisher.samples)) def test_counter_dont_match(self): counter_cfg = ['nomatch'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(0, len(publisher.samples)) self.assertEqual(0, publisher.calls) def test_wildcard_counter(self): counter_cfg = ['*'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], "name")) def test_wildcard_excluded_counters(self): counter_cfg = ['*', '!a'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_meter('a')) def test_wildcard_excluded_counters_not_excluded(self): counter_cfg = ['*', '!b'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], "name")) def test_all_excluded_counters_not_excluded(self): counter_cfg = ['!b', '!c'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], "name")) def test_all_excluded_counters_is_excluded(self): counter_cfg = ['!a', '!c'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_meter('a')) self.assertTrue(pipe.source.support_meter('b')) self.assertFalse(pipe.source.support_meter('c')) def test_wildcard_and_excluded_wildcard_counters(self): counter_cfg = ['*', '!disk.*'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_meter('disk.read.bytes')) self.assertTrue(pipe.source.support_meter('cpu')) def test_included_counter_and_wildcard_counters(self): counter_cfg = ['cpu', 'disk.*'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertTrue(pipe.source.support_meter('disk.read.bytes')) self.assertTrue(pipe.source.support_meter('cpu')) self.assertFalse(pipe.source.support_meter('instance')) def test_excluded_counter_and_excluded_wildcard_counters(self): counter_cfg = ['!cpu', '!disk.*'] self._set_pipeline_cfg('meters', counter_cfg) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_meter('disk.read.bytes')) self.assertFalse(pipe.source.support_meter('cpu')) self.assertTrue(pipe.source.support_meter('instance')) def test_multiple_pipeline(self): self._augment_pipeline_cfg() self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) self.test_counter = sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.samples)) self.assertEqual(1, publisher.calls) self.assertEqual('a', getattr(publisher.samples[0], "name")) new_publisher = pipeline_manager.pipelines[1].publishers[0] self.assertEqual(1, len(new_publisher.samples)) self.assertEqual(1, new_publisher.calls) self.assertEqual('b', getattr(new_publisher.samples[0], "name")) def test_multiple_pipeline_exception(self): self._reraise_exception = False self._break_pipeline_cfg() self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) self.test_counter = sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, publisher.calls) self.assertEqual(1, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], "name")) def test_multiple_publisher(self): self._set_pipeline_cfg('publishers', ['test://', 'new://']) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) publisher = pipeline_manager.pipelines[0].publishers[0] new_publisher = pipeline_manager.pipelines[0].publishers[1] self.assertEqual(1, len(publisher.samples)) self.assertEqual(1, len(new_publisher.samples)) self.assertEqual('a', getattr(new_publisher.samples[0], 'name')) self.assertEqual('a', getattr(publisher.samples[0], 'name')) def test_multiple_publisher_isolation(self): self._reraise_exception = False self._set_pipeline_cfg('publishers', ['except://', 'new://']) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) new_publisher = pipeline_manager.pipelines[0].publishers[1] self.assertEqual(1, len(new_publisher.samples)) self.assertEqual('a', getattr(new_publisher.samples[0], 'name')) def test_multiple_counter_pipeline(self): self._set_pipeline_cfg('meters', ['a', 'b']) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter, sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, )]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(2, len(publisher.samples)) self.assertEqual('a', getattr(publisher.samples[0], 'name')) self.assertEqual('b', getattr(publisher.samples[1], 'name')) def test_unique_pipeline_names(self): self._dup_pipeline_name_cfg() self._exception_create_pipelinemanager() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2721276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/0000775000175000017500000000000015250267567021252 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2183566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/__init__.py0000664000175000017500000000000015250267452023342 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2193568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_discovery.py0000664000175000017500000001026015250267452024662 0ustar00zuulzuul# # Copyright 2014 Red Hat Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/central/manager.py""" from unittest import mock from ceilometer import keystone_client as ceilo_ks_client from ceilometer.polling.discovery import endpoint from ceilometer.polling.discovery import localnode from ceilometer.polling.discovery import tenant as project from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes class TestEndpointDiscovery(base.BaseTestCase): def setUp(self): super().setUp() CONF = service.prepare_service([], []) CONF.set_override('interface', 'publicURL', group='service_credentials') CONF.set_override('region_name', 'test-region-name', group='service_credentials') self.discovery = endpoint.EndpointDiscovery(CONF) self.manager = mock.MagicMock() self.catalog = (self.manager.keystone.session.auth.get_access. return_value.service_catalog) def test_keystone_called(self): self.discovery.discover(self.manager, param='test-service-type') expected = [mock.call(service_type='test-service-type', service_name=None, interface='publicURL', region_name='test-region-name')] self.assertEqual(expected, self.catalog.get_urls.call_args_list) def test_keystone_called_no_service_type(self): self.discovery.discover(self.manager) expected = [mock.call(service_type=None, service_name=None, interface='publicURL', region_name='test-region-name')] self.assertEqual(expected, self.catalog.get_urls .call_args_list) def test_keystone_called_no_endpoints(self): self.catalog.get_urls.return_value = [] self.assertEqual([], self.discovery.discover(self.manager)) class TestLocalnodeDiscovery(base.BaseTestCase): def setUp(self): super().setUp() self.conf = service.prepare_service([], []) self.discovery = localnode.LocalNodeDiscovery(self.conf) self.manager = mock.MagicMock() def test_lockalnode_discovery(self): self.assertEqual([self.conf.host], self.discovery.discover(self.manager)) class TestProjectDiscovery(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.discovery = project.TenantDiscovery(self.CONF) self.manager = mock.MagicMock() self.manager.keystone = ceilo_ks_client.Client(session=mock.Mock()) def test_project_discovery(self): result = self.discovery.discover(self.manager) self.assertEqual(len(result), 4) self.fake_conn.list_domains.assert_called_once_with(filters=None) self.assertEqual( self.fake_conn.list_projects.mock_calls, [mock.call(domain_id=d.id, filters=None) for d in fakes.DEFAULT_DOMAINS_ceilo]) def test_project_discovery_ignore_disabled_projects(self): self.CONF.set_override("ignore_disabled_projects", True, group="polling") result = self.discovery.discover(self.manager) self.assertEqual(len(result), 3) self.fake_conn.list_domains.assert_called_once_with( filters={'enabled': True}) self.assertEqual( self.fake_conn.list_projects.mock_calls, [mock.call(domain_id=d.id, filters={'enabled': True}) for d in fakes.DEFAULT_DOMAINS_ceilo]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2203567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_dynamic_pollster.py0000664000175000017500000021064415250267452026233 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for OpenStack dynamic pollster """ import copy import json import logging from unittest import mock import requests from urllib import parse as urlparse from ceilometer import declarative from ceilometer.polling import dynamic_pollster from ceilometer import sample from ceilometer.tests import base LOG = logging.getLogger(__name__) REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit', 'value_attribute', 'endpoint_type', 'url_path'] class FakeResponse: def __init__(self, status_code=requests.codes.ok, json_object=None, text=None): self.status_code = status_code self.json_object = json_object self._text = text @property def text(self): return self._text or json.dumps(self.json_object) def json(self): return self.json_object def raise_for_status(self): raise requests.HTTPError("Mock HTTP error.", response=self) class SampleGenerator: def __init__(self, samples_dict, turn_to_list=False): self.turn_to_list = turn_to_list self.samples_dict = {} for k, v in samples_dict.items(): if isinstance(v, list): self.samples_dict[k] = [0, v] else: self.samples_dict[k] = [0, [v]] def get_next_sample_dict(self): _dict = {} for key in self.samples_dict.keys(): _dict[key] = self.get_next_sample(key) if self.turn_to_list: _dict = [_dict] return _dict def get_next_sample(self, key): samples = self.samples_dict[key][1] samples_next_iteration = self.samples_dict[key][0] % len(samples) self.samples_dict[key][0] += 1 _sample = samples[samples_next_iteration] if isinstance(_sample, SampleGenerator): return _sample.get_next_sample_dict() return _sample class PagedSamplesGenerator(SampleGenerator): def __init__(self, samples_dict, dict_name, page_link_name): super().__init__(samples_dict) self.dict_name = dict_name self.page_link_name = page_link_name self.response = {} def generate_samples(self, page_base_link, page_links, last_page_size): self.response.clear() current_page_link = page_base_link for page_link, page_size in page_links.items(): page_link = page_base_link + "/" + page_link self.response[current_page_link] = { self.page_link_name: [{'href': page_link, 'rel': 'next'}], self.dict_name: self.populate_page(page_size) } current_page_link = page_link self.response[current_page_link] = { self.dict_name: self.populate_page(last_page_size) } def populate_page(self, page_size): page = [] for item_number in range(0, page_size): page.append(self.get_next_sample_dict()) return page class PagedSamplesGeneratorHttpRequestMock(PagedSamplesGenerator): def mock_request(self, url, **kwargs): return_value = FakeResponse( json_object=self.response[url]) return return_value class TestDynamicPollster(base.BaseTestCase): class FakeManager: def __init__(self, keystone=None): self._keystone = keystone def setUp(self): super().setUp() self.pollster_definition_only_required_fields = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'endpoint_type': "test", 'url_path': "v1/test/endpoint/fake"} self.pollster_definition_all_fields = { 'metadata_fields': "metadata-field-name", 'skip_sample_values': ["I-do-not-want-entries-with-this-value"], 'value_mapping': { 'value-to-map': 'new-value', 'value-to-map-to-numeric': 12 }, 'default_value_mapping': 0, 'metadata_mapping': { 'old-metadata-name': "new-metadata-name" }, 'preserve_mapped_metadata': False} self.pollster_definition_all_fields.update( self.pollster_definition_only_required_fields) self.multi_metric_pollster_definition = { 'name': "test-pollster.{category}", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "[categories].ops", 'endpoint_type': "test", 'url_path': "v1/test/endpoint/fake"} def execute_basic_asserts(self, pollster, pollster_definition): self.assertEqual(pollster, pollster.obj) self.assertEqual(pollster_definition['name'], pollster.name) for key in REQUIRED_POLLSTER_FIELDS: self.assertEqual(pollster_definition[key], pollster.pollster_definitions[key]) self.assertEqual(pollster_definition, pollster.pollster_definitions) def test_skip_samples_with_linked_samples(self): generator = PagedSamplesGeneratorHttpRequestMock(samples_dict={ 'volume': SampleGenerator(samples_dict={ 'name': ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], 'tmp': ['ra', 'rb', 'rc', 'rd', 're', 'rf', 'rg', 'rh']}, turn_to_list=True), 'id': [1, 2, 3, 4, 5, 6, 7, 8], 'name': ['a1', 'b2', 'c3', 'd4', 'e5', 'f6', 'g7', 'h8'] }, dict_name='servers', page_link_name='server_link') generator.generate_samples('http://test.com/v1/test-volumes', { 'marker=c3': 3, 'marker=f6': 3 }, 2) keystone_mock = mock.Mock() keystone_mock.session.get.side_effect = generator.mock_request fake_manager = self.FakeManager(keystone=keystone_mock) pollster_definition = dict(self.multi_metric_pollster_definition) pollster_definition['name'] = 'test-pollster.{name}' pollster_definition['value_attribute'] = '[volume].tmp' pollster_definition['skip_sample_values'] = ['rb'] pollster_definition['url_path'] = 'v1/test-volumes' pollster_definition['response_entries_key'] = 'servers' pollster_definition['next_sample_url_attribute'] = \ 'server_link | filter(lambda v: v.get("rel") == "next", value) |' \ 'list(value) | value[0] | value.get("href")' pollster = dynamic_pollster.DynamicPollster(pollster_definition) samples = pollster.get_samples(fake_manager, None, ['http://test.com']) self.assertEqual(['ra', 'rc', 'rd', 're', 'rf', 'rg', 'rh'], list(map(lambda s: s.volume, samples))) generator.generate_samples('http://test.com/v1/test-volumes', { 'marker=c3': 3, 'marker=f6': 3 }, 2) pollster_definition['name'] = 'test-pollster' pollster_definition['value_attribute'] = 'name' pollster_definition['skip_sample_values'] = ['b2'] pollster = dynamic_pollster.DynamicPollster(pollster_definition) samples = pollster.get_samples(fake_manager, None, ['http://test.com']) self.assertEqual(['a1', 'c3', 'd4', 'e5', 'f6', 'g7', 'h8'], list(map(lambda s: s.volume, samples))) def test_all_required_fields_ok(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) self.execute_basic_asserts( pollster, self.pollster_definition_only_required_fields) self.assertEqual( 0, len(pollster.pollster_definitions['skip_sample_values'])) self.assertEqual( 0, len(pollster.pollster_definitions['value_mapping'])) self.assertEqual( -1, pollster.pollster_definitions['default_value']) self.assertEqual( 0, len(pollster.pollster_definitions['metadata_mapping'])) self.assertEqual( True, pollster.pollster_definitions['preserve_mapped_metadata']) def test_all_fields_ok(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_all_fields) self.execute_basic_asserts(pollster, self.pollster_definition_all_fields) self.assertEqual( 1, len(pollster.pollster_definitions['skip_sample_values'])) self.assertEqual( 2, len(pollster.pollster_definitions['value_mapping'])) self.assertEqual( 0, pollster.pollster_definitions['default_value_mapping']) self.assertEqual( 1, len(pollster.pollster_definitions['metadata_mapping'])) self.assertEqual( False, pollster.pollster_definitions['preserve_mapped_metadata']) def test_all_required_fields_exceptions(self): for key in REQUIRED_POLLSTER_FIELDS: pollster_definition = copy.deepcopy( self.pollster_definition_only_required_fields) pollster_definition.pop(key) exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, pollster_definition) self.assertEqual("Required fields ['%s'] not specified." % key, exception.brief_message) def test_invalid_sample_type(self): self.pollster_definition_only_required_fields[ 'sample_type'] = "invalid_sample_type" exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, self.pollster_definition_only_required_fields) self.assertEqual("Invalid sample type [invalid_sample_type]. " "Valid ones are [('gauge', 'delta', 'cumulative')].", exception.brief_message) def test_all_valid_sample_type(self): for sample_type in sample.TYPES: self.pollster_definition_only_required_fields[ 'sample_type'] = sample_type pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) self.execute_basic_asserts( pollster, self.pollster_definition_only_required_fields) def test_default_discovery_method(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) self.assertEqual("endpoint:test", pollster.definitions.sample_gatherer .default_discovery) def test_execute_request_get_samples_empty_response(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( json_object={}) samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(0, len(samples)) def test_execute_request_get_samples_response_non_empty(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( json_object={"firstElement": [{}, {}, {}]}) samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) def test_execute_request_json_response_handler(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( text='{"test": [1,2,3]}') samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) def test_execute_request_xml_response_handler(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml'] pollster = dynamic_pollster.DynamicPollster(definitions) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( text='123') samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) def test_execute_request_xml_json_response_handler(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml', 'json'] pollster = dynamic_pollster.DynamicPollster(definitions) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( text='123') samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) client_mock.session.get.return_value = FakeResponse( text='{"test": [1,2,3,4]}') samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(4, len(samples)) def test_execute_request_extra_metadata_fields_cache_disabled(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) extra_metadata_fields = { 'extra_metadata_fields_cache_seconds': 0, 'name': "project_name", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + str(sample['project_id'])", 'value': "name", } definitions['value_attribute'] = 'project_id' definitions['extra_metadata_fields'] = extra_metadata_fields pollster = dynamic_pollster.DynamicPollster(definitions) return_value = FakeResponse(text=''' {"projects": [ {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}, {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}, {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}] } ''') return_value9999 = FakeResponse(text=''' {"project": {"project_id": 9999, "name": "project1"} } ''') return_value8888 = FakeResponse(text=''' {"project": {"project_id": 8888, "name": "project2"} } ''') return_value7777 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "project3"} } ''') def get(url, *args, **kwargs): if '9999' in url: return return_value9999 if '8888' in url: return return_value8888 if '7777' in url: return return_value7777 return return_value client_mock = mock.Mock() client_mock.session.get.side_effect = get manager = mock.Mock manager._keystone = client_mock def discover(*args, **kwargs): return ["https://endpoint.server.name/"] manager.discover = discover samples = pollster.get_samples( manager=manager, cache=None, resources=["https://endpoint.server.name/"]) samples = list(samples) n_calls = client_mock.session.get.call_count self.assertEqual(9, len(samples)) self.assertEqual(10, n_calls) def test_execute_request_extra_metadata_fields_cache_enabled(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) extra_metadata_fields = { 'extra_metadata_fields_cache_seconds': 3600, 'name': "project_name", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + str(sample['project_id'])", 'value': "name", } definitions['value_attribute'] = 'project_id' definitions['extra_metadata_fields'] = extra_metadata_fields pollster = dynamic_pollster.DynamicPollster(definitions) return_value = FakeResponse(text=''' {"projects": [ {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}, {"project_id": 9999, "name": "project4"}, {"project_id": 8888, "name": "project5"}, {"project_id": 7777, "name": "project6"}, {"project_id": 9999, "name": "project7"}, {"project_id": 8888, "name": "project8"}, {"project_id": 7777, "name": "project9"}] } ''') return_value9999 = FakeResponse(text=''' {"project": {"project_id": 9999, "name": "project1"} } ''') return_value8888 = FakeResponse(text=''' {"project": {"project_id": 8888, "name": "project2"} } ''') return_value7777 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "project3"} } ''') def get(url, *args, **kwargs): if '9999' in url: return return_value9999 if '8888' in url: return return_value8888 if '7777' in url: return return_value7777 return return_value client_mock = mock.Mock() client_mock.session.get.side_effect = get manager = mock.Mock manager._keystone = client_mock def discover(*args, **kwargs): return ["https://endpoint.server.name/"] manager.discover = discover samples = pollster.get_samples( manager=manager, cache=None, resources=["https://endpoint.server.name/"]) samples = list(samples) n_calls = client_mock.session.get.call_count self.assertEqual(9, len(samples)) self.assertEqual(4, n_calls) def test_execute_request_extra_metadata_fields(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) extra_metadata_fields = [{ 'name': "project_name", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + str(sample['project_id'])", 'value': "name", 'metadata_fields': ['meta'] }, { 'name': "project_alias", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + " "str(extra_metadata_captured['project_name'])", 'value': "name", 'metadata_fields': ['meta'] }, { 'name': "project_meta", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + " "str(extra_metadata_by_name['project_name']" "['metadata']['meta'])", 'value': "project_id", 'metadata_fields': ['meta'] }] definitions['value_attribute'] = 'project_id' definitions['extra_metadata_fields'] = extra_metadata_fields pollster = dynamic_pollster.DynamicPollster(definitions) return_value = FakeResponse(text=''' {"projects": [ {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}] } ''') return_value9999 = FakeResponse(text=''' {"project": {"project_id": 9999, "name": "project1", "meta": "m1"} } ''') return_value8888 = FakeResponse(text=''' {"project": {"project_id": 8888, "name": "project2", "meta": "m2"} } ''') return_value7777 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "project3", "meta": "m3"} } ''') return_valueP1 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p1", "meta": null} } ''') return_valueP2 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p2", "meta": null} } ''') return_valueP3 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p3", "meta": null} } ''') return_valueM1 = FakeResponse(text=''' {"project": {"project_id": "META1", "name": "p3", "meta": null} } ''') return_valueM2 = FakeResponse(text=''' {"project": {"project_id": "META2", "name": "p3", "meta": null} } ''') return_valueM3 = FakeResponse(text=''' {"project": {"project_id": "META3", "name": "p3", "meta": null} } ''') def get(url, *args, **kwargs): if '9999' in url: return return_value9999 if '8888' in url: return return_value8888 if '7777' in url: return return_value7777 if 'project1' in url: return return_valueP1 if 'project2' in url: return return_valueP2 if 'project3' in url: return return_valueP3 if 'm1' in url: return return_valueM1 if 'm2' in url: return return_valueM2 if 'm3' in url: return return_valueM3 return return_value client_mock = mock.Mock() client_mock.session.get = get manager = mock.Mock manager._keystone = client_mock def discover(*args, **kwargs): return ["https://endpoint.server.name/"] manager.discover = discover samples = pollster.get_samples( manager=manager, cache=None, resources=["https://endpoint.server.name/"]) samples = list(samples) self.assertEqual(3, len(samples)) self.assertEqual(samples[0].volume, 9999) self.assertEqual(samples[1].volume, 8888) self.assertEqual(samples[2].volume, 7777) self.assertEqual(samples[0].resource_metadata, {'project_name': 'project1', 'project_alias': 'p1', 'meta': 'm1', 'project_meta': 'META1'}) self.assertEqual(samples[1].resource_metadata, {'project_name': 'project2', 'project_alias': 'p2', 'meta': 'm2', 'project_meta': 'META2'}) self.assertEqual(samples[2].resource_metadata, {'project_name': 'project3', 'project_alias': 'p3', 'meta': 'm3', 'project_meta': 'META3'}) def test_execute_request_extra_metadata_fields_skip(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) extra_metadata_fields = [{ 'name': "project_name", 'endpoint_type': "identity", 'url_path': "'/v3/projects/' + str(sample['project_id'])", 'value': "name", }, { 'name': "project_alias", 'endpoint_type': "identity", 'extra_metadata_fields_skip': [{ 'value': 7777 }], 'url_path': "'/v3/projects/' + " "str(sample['p_name'])", 'value': "name", }] definitions['value_attribute'] = 'project_id' definitions['metadata_fields'] = ['to_skip', 'p_name'] definitions['extra_metadata_fields'] = extra_metadata_fields definitions['extra_metadata_fields_skip'] = [{ 'metadata': { 'to_skip': 'skip1' } }, { 'value': 8888 }] pollster = dynamic_pollster.DynamicPollster(definitions) return_value = FakeResponse(text=''' {"projects": [ {"project_id": 9999, "p_name": "project1", "to_skip": "skip1"}, {"project_id": 8888, "p_name": "project2", "to_skip": "skip2"}, {"project_id": 7777, "p_name": "project3", "to_skip": "skip3"}, {"project_id": 6666, "p_name": "project4", "to_skip": "skip4"}] } ''') return_value9999 = FakeResponse(text=''' {"project": {"project_id": 9999, "name": "project1"} } ''') return_value8888 = FakeResponse(text=''' {"project": {"project_id": 8888, "name": "project2"} } ''') return_value7777 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "project3"} } ''') return_value6666 = FakeResponse(text=''' {"project": {"project_id": 6666, "name": "project4"} } ''') return_valueP1 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p1"} } ''') return_valueP2 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p2"} } ''') return_valueP3 = FakeResponse(text=''' {"project": {"project_id": 7777, "name": "p3"} } ''') return_valueP4 = FakeResponse(text=''' {"project": {"project_id": 6666, "name": "p4"} } ''') def get(url, *args, **kwargs): if '9999' in url: return return_value9999 if '8888' in url: return return_value8888 if '7777' in url: return return_value7777 if '6666' in url: return return_value6666 if 'project1' in url: return return_valueP1 if 'project2' in url: return return_valueP2 if 'project3' in url: return return_valueP3 if 'project4' in url: return return_valueP4 return return_value client_mock = mock.Mock() client_mock.session.get = get manager = mock.Mock manager._keystone = client_mock def discover(*args, **kwargs): return ["https://endpoint.server.name/"] manager.discover = discover samples = pollster.get_samples( manager=manager, cache=None, resources=["https://endpoint.server.name/"]) samples = list(samples) self.assertEqual(4, len(samples)) self.assertEqual(samples[0].volume, 9999) self.assertEqual(samples[1].volume, 8888) self.assertEqual(samples[2].volume, 7777) self.assertEqual(samples[0].resource_metadata, {'p_name': 'project1', 'project_alias': 'p1', 'to_skip': 'skip1'}) self.assertEqual(samples[1].resource_metadata, {'p_name': 'project2', 'project_alias': 'p2', 'to_skip': 'skip2'}) self.assertEqual(samples[2].resource_metadata, {'p_name': 'project3', 'project_name': 'project3', 'to_skip': 'skip3'}) self.assertEqual(samples[3].resource_metadata, {'p_name': 'project4', 'project_alias': 'p4', 'project_name': 'project4', 'to_skip': 'skip4'}) def test_execute_request_extra_metadata_fields_different_requests(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) command = ''' \'\'\'echo '{"project": {"project_id": \'\'\'+ str(sample['project_id']) +\'\'\' , "name": "project1"}}' \'\'\' '''.replace('\n', '') command2 = ''' \'\'\'echo '{"project": {"project_id": \'\'\'+ str(sample['project_id']) +\'\'\' , "name": "project2"}}' \'\'\' '''.replace('\n', '') extra_metadata_fields_embedded = { 'name': "project_name2", 'host_command': command2, 'value': "name", } extra_metadata_fields = { 'name': "project_id2", 'host_command': command, 'value': "project_id", 'extra_metadata_fields': extra_metadata_fields_embedded } definitions['value_attribute'] = 'project_id' definitions['extra_metadata_fields'] = extra_metadata_fields pollster = dynamic_pollster.DynamicPollster(definitions) return_value = FakeResponse(text=''' {"projects": [ {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}] } ''') def get(url, *args, **kwargs): return return_value client_mock = mock.Mock() client_mock.session.get = get manager = mock.Mock manager._keystone = client_mock def discover(*args, **kwargs): return ["https://endpoint.server.name/"] manager.discover = discover samples = pollster.get_samples( manager=manager, cache=None, resources=["https://endpoint.server.name/"]) samples = list(samples) self.assertEqual(3, len(samples)) self.assertEqual(samples[0].volume, 9999) self.assertEqual(samples[1].volume, 8888) self.assertEqual(samples[2].volume, 7777) self.assertEqual(samples[0].resource_metadata, {'project_id2': 9999, 'project_name2': 'project2'}) self.assertEqual(samples[1].resource_metadata, {'project_id2': 8888, 'project_name2': 'project2'}) self.assertEqual(samples[2].resource_metadata, {'project_id2': 7777, 'project_name2': 'project2'}) def test_execute_request_xml_json_response_handler_invalid_response(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml', 'json'] pollster = dynamic_pollster.DynamicPollster(definitions) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( text='Invalid response') with self.assertLogs('ceilometer.polling.dynamic_pollster', level='DEBUG') as logs: gatherer = pollster.definitions.sample_gatherer exception = self.assertRaises( declarative.InvalidResponseTypeException, gatherer.execute_request_get_samples, keystone_client=client_mock, resource="https://endpoint.server.name/") xml_handling_error = logs.output[3] json_handling_error = logs.output[4] self.assertIn( 'DEBUG:ceilometer.polling.dynamic_pollster:' 'Error handling response [Invalid response] ' 'with handler [XMLResponseHandler]', xml_handling_error) self.assertIn( 'DEBUG:ceilometer.polling.dynamic_pollster:' 'Error handling response [Invalid response] ' 'with handler [JsonResponseHandler]', json_handling_error) self.assertEqual( "InvalidResponseTypeException None: " "No remaining handlers to handle the response " "[Invalid response], used handlers " "[XMLResponseHandler, JsonResponseHandler]. " "[{'url_path': 'v1/test/endpoint/fake'}].", str(exception)) def test_configure_response_handler_definition_invalid_value(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['jason'] exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, pollster_definitions=definitions) self.assertEqual("DynamicPollsterDefinitionException None: " "Invalid response_handler value [jason]. " "Accepted values are [json, xml, text]", str(exception)) def test_configure_extra_metadata_field_skip_invalid_value(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['extra_metadata_fields_skip'] = 'teste' exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, pollster_definitions=definitions) self.assertEqual("DynamicPollsterDefinitionException None: " "Invalid extra_metadata_fields_skip configuration." " It must be a list of maps. Provided value: teste," " value type: str.", str(exception)) def test_configure_extra_metadata_field_skip_invalid_sub_value(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['extra_metadata_fields_skip'] = [{'test': '1'}, {'test': '2'}, 'teste'] exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, pollster_definitions=definitions) self.assertEqual("DynamicPollsterDefinitionException None: " "Invalid extra_metadata_fields_skip configuration." " It must be a list of maps. Provided value: " "[{'test': '1'}, {'test': '2'}, 'teste'], " "value type: list.", str(exception)) def test_configure_response_handler_definition_invalid_type(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = 'json' exception = self.assertRaises( declarative.DynamicPollsterDefinitionException, dynamic_pollster.DynamicPollster, pollster_definitions=definitions) self.assertEqual("DynamicPollsterDefinitionException None: " "Invalid response_handlers configuration. " "It must be a list. Provided value type: str", str(exception)) def test_execute_request_get_samples_exception_on_request(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) client_mock = mock.Mock() client_mock.session.get.return_value = FakeResponse( status_code=requests.codes.bad) exception = self.assertRaises(requests.HTTPError, pollster.definitions.sample_gatherer. execute_request_get_samples, keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual("Mock HTTP error.", str(exception)) def test_execute_host_command_paged_responses(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['host_command'] = ''' echo '{"server": [{"status": "ACTIVE"}], "next": ""}' ''' str_json = "'{\\\"server\\\": [{\\\"status\\\": \\\"INACTIVE\\\"}]}'" definitions['next_sample_url_attribute'] = \ "next|\"echo \"+value+\"" + str_json + '"' pollster = dynamic_pollster.DynamicPollster(definitions) samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples() resp_json = [{'status': 'ACTIVE'}, {'status': 'INACTIVE'}] self.assertEqual(resp_json, samples) def test_execute_host_command_response_handler(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml', 'json'] definitions['host_command'] = 'echo "xml\nxml"' entry = 'a' definitions['response_entries_key'] = entry definitions.pop('url_path') definitions.pop('endpoint_type') pollster = dynamic_pollster.DynamicPollster(definitions) samples_xml = pollster.definitions.sample_gatherer. \ execute_request_get_samples() definitions['host_command'] = 'echo \'{"a": {"y":"json",' \ '\n"s":"json"}}\'' samples_json = pollster.definitions.sample_gatherer. \ execute_request_get_samples() resp_xml = {'a': {'y': 'xml', 's': 'xml'}} resp_json = {'a': {'y': 'json', 's': 'json'}} self.assertEqual(resp_xml[entry], samples_xml) self.assertEqual(resp_json[entry], samples_json) def test_execute_host_command_invalid_command(self): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['host_command'] = 'invalid-command' definitions.pop('url_path') definitions.pop('endpoint_type') pollster = dynamic_pollster.DynamicPollster(definitions) self.assertRaises( declarative.InvalidResponseTypeException, pollster.definitions.sample_gatherer.execute_request_get_samples) def test_generate_new_metadata_fields_no_metadata_mapping(self): metadata = {'name': 'someName', 'value': 1} metadata_before_call = copy.deepcopy(metadata) self.pollster_definition_only_required_fields['metadata_mapping'] = {} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) pollster.definitions.sample_extractor.generate_new_metadata_fields( metadata, self.pollster_definition_only_required_fields) self.assertEqual(metadata_before_call, metadata) def test_generate_new_metadata_fields_preserve_old_key(self): metadata = {'name': 'someName', 'value': 2} expected_metadata = copy.deepcopy(metadata) expected_metadata['balance'] = metadata['value'] self.pollster_definition_only_required_fields[ 'metadata_mapping'] = {'value': 'balance'} self.pollster_definition_only_required_fields[ 'preserve_mapped_metadata'] = True pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) pollster.definitions.sample_extractor.generate_new_metadata_fields( metadata, self.pollster_definition_only_required_fields) self.assertEqual(expected_metadata, metadata) def test_generate_new_metadata_fields_preserve_old_key_equals_false(self): metadata = {'name': 'someName', 'value': 1} expected_clean_metadata = copy.deepcopy(metadata) expected_clean_metadata['balance'] = metadata['value'] expected_clean_metadata.pop('value') self.pollster_definition_only_required_fields[ 'metadata_mapping'] = {'value': 'balance'} self.pollster_definition_only_required_fields[ 'preserve_mapped_metadata'] = False pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) pollster.definitions.sample_extractor.generate_new_metadata_fields( metadata, self.pollster_definition_only_required_fields) self.assertEqual(expected_clean_metadata, metadata) def test_execute_value_mapping_no_value_mapping(self): self.pollster_definition_only_required_fields['value_mapping'] = {} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) value_to_be_mapped = "test" expected_value = value_to_be_mapped value = pollster.definitions.value_mapper. \ execute_value_mapping(value_to_be_mapped) self.assertEqual(expected_value, value) def test_execute_value_mapping_no_value_mapping_found_with_default(self): self.pollster_definition_only_required_fields[ 'value_mapping'] = {'some-possible-value': 15} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) value_to_be_mapped = "test" expected_value = -1 value = pollster.definitions.value_mapper. \ execute_value_mapping(value_to_be_mapped) self.assertEqual(expected_value, value) def test_execute_value_mapping_no_value_mapping_found_with_custom_default( self): self.pollster_definition_only_required_fields[ 'value_mapping'] = {'some-possible-value': 5} self.pollster_definition_only_required_fields[ 'default_value'] = 0 pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) value_to_be_mapped = "test" expected_value = 0 value = pollster.definitions.value_mapper. \ execute_value_mapping(value_to_be_mapped) self.assertEqual(expected_value, value) def test_execute_value_mapping(self): self.pollster_definition_only_required_fields[ 'value_mapping'] = {'test': 'new-value'} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) value_to_be_mapped = "test" expected_value = 'new-value' value = pollster.definitions.value_mapper. \ execute_value_mapping(value_to_be_mapped) self.assertEqual(expected_value, value) def test_get_samples_no_resources(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) samples = pollster.get_samples(None, None, None) self.assertIsNone(next(samples)) @mock.patch('ceilometer.polling.dynamic_pollster.' 'PollsterSampleGatherer.execute_request_get_samples') def test_get_samples_empty_samples(self, execute_request_get_samples_mock): execute_request_get_samples_mock.side_effect = [] pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) fake_manager = self.FakeManager() samples = pollster.get_samples( fake_manager, None, ["https://endpoint.server.name.com/"]) samples_list = list() try: for s in samples: samples_list.append(s) except RuntimeError as e: LOG.debug("Generator threw a StopIteration " "and we need to catch it [%s].", e) self.assertEqual(0, len(samples_list)) def fake_sample_list(self, **kwargs): samples_list = list() samples_list.append( {'name': "sample5", 'volume': 5, 'description': "desc-sample-5", 'user_id': "924d1f77-5d75-4b96-a755-1774d6be17af", 'project_id': "6c7a0e87-7f2e-45d3-89ca-5a2dbba71a0e", 'id': "e335c317-dfdd-4f22-809a-625bd9a5992d" } ) samples_list.append( {'name': "sample1", 'volume': 2, 'description': "desc-sample-2", 'user_id': "20b5a704-b481-4603-a99e-2636c144b876", 'project_id': "6c7a0e87-7f2e-45d3-89ca-5a2dbba71a0e", 'id': "2e350554-6c05-4fda-8109-e47b595a714c" } ) return samples_list @mock.patch.object( dynamic_pollster.PollsterSampleGatherer, 'execute_request_get_samples', fake_sample_list) def test_get_samples(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) fake_manager = self.FakeManager() samples = pollster.get_samples( fake_manager, None, ["https://endpoint.server.name.com/"]) samples_list = list(samples) self.assertEqual(2, len(samples_list)) first_element = [ s for s in samples_list if s.resource_id == "e335c317-dfdd-4f22-809a-625bd9a5992d"][0] self.assertEqual(5, first_element.volume) self.assertEqual( "6c7a0e87-7f2e-45d3-89ca-5a2dbba71a0e", first_element.project_id) self.assertEqual( "924d1f77-5d75-4b96-a755-1774d6be17af", first_element.user_id) second_element = [ s for s in samples_list if s.resource_id == "2e350554-6c05-4fda-8109-e47b595a714c"][0] self.assertEqual(2, second_element.volume) self.assertEqual( "6c7a0e87-7f2e-45d3-89ca-5a2dbba71a0e", second_element.project_id) self.assertEqual( "20b5a704-b481-4603-a99e-2636c144b876", second_element.user_id) def test_retrieve_entries_from_response_response_is_a_list(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) response = [{"object1-attr1": 1}, {"object1-attr2": 2}] entries = pollster.definitions.sample_gatherer. \ retrieve_entries_from_response(response, pollster.definitions) self.assertEqual(response, entries) def test_retrieve_entries_using_first_entry_from_response(self): self.pollster_definition_only_required_fields[ 'response_entries_key'] = "first" pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) first_entries_from_response = [{"object1-attr1": 1}, {"object1-attr2": 2}] second_entries_from_response = [{"object1-attr3": 3}, {"object1-attr4": 33}] response = {"first": first_entries_from_response, "second": second_entries_from_response} entries = pollster.definitions.sample_gatherer.\ retrieve_entries_from_response( response, pollster.definitions.configurations) self.assertEqual(first_entries_from_response, entries) def test_retrieve_entries_using_second_entry_from_response(self): self.pollster_definition_only_required_fields[ 'response_entries_key'] = "second" pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) first_entries_from_response = [{"object1-attr1": 1}, {"object1-attr2": 2}] second_entries_from_response = [{"object1-attr3": 3}, {"object1-attr4": 33}] response = {"first": first_entries_from_response, "second": second_entries_from_response} entries = pollster.definitions.sample_gatherer. \ retrieve_entries_from_response(response, pollster.definitions.configurations) self.assertEqual(second_entries_from_response, entries) def test_retrieve_attribute_nested_value_non_nested_key(self): key = "key" value = [{"d": 2}, {"g": {"h": "val"}}] json_object = {"key": value} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) returned_value = pollster.definitions.sample_extractor.\ retrieve_attribute_nested_value(json_object, key) self.assertEqual(value, returned_value) def test_retrieve_attribute_nested_value_nested_key(self): key = "key.subKey" value1 = [{"d": 2}, {"g": {"h": "val"}}] sub_value = [{"r": 245}, {"h": {"yu": "yu"}}] json_object = {"key": {"subKey": sub_value, "subkey2": value1}} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) returned_value = pollster.definitions.sample_extractor. \ retrieve_attribute_nested_value(json_object, key) self.assertEqual(sub_value, returned_value) def test_retrieve_attribute_nested_value_with_operation_on_attribute(self): # spaces here are added on purpose at the end to make sure we # execute the strip in the code before the eval key = "key.subKey | value + 1|value / 2 | value * 3" value1 = [{"d": 2}, {"g": {"h": "val"}}] sub_value = 1 expected_value_after_operations = 3 json_object = {"key": {"subKey": sub_value, "subkey2": value1}} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) returned_value = pollster.definitions.sample_extractor.\ retrieve_attribute_nested_value(json_object, key) self.assertEqual(expected_value_after_operations, returned_value) def test_retrieve_attribute_nested_value_simulate_radosgw_processing(self): key = "user | value.split('$') | value[0] | value.strip()" json_object = {"categories": [ { "bytes_received": 0, "bytes_sent": 357088, "category": "complete_multipart", "ops": 472, "successful_ops": 472 }], "total": { "bytes_received": 206739531986, "bytes_sent": 273793180, "ops": 119690, "successful_ops": 119682 }, "user": " 00ab8d7e76fc4$00ab8d7e76fc45a37776732" } expected_value_after_operations = "00ab8d7e76fc4" pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) returned_value = pollster.definitions.sample_extractor.\ retrieve_attribute_nested_value(json_object, key) self.assertEqual(expected_value_after_operations, returned_value) def fake_sample_multi_metric(self, **kwargs): multi_metric_sample_list = [ {"categories": [ { "bytes_received": 0, "bytes_sent": 0, "category": "create_bucket", "ops": 2, "successful_ops": 2 }, { "bytes_received": 0, "bytes_sent": 2120428, "category": "get_obj", "ops": 46, "successful_ops": 46 }, { "bytes_received": 0, "bytes_sent": 21484, "category": "list_bucket", "ops": 8, "successful_ops": 8 }, { "bytes_received": 6889056, "bytes_sent": 0, "category": "put_obj", "ops": 46, "successful_ops": 6 }], "total": { "bytes_received": 6889056, "bytes_sent": 2141912, "ops": 102, "successful_ops": 106 }, "user": "test-user"}] return multi_metric_sample_list @mock.patch.object( dynamic_pollster.PollsterSampleGatherer, 'execute_request_get_samples', fake_sample_multi_metric) def test_get_samples_multi_metric_pollster(self): pollster = dynamic_pollster.DynamicPollster( self.multi_metric_pollster_definition) fake_manager = self.FakeManager() samples = pollster.get_samples( fake_manager, None, ["https://endpoint.server.name.com/"]) samples_list = list(samples) self.assertEqual(4, len(samples_list)) create_bucket_sample = [ s for s in samples_list if s.name == "test-pollster.create_bucket"][0] get_obj_sample = [ s for s in samples_list if s.name == "test-pollster.get_obj"][0] list_bucket_sample = [ s for s in samples_list if s.name == "test-pollster.list_bucket"][0] put_obj_sample = [ s for s in samples_list if s.name == "test-pollster.put_obj"][0] self.assertEqual(2, create_bucket_sample.volume) self.assertEqual(46, get_obj_sample.volume) self.assertEqual(8, list_bucket_sample.volume) self.assertEqual(46, put_obj_sample.volume) def test_execute_request_get_samples_custom_ids(self): sample = {'user_id_attribute': "1", 'project_id_attribute': "2", 'resource_id_attribute': "3", 'user_id': "234", 'project_id': "2334", 'id': "35"} def internal_execute_request_get_samples_mock(self, **kwargs): class Response: @property def text(self): return json.dumps([sample]) def json(self): return [sample] return Response(), "url" original_method = dynamic_pollster.PollsterSampleGatherer.\ _internal_execute_request_get_samples try: dynamic_pollster.PollsterSampleGatherer. \ _internal_execute_request_get_samples = \ internal_execute_request_get_samples_mock self.pollster_definition_all_fields[ 'user_id_attribute'] = 'user_id_attribute' self.pollster_definition_all_fields[ 'project_id_attribute'] = 'project_id_attribute' self.pollster_definition_all_fields[ 'resource_id_attribute'] = 'resource_id_attribute' pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_all_fields) params = {"d": "d"} response = pollster.definitions.sample_gatherer. \ execute_request_get_samples(**params) self.assertEqual(sample['user_id_attribute'], response[0]['user_id']) self.assertEqual(sample['project_id_attribute'], response[0]['project_id']) self.assertEqual(sample['resource_id_attribute'], response[0]['id']) finally: dynamic_pollster.PollsterSampleGatherer. \ _internal_execute_request_get_samples = original_method def test_retrieve_attribute_self_reference_sample(self): key = " . | value['key1']['subKey1'][0]['d'] if 'key1' in value else 0" sub_value1 = [{"d": 2}, {"g": {"h": "val"}}] sub_value2 = [{"r": 245}, {"h": {"yu": "yu"}}] json_object = {"key1": {"subKey1": sub_value1}, "key2": {"subkey2": sub_value2}} pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) returned_value = pollster.definitions.sample_extractor.\ retrieve_attribute_nested_value(json_object, key) self.assertEqual(2, returned_value) del json_object['key1'] returned_value = pollster.definitions.sample_extractor.\ retrieve_attribute_nested_value(json_object, key) self.assertEqual(0, returned_value) def test_create_request_arguments_NonOpenStackApisSamplesGatherer(self): pollster_definition = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'url_path': "https://test.com/v1/test/endpoint/fake", "module": "someModule", "authentication_object": "objectAuthentication", "authentication_parameters": "authParam", "headers": [{"header1": "val1"}, {"header2": "val2"}]} pollster = dynamic_pollster.DynamicPollster(pollster_definition) request_args = pollster.definitions.sample_gatherer\ .create_request_arguments(pollster.definitions.configurations) self.assertIn("headers", request_args) self.assertEqual(2, len(request_args["headers"])) self.assertEqual(['header1', 'header2'], list(map(lambda h: list(h.keys())[0], request_args["headers"]))) self.assertEqual(['val1', 'val2'], list(map(lambda h: list(h.values())[0], request_args["headers"]))) self.assertNotIn("authenticated", request_args) def test_create_request_arguments_PollsterSampleGatherer(self): pollster_definition = copy.deepcopy( self.pollster_definition_only_required_fields) pollster_definition["headers"] = [ {"x-openstack-nova-api-version": "2.46"}, {"custom_header": "custom"}, {"some_other_header": "something"}] pollster = dynamic_pollster.DynamicPollster(pollster_definition) request_args = pollster.definitions.sample_gatherer\ .create_request_arguments(pollster.definitions.configurations) self.assertIn("headers", request_args) self.assertIn("authenticated", request_args) self.assertTrue(request_args["authenticated"]) self.assertEqual(3, len(request_args["headers"])) self.assertEqual(['x-openstack-nova-api-version', 'custom_header', "some_other_header"], list(map(lambda h: list(h.keys())[0], request_args["headers"]))) self.assertEqual(['2.46', 'custom', 'something'], list(map(lambda h: list(h.values())[0], request_args["headers"]))) def test_create_request_arguments_PollsterSampleGatherer_no_headers(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) request_args =\ pollster.definitions.sample_gatherer.create_request_arguments( pollster.definitions.configurations) self.assertNotIn("headers", request_args) self.assertIn("authenticated", request_args) self.assertTrue(request_args["authenticated"]) def test_metadata_nested_objects(self): generator = PagedSamplesGeneratorHttpRequestMock(samples_dict={ 'flavor': [{"name": "a", "ram": 1}, {"name": "b", "ram": 2}, {"name": "c", "ram": 3}, {"name": "d", "ram": 4}, {"name": "e", "ram": 5}, {"name": "f", "ram": 6}, {"name": "g", "ram": 7}, {"name": "h", "ram": 8}], 'name': ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8'], 'state': ['Active', 'Error', 'Down', 'Active', 'Active', 'Migrating', 'Active', 'Error'] }, dict_name='servers', page_link_name='server_link') generator.generate_samples('http://test.com/v1/test-servers', { 'marker=c3': 3, 'marker=f6': 3 }, 2) keystone_mock = mock.Mock() keystone_mock.session.get.side_effect = generator.mock_request fake_manager = self.FakeManager(keystone=keystone_mock) pollster_definition = dict(self.multi_metric_pollster_definition) pollster_definition['name'] = 'test-pollster' pollster_definition['value_attribute'] = 'state' pollster_definition['url_path'] = 'v1/test-servers' pollster_definition['response_entries_key'] = 'servers' pollster_definition['metadata_fields'] = ['flavor.name', 'flavor.ram'] pollster_definition['next_sample_url_attribute'] = \ 'server_link | filter(lambda v: v.get("rel") == "next", value) |' \ 'list(value)| value [0] | value.get("href")' pollster = dynamic_pollster.DynamicPollster(pollster_definition) samples = pollster.get_samples(fake_manager, None, ['http://test.com']) samples = list(samples) self.assertEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], list(map(lambda s: s.resource_metadata["flavor.name"], samples))) self.assertEqual(list(range(1, 9)), list(map(lambda s: s.resource_metadata["flavor.ram"], samples))) def test_get_request_linked_samples_url_endpoint_no_trailing_slash(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) base_url = ( "http://test.com:8779/v1.0/1a2b3c4d5e1a2b3c4d5e1a2b3c4d5e1a" ) expected_url = urlparse.urljoin( base_url + "/", self.pollster_definition_only_required_fields[ 'url_path']) kwargs = {'resource': base_url} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url( kwargs, pollster.definitions.configurations) self.assertEqual(expected_url, url) def test_get_request_linked_samples_url_endpoint_trailing_slash(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) base_url = "http://test.com:9511/v1/" expected_url = urlparse.urljoin( base_url, self.pollster_definition_only_required_fields[ 'url_path']) kwargs = {'resource': base_url} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url( kwargs, pollster.definitions.configurations) self.assertEqual(expected_url, url) def test_get_request_linked_samples_url_next_sample_url(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) base_url = "http://test.com/something_that_we_do_not_care" expected_url = "http://test.com/next_page" kwargs = {'resource': base_url, 'next_sample_url': expected_url} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url(kwargs, pollster.definitions) self.assertEqual(expected_url, url) def test_get_request_linked_samples_url_next_sample_only_url_path(self): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) base_url = "http://test.com/something_that_we_do_not_care" expected_url = "http://test.com/next_page" kwargs = {'resource': base_url, 'next_sample_url': "/next_page"} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url( kwargs, pollster.definitions.configurations) self.assertEqual(expected_url, url) def test_generate_sample_and_extract_metadata(self): definition = self.pollster_definition_only_required_fields.copy() definition['metadata_fields'] = ["metadata1", 'metadata2'] pollster = dynamic_pollster.DynamicPollster(definition) pollster_sample = {'metadata1': 'metadata1', 'metadata2': 'metadata2', 'value': 1} sample = pollster.definitions.sample_extractor.generate_sample( pollster_sample, pollster.definitions.configurations, manager=mock.Mock(), conf={}) self.assertEqual(1, sample.volume) self.assertEqual(2, len(sample.resource_metadata)) self.assertEqual('metadata1', sample.resource_metadata['metadata1']) self.assertEqual('metadata2', sample.resource_metadata['metadata2']) def test_generate_sample_and_extract_metadata_false_value(self): definition = self.pollster_definition_only_required_fields.copy() definition['metadata_fields'] = ["metadata1", 'metadata2', 'metadata3_false'] pollster = dynamic_pollster.DynamicPollster(definition) pollster_sample = {'metadata1': 'metadata1', 'metadata2': 'metadata2', 'metadata3_false': False, 'value': 1} sample = pollster.definitions.sample_extractor.generate_sample( pollster_sample, pollster.definitions.configurations, manager=mock.Mock(), conf={}) self.assertEqual(1, sample.volume) self.assertEqual(3, len(sample.resource_metadata)) self.assertEqual('metadata1', sample.resource_metadata['metadata1']) self.assertEqual('metadata2', sample.resource_metadata['metadata2']) self.assertIs(False, sample.resource_metadata['metadata3_false']) def test_generate_sample_and_extract_metadata_none_value(self): definition = self.pollster_definition_only_required_fields.copy() definition['metadata_fields'] = ["metadata1", 'metadata2', 'metadata3'] pollster = dynamic_pollster.DynamicPollster(definition) pollster_sample = {'metadata1': 'metadata1', 'metadata2': 'metadata2', 'metadata3': None, 'value': 1} sample = pollster.definitions.sample_extractor.generate_sample( pollster_sample, pollster.definitions.configurations, manager=mock.Mock(), conf={}) self.assertEqual(1, sample.volume) self.assertEqual(3, len(sample.resource_metadata)) self.assertEqual('metadata1', sample.resource_metadata['metadata1']) self.assertEqual('metadata2', sample.resource_metadata['metadata2']) self.assertIsNone(sample.resource_metadata['metadata3']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2203567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_heartbeat.py0000664000175000017500000001031515250267452024613 0ustar00zuulzuul# # Copyright 2024 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer polling heartbeat process""" import multiprocessing import shutil import tempfile from oslo_utils import timeutils from unittest import mock from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class TestHeartBeatManagert(base.BaseTestCase): def setUp(self): super().setUp() self.conf = service.prepare_service([], []) self.tmpdir = tempfile.mkdtemp() self.queue = multiprocessing.Queue() self.mgr = manager.AgentManager(0, self.conf, namespaces='central', queue=self.queue) def tearDown(self): super().tearDown() shutil.rmtree(self.tmpdir) def test_hb_not_configured(self): self.assertRaises(manager.HeartBeatException, manager.AgentHeartBeatManager, 0, self.conf, namespaces='ipmi', queue=self.queue) @mock.patch('ceilometer.polling.manager.LOG') def test_hb_startup(self, LOG): # activate heartbeat agent self.conf.set_override('heartbeat_socket_dir', self.tmpdir, group='polling') manager.AgentHeartBeatManager(0, self.conf, namespaces='compute', queue=self.queue) calls = [mock.call("Starting heartbeat child service. Listening on %s", f"{self.tmpdir}/ceilometer-compute.socket")] LOG.info.assert_has_calls(calls) @mock.patch('ceilometer.polling.manager.LOG') def test_hb_update(self, LOG): self.conf.set_override('heartbeat_socket_dir', self.tmpdir, group='polling') hb = manager.AgentHeartBeatManager(0, self.conf, namespaces='central', queue=self.queue) timestamp = timeutils.utcnow().isoformat() self.queue.put_nowait({'timestamp': timestamp, 'pollster': 'test'}) hb._update_status() calls = [mock.call("Updated heartbeat for %s %s", "test", timestamp)] LOG.debug.assert_has_calls(calls) @mock.patch('ceilometer.polling.manager.LOG') def test_hb_send(self, LOG): with mock.patch('socket.socket') as FakeSocket: sub_skt = mock.Mock() sub_skt.sendall.return_value = None sub_skt.sendall.return_value = None skt = FakeSocket.return_value skt.bind.return_value = mock.Mock() skt.listen.return_value = mock.Mock() skt.accept.return_value = (sub_skt, "") self.conf.set_override('heartbeat_socket_dir', self.tmpdir, group='polling') hb = manager.AgentHeartBeatManager(0, self.conf, namespaces='central', queue=self.queue) timestamp = timeutils.utcnow().isoformat() self.queue.put_nowait({'timestamp': timestamp, 'pollster': 'test1'}) hb._update_status() self.queue.put_nowait({'timestamp': timestamp, 'pollster': 'test2'}) hb._update_status() # test status report hb._send_heartbeat() calls = [mock.call("Heartbeat status report requested at %s", f"{self.tmpdir}/ceilometer-central.socket"), mock.call("Reported heartbeat status:\n%s", f"test1 {timestamp}\ntest2 {timestamp}")] LOG.debug.assert_has_calls(calls) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2213566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_manager.py0000664000175000017500000011502615250267452024273 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # Copyright 2013 Intel corp. # Copyright 2013 eNovance # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer agent manager""" import copy import datetime import multiprocessing import shutil import tempfile from unittest import mock import fixtures from openstack import exceptions as sdk_exceptions from oslo_utils import timeutils from stevedore import extension from ceilometer.compute import discovery as nova_discover from ceilometer.polling.dynamic_pollster import DynamicPollster from ceilometer.polling.dynamic_pollster import \ NonOpenStackApisPollsterDefinition from ceilometer.polling.dynamic_pollster import SingleMetricPollsterDefinitions from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import sample from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes def default_test_data(name='test'): return sample.Sample( name=name, type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'Pollster'}) class TestPollster(plugin_base.PollsterBase): test_data = default_test_data() discovery = None @property def default_discovery(self): return self.discovery def get_samples(self, manager, cache, resources): resources = resources or [] self.samples.append((manager, resources)) self.resources.extend(resources) c = copy.deepcopy(self.test_data) c.resource_metadata['resources'] = resources return [c] class PollingException(Exception): pass class TestPollsterBuilder(TestPollster): @classmethod def build_pollsters(cls, conf): return [('builder1', cls(conf)), ('builder2', cls(conf))] class TestManager(base.BaseTestCase): def setUp(self): super().setUp() self.conf = service.prepare_service([], []) def test_hash_of_set(self): x = ['a', 'b'] y = ['a', 'b', 'a'] z = ['a', 'c'] self.assertEqual(manager.hash_of_set(x), manager.hash_of_set(y)) self.assertNotEqual(manager.hash_of_set(x), manager.hash_of_set(z)) self.assertNotEqual(manager.hash_of_set(y), manager.hash_of_set(z)) def test_load_plugins(self): mgr = manager.AgentManager(0, self.conf, queue=multiprocessing.Queue()) self.assertIsNotNone(list(mgr.extensions)) @mock.patch('ceilometer.ipmi.pollsters.sensor.SensorPollster.__init__', mock.Mock(return_value=None)) def test_load_normal_plugins(self): mgr = manager.AgentManager(0, self.conf, namespaces=['ipmi'], queue=multiprocessing.Queue()) self.assertEqual(5, len(mgr.extensions)) # Skip loading pollster upon ImportError @mock.patch('ceilometer.ipmi.pollsters.sensor.SensorPollster.__init__', mock.Mock(side_effect=ImportError)) @mock.patch('ceilometer.polling.manager.LOG') def test_import_error_in_plugin(self, LOG): namespaces = ['ipmi'] manager.AgentManager(0, self.conf, namespaces=namespaces, queue=multiprocessing.Queue()) LOG.warning.assert_called_with( 'No valid pollsters can be loaded from %s namespaces', namespaces) # Exceptions other than ExtensionLoadError are propagated @mock.patch('ceilometer.ipmi.pollsters.sensor.SensorPollster.__init__', mock.Mock(side_effect=PollingException)) def test_load_exceptional_plugins(self): self.assertRaises(PollingException, manager.AgentManager, 0, self.conf, ['ipmi']) def test_builder(self): @staticmethod def fake_get_ext_mgr(namespace, *args, **kwargs): if 'builder' in namespace: return extension.ExtensionManager.make_test_instance( [ extension.Extension('builder', None, TestPollsterBuilder, None), ] ) else: return extension.ExtensionManager.make_test_instance( [ extension.Extension('test', None, None, TestPollster(self.conf)), ] ) with mock.patch.object(manager.AgentManager, '_get_ext_mgr', new=fake_get_ext_mgr): mgr = manager.AgentManager(0, self.conf, namespaces=['central']) self.assertEqual(3, len(mgr.extensions)) for ext in mgr.extensions: self.assertIn(ext.name, ['builder1', 'builder2', 'test']) self.assertIsInstance(ext.obj, TestPollster) class BatchTestPollster(TestPollster): test_data = default_test_data() discovery = None @property def default_discovery(self): return self.discovery def get_samples(self, manager, cache, resources): resources = resources or [] self.samples.append((manager, resources)) self.resources.extend(resources) for resource in resources: c = copy.deepcopy(self.test_data) c.timestamp = timeutils.utcnow().isoformat() c.resource_id = resource c.resource_metadata['resource'] = resource yield c class TestPollsterKeystone(TestPollster): def get_samples(self, manager, cache, resources): # Just try to use keystone, that will raise an exception manager.keystone.list_projects() class TestPollsterPollingException(TestPollster): discovery = 'test' polling_failures = 0 def get_samples(self, manager, cache, resources): func = super().get_samples sample = func(manager=manager, cache=cache, resources=resources) # Raise polling exception after 2 times self.polling_failures += 1 if self.polling_failures > 2: raise plugin_base.PollsterPermanentError(resources) return sample class TestDiscovery(plugin_base.DiscoveryBase): def discover(self, manager, param=None): self.params.append(param) return self.resources class TestDiscoveryException(plugin_base.DiscoveryBase): def discover(self, manager, param=None): self.params.append(param) raise Exception() class BaseAgent(base.BaseTestCase): class Pollster(TestPollster): samples = [] resources = [] test_data = default_test_data() class BatchPollster(BatchTestPollster): samples = [] resources = [] test_data = default_test_data() class PollsterAnother(TestPollster): samples = [] resources = [] test_data = default_test_data('testanother') class PollsterKeystone(TestPollsterKeystone): samples = [] resources = [] test_data = default_test_data('testkeystone') class PollsterPollingException(TestPollsterPollingException): samples = [] resources = [] test_data = default_test_data('testpollingexception') class Discovery(TestDiscovery): params = [] resources = [] class DiscoveryAnother(TestDiscovery): params = [] resources = [] @property def group_id(self): return 'another_group' class DiscoveryException(TestDiscoveryException): params = [] def setup_polling(self, poll_cfg=None, override_conf=None): name = self.cfg2file(poll_cfg or self.polling_cfg) conf_to_use = override_conf or self.CONF conf_to_use.set_override('cfg_file', name, group='polling') self.mgr.polling_manager = manager.PollingManager(conf_to_use) def create_manager(self): queue = multiprocessing.Queue() return manager.AgentManager(0, self.CONF, queue=queue) def fake_notifier_sample(self, ctxt, event_type, payload): for m in payload['samples']: del m['message_signature'] self.notified_samples.append(m) def setUp(self): super().setUp() self.notified_samples = [] self.notifier = mock.Mock() self.notifier.sample.side_effect = self.fake_notifier_sample self.useFixture(fixtures.MockPatch('oslo_messaging.Notifier', return_value=self.notifier)) self.setup_connection(projects=[fakes.PROJECT_ADMIN_sdk]) self.CONF = service.prepare_service([], []) self.CONF.set_override( 'cfg_file', self.path_get('etc/ceilometer/polling_all.yaml'), group='polling' ) self.polling_cfg = { 'sources': [{ 'name': 'test_polling', 'interval': 60, 'meters': ['test'], 'resources': ['test://']}] } def tearDown(self): self.PollsterKeystone.samples = [] self.PollsterKeystone.resources = [] self.PollsterPollingException.samples = [] self.PollsterPollingException.resources = [] self.Pollster.samples = [] self.Pollster.discovery = [] self.PollsterAnother.samples = [] self.PollsterAnother.discovery = [] self.Pollster.resources = [] self.PollsterAnother.resources = [] self.Discovery.params = [] self.DiscoveryAnother.params = [] self.DiscoveryException.params = [] self.Discovery.resources = [] self.DiscoveryAnother.resources = [] super().tearDown() def create_extension_list(self): return [extension.Extension('test', None, None, self.Pollster(self.CONF), ), extension.Extension('testbatch', None, None, self.BatchPollster(self.CONF), ), extension.Extension('testanother', None, None, self.PollsterAnother(self.CONF), ), extension.Extension('testkeystone', None, None, self.PollsterKeystone(self.CONF), ), extension.Extension('testpollingexception', None, None, self.PollsterPollingException(self.CONF), ) ] def create_discoveries(self): return extension.ExtensionManager.make_test_instance( [ extension.Extension( 'testdiscovery', None, None, self.Discovery(self.CONF), ), extension.Extension( 'testdiscoveryanother', None, None, self.DiscoveryAnother(self.CONF), ), extension.Extension( 'testdiscoveryexception', None, None, self.DiscoveryException(self.CONF), ), ], ) class TestPollingAgent(BaseAgent): def setUp(self): super().setUp() self.mgr = self.create_manager() self.mgr.extensions = self.create_extension_list() self.setup_polling() @mock.patch('ceilometer.polling.manager.PollingManager') def test_start(self, poll_manager): self.mgr.setup_polling_tasks = mock.MagicMock() self.mgr.run() poll_manager.assert_called_once_with(self.CONF) self.mgr.setup_polling_tasks.assert_called_once_with() self.mgr.terminate() def test_setup_polling_tasks(self): polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(1, len(polling_tasks)) self.assertIn(60, polling_tasks.keys()) per_task_resources = polling_tasks[60].resources self.assertEqual(1, len(per_task_resources)) self.assertEqual(set(self.polling_cfg['sources'][0]['resources']), set(per_task_resources['test_polling-test'].get({}))) def test_setup_polling_tasks_multiple_interval(self): self.polling_cfg['sources'].append({ 'name': 'test_polling_1', 'interval': 10, 'meters': ['test'], 'resources': ['test://'], }) self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(2, len(polling_tasks)) self.assertIn(60, polling_tasks.keys()) self.assertIn(10, polling_tasks.keys()) def test_setup_polling_tasks_mismatch_counter(self): self.polling_cfg['sources'].append({ 'name': 'test_polling_1', 'interval': 10, 'meters': ['test_invalid'], 'resources': ['invalid://'], }) polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(1, len(polling_tasks)) self.assertIn(60, polling_tasks.keys()) self.assertNotIn(10, polling_tasks.keys()) @mock.patch('glob.glob') @mock.patch('ceilometer.declarative.load_definitions') def test_setup_polling_dynamic_pollster_namespace(self, load_mock, glob_mock): glob_mock.return_value = ['test.yml'] load_mock.return_value = [{ 'name': "test.dynamic.pollster", 'namespaces': "dynamic", 'sample_type': 'gauge', 'unit': 'test', 'endpoint_type': 'test', 'url_path': 'test', 'value_attribute': 'test' }, { 'name': "test.compute.central.pollster", 'sample_type': 'gauge', 'namespaces': ["compute", "central"], 'unit': 'test', 'endpoint_type': 'test', 'url_path': 'test', 'value_attribute': 'test' }, { 'name': "test.compute.pollster", 'namespaces': ["compute"], 'sample_type': 'gauge', 'unit': 'test', 'endpoint_type': 'test', 'url_path': 'test', 'value_attribute': 'test' }, { 'name': "test.central.pollster", 'sample_type': 'gauge', 'unit': 'test', 'endpoint_type': 'test', 'url_path': 'test', 'value_attribute': 'test' }] mgr = manager.AgentManager(0, self.CONF, namespaces=['dynamic']) self.assertEqual(len(mgr.extensions), 1) self.assertEqual( mgr.extensions[0].definitions.configurations['name'], 'test.dynamic.pollster') mgr = manager.AgentManager(0, self.CONF) self.assertEqual( mgr.extensions[-3].definitions.configurations['name'], 'test.compute.central.pollster') self.assertEqual( mgr.extensions[-2].definitions.configurations['name'], 'test.compute.pollster') self.assertEqual( mgr.extensions[-1].definitions.configurations['name'], 'test.central.pollster') mgr = manager.AgentManager(0, self.CONF, namespaces=['compute']) self.assertEqual( mgr.extensions[-2].definitions.configurations['name'], 'test.compute.central.pollster') self.assertEqual( mgr.extensions[-1].definitions.configurations['name'], 'test.compute.pollster') mgr = manager.AgentManager(0, self.CONF, ['central']) self.assertEqual( mgr.extensions[-2].definitions.configurations['name'], 'test.compute.central.pollster') self.assertEqual( mgr.extensions[-1].definitions.configurations['name'], 'test.central.pollster') def test_setup_polling_task_same_interval(self): self.polling_cfg['sources'].append({ 'name': 'test_polling_1', 'interval': 60, 'meters': ['testanother'], 'resources': ['testanother://'], }) self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(1, len(polling_tasks)) pollsters = polling_tasks.get(60).pollster_matches self.assertEqual(2, len(pollsters)) per_task_resources = polling_tasks[60].resources self.assertEqual(2, len(per_task_resources)) key = 'test_polling-test' self.assertEqual(set(self.polling_cfg['sources'][0]['resources']), set(per_task_resources[key].get({}))) key = 'test_polling_1-testanother' self.assertEqual(set(self.polling_cfg['sources'][1]['resources']), set(per_task_resources[key].get({}))) def _verify_discovery_params(self, expected): self.assertEqual(expected, self.Discovery.params) self.assertEqual(expected, self.DiscoveryAnother.params) self.assertEqual(expected, self.DiscoveryException.params) def _do_test_per_pollster_discovery(self, discovered_resources, static_resources): self.Pollster.discovery = 'testdiscovery' self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = discovered_resources self.DiscoveryAnother.resources = [d[::-1] for d in discovered_resources] if static_resources: # just so we can test that static + pre_polling amalgamated # override per_pollster self.polling_cfg['sources'][0]['discovery'] = [ 'testdiscoveryanother', 'testdiscoverynonexistent', 'testdiscoveryexception'] self.polling_cfg['sources'][0]['resources'] = static_resources self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) if static_resources: self.assertEqual(set(static_resources + self.DiscoveryAnother.resources), set(self.Pollster.resources)) else: self.assertEqual(set(self.Discovery.resources), set(self.Pollster.resources)) # Make sure no duplicated resource from discovery for x in self.Pollster.resources: self.assertEqual(1, self.Pollster.resources.count(x)) def test_per_pollster_discovery(self): self._do_test_per_pollster_discovery(['discovered_1', 'discovered_2'], []) def test_per_pollster_discovery_overridden_by_per_polling_discovery(self): # ensure static+per_source_discovery overrides per_pollster_discovery self._do_test_per_pollster_discovery(['discovered_1', 'discovered_2'], ['static_1', 'static_2']) def test_per_pollster_discovery_duplicated(self): self._do_test_per_pollster_discovery(['dup', 'discovered_1', 'dup'], []) def test_per_pollster_discovery_overridden_by_duplicated_static(self): self._do_test_per_pollster_discovery(['discovered_1', 'discovered_2'], ['static_1', 'dup', 'dup']) def test_per_pollster_discovery_caching(self): # ensure single discovery associated with multiple pollsters # only called once per polling cycle discovered_resources = ['discovered_1', 'discovered_2'] self.Pollster.discovery = 'testdiscovery' self.PollsterAnother.discovery = 'testdiscovery' self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = discovered_resources self.polling_cfg['sources'][0]['meters'].append('testanother') self.polling_cfg['sources'][0]['resources'] = [] self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) self.assertEqual(1, len(self.Discovery.params)) self.assertEqual(discovered_resources, self.Pollster.resources) self.assertEqual(discovered_resources, self.PollsterAnother.resources) def _do_test_per_polling_discovery(self, discovered_resources, static_resources): self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = discovered_resources self.DiscoveryAnother.resources = [d[::-1] for d in discovered_resources] self.polling_cfg['sources'][0]['discovery'] = [ 'testdiscovery', 'testdiscoveryanother', 'testdiscoverynonexistent', 'testdiscoveryexception'] self.polling_cfg['sources'][0]['resources'] = static_resources self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) discovery = self.Discovery.resources + self.DiscoveryAnother.resources # compare resource lists modulo ordering self.assertEqual(set(static_resources + discovery), set(self.Pollster.resources)) # Make sure no duplicated resource from discovery for x in self.Pollster.resources: self.assertEqual(1, self.Pollster.resources.count(x)) def test_per_polling_discovery_discovered_only(self): self._do_test_per_polling_discovery(['discovered_1', 'discovered_2'], []) def test_per_polling_discovery_static_only(self): self._do_test_per_polling_discovery([], ['static_1', 'static_2']) def test_per_polling_discovery_discovered_augmented_by_static(self): self._do_test_per_polling_discovery(['discovered_1', 'discovered_2'], ['static_1', 'static_2']) def test_per_polling_discovery_discovered_duplicated_static(self): self._do_test_per_polling_discovery(['discovered_1', 'pud'], ['dup', 'static_1', 'dup']) def test_multiple_pollings_different_static_resources(self): # assert that the individual lists of static and discovered resources # for each polling with a common interval are passed to individual # pollsters matching each polling self.polling_cfg['sources'][0]['resources'] = ['test://'] self.polling_cfg['sources'][0]['discovery'] = ['testdiscovery'] self.polling_cfg['sources'].append({ 'name': 'another_polling', 'interval': 60, 'meters': ['test'], 'resources': ['another://'], 'discovery': ['testdiscoveryanother'], }) self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = ['discovered_1', 'discovered_2'] self.DiscoveryAnother.resources = ['discovered_3', 'discovered_4'] self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(1, len(polling_tasks)) self.assertIn(60, polling_tasks.keys()) self.mgr.interval_task(polling_tasks.get(60)) self.assertEqual([None], self.Discovery.params) self.assertEqual([None], self.DiscoveryAnother.params) self.assertEqual(2, len(self.Pollster.samples)) samples = self.Pollster.samples test_resources = ['test://', 'discovered_1', 'discovered_2'] another_resources = ['another://', 'discovered_3', 'discovered_4'] if samples[0][1] == test_resources: self.assertEqual(another_resources, samples[1][1]) elif samples[0][1] == another_resources: self.assertEqual(test_resources, samples[1][1]) else: self.fail('unexpected sample resources %s' % samples) def test_multiple_sources_different_discoverers(self): self.Discovery.resources = ['discovered_1', 'discovered_2'] self.DiscoveryAnother.resources = ['discovered_3', 'discovered_4'] sources = [{'name': 'test_source_1', 'interval': 60, 'meters': ['test'], 'discovery': ['testdiscovery']}, {'name': 'test_source_2', 'interval': 60, 'meters': ['testanother'], 'discovery': ['testdiscoveryanother']}] self.polling_cfg = {'sources': sources} self.mgr.discoveries = self.create_discoveries() self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.assertEqual(1, len(polling_tasks)) self.assertIn(60, polling_tasks.keys()) self.mgr.interval_task(polling_tasks.get(60)) self.assertEqual(1, len(self.Pollster.samples)) self.assertEqual(['discovered_1', 'discovered_2'], self.Pollster.resources) self.assertEqual(1, len(self.PollsterAnother.samples)) self.assertEqual(['discovered_3', 'discovered_4'], self.PollsterAnother.resources) @mock.patch('ceilometer.polling.manager.LOG') def test_polling_and_notify_with_resources(self, LOG): self.setup_polling() polling_task = list(self.mgr.setup_polling_tasks().values())[0] polling_task.poll_and_notify() LOG.info.assert_has_calls([ mock.call('Polling pollster %(poll)s in the context of %(src)s', {'poll': 'test', 'src': 'test_polling'}), mock.call('Finished polling pollster %(poll)s in the context ' 'of %(src)s', {'poll': 'test', 'src': 'test_polling'}) ]) LOG.debug.assert_has_calls([ mock.call('Polster heartbeat update: %s', 'test') ]) @mock.patch('ceilometer.polling.manager.LOG') def test_polling_and_notify_with_resources_with_threads(self, log_mock): conf_to_use = self.CONF conf_to_use.set_override( 'threads_to_process_pollsters', 4, group='polling') self.setup_polling(override_conf=conf_to_use) polling_task = list(self.mgr.setup_polling_tasks().values())[0] polling_task.poll_and_notify() log_mock.info.assert_has_calls([ mock.call('Polling pollster %(poll)s in the context of %(src)s', {'poll': 'test', 'src': 'test_polling'}), mock.call('Finished polling pollster %(poll)s in the context ' 'of %(src)s', {'poll': 'test', 'src': 'test_polling'}) ]) log_mock.debug.assert_has_calls([ mock.call('Polster heartbeat update: %s', 'test') ]) # Even though we enabled 4 threads, we have only one metric configured. # Therefore, there should be only one call here. self.assertEqual(1, polling_task.manager.notifier.sample.call_count) @mock.patch('ceilometer.polling.manager.LOG') def test_skip_polling_and_notify_with_no_resources(self, LOG): self.polling_cfg['sources'][0]['resources'] = [] self.setup_polling() polling_task = list(self.mgr.setup_polling_tasks().values())[0] pollster = list(polling_task.pollster_matches['test_polling'])[0] polling_task.poll_and_notify() LOG.debug.assert_has_calls([mock.call( 'Skip pollster %(name)s, no %(p_context)s resources found ' 'this cycle', {'name': pollster.name, 'p_context': ''})]) @mock.patch('ceilometer.polling.manager.LOG') def test_skip_polling_polled_resources(self, LOG): self.polling_cfg['sources'].append({ 'name': 'test_polling_1', 'interval': 60, 'meters': ['test'], 'resources': ['test://'], }) self.setup_polling() polling_task = list(self.mgr.setup_polling_tasks().values())[0] polling_task.poll_and_notify() LOG.debug.assert_has_calls([mock.call( 'Skip pollster %(name)s, no %(p_context)s resources found ' 'this cycle', {'name': 'test', 'p_context': 'new'})]) @mock.patch('oslo_utils.timeutils.utcnow') def test_polling_samples_timestamp(self, mock_utc): polled_samples = [] timestamp = '2222-11-22T00:11:22.333333' def fake_send_notification(samples): polled_samples.extend(samples) mock_utc.return_value = datetime.datetime.strptime( timestamp, "%Y-%m-%dT%H:%M:%S.%f") self.setup_polling() polling_task = list(self.mgr.setup_polling_tasks().values())[0] polling_task._send_notification = mock.Mock( side_effect=fake_send_notification) polling_task.poll_and_notify() self.assertEqual(timestamp, polled_samples[0]['timestamp']) def test_get_sample_resources(self): polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(list(polling_tasks.values())[0]) self.assertTrue(self.Pollster.resources) def test_when_keystone_fail(self): """Test for bug 1316532.""" poll_cfg = { 'sources': [{ 'name': "test_keystone", 'interval': 10, 'meters': ['testkeystone'], 'resources': ['test://'], 'sinks': ['test_sink']}], 'sinks': [{ 'name': 'test_sink', 'publishers': ["test"]}] } self.setup_polling(poll_cfg) polling_tasks = self.mgr.setup_polling_tasks() # Reset the cached keystone client so the next access re-initialises, # then override the v3 mock to simulate keystone becoming unavailable. self.mgr._keystone = None self.mgr._keystone_last_exception = None self.useFixture(fixtures.MockPatch( 'openstack.connection.Connection', side_effect=sdk_exceptions.HttpException)) self.mgr.interval_task(list(polling_tasks.values())[0]) self.assertFalse(self.PollsterKeystone.samples) self.assertFalse(self.notified_samples) @mock.patch('ceilometer.polling.manager.LOG') def test_polling_exception(self, LOG): source_name = 'test_pollingexception' res_list = ['test://'] poll_cfg = { 'sources': [{ 'name': source_name, 'interval': 10, 'meters': ['testpollingexception'], 'resources': res_list, 'sinks': ['test_sink']}], 'sinks': [{ 'name': 'test_sink', 'publishers': ["test"]}] } self.setup_polling(poll_cfg) polling_task = list(self.mgr.setup_polling_tasks().values())[0] pollster = list(polling_task.pollster_matches[source_name])[0] # 2 samples after 4 pollings, as pollster got disabled upon exception for x in range(0, 4): self.mgr.interval_task(polling_task) samples = self.notified_samples self.assertEqual(2, len(samples)) LOG.error.assert_called_once_with(( 'Prevent pollster %(name)s from ' 'polling %(res_list)s on source %(source)s anymore!'), dict(name=pollster.name, res_list=str(res_list), source=source_name)) @mock.patch('ceilometer.polling.manager.LOG') def test_polling_novalike_exception(self, LOG): source_name = 'test_pollingexception' poll_cfg = { 'sources': [{ 'name': source_name, 'interval': 10, 'meters': ['testpollingexception'], 'sinks': ['test_sink']}], 'sinks': [{ 'name': 'test_sink', 'publishers': ["test"]}] } self.setup_polling(poll_cfg) polling_task = list(self.mgr.setup_polling_tasks().values())[0] pollster = list(polling_task.pollster_matches[source_name])[0] with mock.patch.object(polling_task.manager, 'discover') as disco: # NOTE(gordc): polling error on 3rd poll for __ in range(4): disco.return_value = ( [nova_discover.NovaLikeServer(**{'id': 1})]) self.mgr.interval_task(polling_task) LOG.error.assert_called_once_with(( 'Prevent pollster %(name)s from ' 'polling %(res_list)s on source %(source)s anymore!'), dict(name=pollster.name, res_list="[]", source=source_name)) def test_batching_polled_samples_disable_batch(self): self.CONF.set_override('batch_size', 0, group='polling') self._batching_samples(4, 4) def test_batching_polled_samples_batch_size(self): self.CONF.set_override('batch_size', 2, group='polling') self._batching_samples(4, 2) def test_batching_polled_samples_default(self): self._batching_samples(4, 1) def _batching_samples(self, expected_samples, call_count): poll_cfg = { 'sources': [{ 'name': 'test_pipeline', 'interval': 1, 'meters': ['testbatch'], 'resources': ['alpha', 'beta', 'gamma', 'delta'], 'sinks': ['test_sink']}], 'sinks': [{ 'name': 'test_sink', 'publishers': ["test"]}] } self.setup_polling(poll_cfg) polling_task = list(self.mgr.setup_polling_tasks().values())[0] self.mgr.interval_task(polling_task) samples = self.notified_samples self.assertEqual(expected_samples, len(samples)) self.assertEqual(call_count, self.notifier.sample.call_count) class TestPollingAgentPartitioned(BaseAgent): def setUp(self): super().setUp() self.tempdir = tempfile.mkdtemp() self.CONF.set_override("backend_url", "file://%s" % self.tempdir, "coordination") self.addCleanup(shutil.rmtree, self.tempdir, ignore_errors=True) self.hashring = mock.MagicMock() self.hashring.belongs_to_self = mock.MagicMock() self.hashring.belongs_to_self.return_value = True self.mgr = self.create_manager() self.mgr.extensions = self.create_extension_list() self.mgr.hashrings = mock.MagicMock() self.mgr.hashrings.__getitem__.return_value = self.hashring self.setup_polling() def test_discovery_partitioning(self): discovered_resources = ['discovered_1', 'discovered_2'] self.Pollster.discovery = 'testdiscovery' self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = discovered_resources self.polling_cfg['sources'][0]['discovery'] = [ 'testdiscovery', 'testdiscoveryanother', 'testdiscoverynonexistent', 'testdiscoveryexception'] self.polling_cfg['sources'][0]['resources'] = [] self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) self.hashring.belongs_to_self.assert_has_calls( [mock.call('discovered_1'), mock.call('discovered_2')]) def test_discovery_partitioning_unhashable(self): discovered_resources = [{'unhashable': True}] self.Pollster.discovery = 'testdiscovery' self.mgr.discoveries = self.create_discoveries() self.Discovery.resources = discovered_resources self.polling_cfg['sources'][0]['discovery'] = [ 'testdiscovery', 'testdiscoveryanother', 'testdiscoverynonexistent', 'testdiscoveryexception'] self.polling_cfg['sources'][0]['resources'] = [] self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) self.hashring.belongs_to_self.assert_has_calls( [mock.call('{\'unhashable\': True}')]) def test_static_resources_partitioning(self): static_resources = ['static_1', 'static_2'] static_resources2 = ['static_3', 'static_4'] self.polling_cfg['sources'][0]['resources'] = static_resources self.polling_cfg['sources'].append({ 'name': 'test_polling2', 'interval': 60, 'meters': ['test', 'test2'], 'resources': static_resources2, }) # have one polling without static resources defined self.polling_cfg['sources'].append({ 'name': 'test_polling3', 'interval': 60, 'meters': ['test', 'test2'], 'resources': [], }) self.setup_polling() polling_tasks = self.mgr.setup_polling_tasks() self.mgr.interval_task(polling_tasks.get(60)) self.hashring.belongs_to_self.assert_has_calls([ mock.call('static_1'), mock.call('static_2'), mock.call('static_3'), mock.call('static_4'), ], any_order=True) def test_instantiate_dynamic_pollster_standard_pollster(self): pollster_definition_only_required_fields = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'endpoint_type': "test", 'url_path': "v1/test/endpoint/fake"} pollster = DynamicPollster(pollster_definition_only_required_fields) self.assertIsInstance(pollster.definitions, SingleMetricPollsterDefinitions) def test_instantiate_dynamic_pollster_non_openstack_api(self): pollster_definition_only_required_fields = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'url_path': "v1/test/endpoint/fake", 'module': "module-name", 'authentication_object': "authentication_object"} pollster = DynamicPollster(pollster_definition_only_required_fields) self.assertIsInstance(pollster.definitions, NonOpenStackApisPollsterDefinition) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2213566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_non_openstack_credentials_discovery.py0000664000175000017500000000743615250267452032173 0ustar00zuulzuul# Copyright 2014-2015 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import requests from ceilometer.polling.discovery.endpoint import EndpointDiscovery from ceilometer.polling.discovery.non_openstack_credentials_discovery import \ NonOpenStackCredentialsDiscovery from ceilometer.tests import base class TestNonOpenStackCredentialsDiscovery(base.BaseTestCase): class FakeResponse: status_code = None json_object = None _content = "" def json(self): return self.json_object def raise_for_status(self): raise requests.HTTPError("Mock HTTP error.", response=self) class FakeManager: def __init__(self, keystone_client_mock): self._keystone = keystone_client_mock def setUp(self): super().setUp() self.discovery = NonOpenStackCredentialsDiscovery(None) def test_discover_no_parameters(self): result = self.discovery.discover(None, None) self.assertEqual([], result) result = self.discovery.discover(None, "") self.assertEqual([], result) def test_discover_no_barbican_endpoint(self): def discover_mock(self, manager, param=None): return [] original_discover_method = EndpointDiscovery.discover EndpointDiscovery.discover = discover_mock result = self.discovery.discover(None, "param") self.assertEqual([], result) EndpointDiscovery.discover = original_discover_method def test_discover_error_response(self): def discover_mock(self, manager, param=None): return ["barbican_url"] original_discover_method = EndpointDiscovery.discover EndpointDiscovery.discover = discover_mock for http_status_code in requests.status_codes._codes.keys(): if http_status_code < 400: continue return_value = self.FakeResponse() return_value.status_code = http_status_code return_value.json_object = {} client_mock = mock.Mock() client_mock.session.get.return_value = return_value exception = self.assertRaises( requests.HTTPError, self.discovery.discover, manager=self.FakeManager(client_mock), param="param") self.assertEqual("Mock HTTP error.", str(exception)) EndpointDiscovery.discover = original_discover_method def test_discover_response_ok(self): discover_mock = mock.MagicMock() discover_mock.return_value = ["barbican_url"] original_discover_method = EndpointDiscovery.discover EndpointDiscovery.discover = discover_mock return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value.json_object = {} return_value._content = "content" client_mock = mock.Mock() client_mock.session.get.return_value = return_value fake_manager = self.FakeManager(client_mock) response = self.discovery.discover(manager=fake_manager, param="param") self.assertEqual(["content"], response) discover_mock.assert_has_calls([ mock.call(fake_manager, "key-manager")]) EndpointDiscovery.discover = original_discover_method ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2223568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py0000664000175000017500000004615515250267452031160 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for Non-OpenStack dynamic pollsters """ import copy import json import sys from unittest import mock import requests from urllib import parse as urlparse from ceilometer.declarative import DynamicPollsterDefinitionException from ceilometer.declarative import NonOpenStackApisDynamicPollsterException from ceilometer.polling.dynamic_pollster import DynamicPollster from ceilometer.polling.dynamic_pollster import MultiMetricPollsterDefinitions from ceilometer.polling.dynamic_pollster import \ NonOpenStackApisPollsterDefinition from ceilometer.polling.dynamic_pollster import NonOpenStackApisSamplesGatherer from ceilometer.polling.dynamic_pollster import PollsterSampleGatherer from ceilometer.polling.dynamic_pollster import SingleMetricPollsterDefinitions from ceilometer.tests import base REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit', 'value_attribute', 'url_path', 'module', 'authentication_object'] OPTIONAL_POLLSTER_FIELDS = ['metadata_fields', 'skip_sample_values', 'value_mapping', 'default_value', 'metadata_mapping', 'preserve_mapped_metadata', 'response_entries_key', 'user_id_attribute', 'resource_id_attribute', 'barbican_secret_id', 'authentication_parameters', 'project_id_attribute'] ALL_POLLSTER_FIELDS = REQUIRED_POLLSTER_FIELDS + OPTIONAL_POLLSTER_FIELDS def fake_sample_multi_metric(self, **kwargs): multi_metric_sample_list = [ {"user_id": "UID-U007", "project_id": "UID-P007", "id": "UID-007", "categories": [ { "bytes_received": 0, "bytes_sent": 0, "category": "create_bucket", "ops": 2, "successful_ops": 2 }, { "bytes_received": 0, "bytes_sent": 2120428, "category": "get_obj", "ops": 46, "successful_ops": 46 }, { "bytes_received": 0, "bytes_sent": 21484, "category": "list_bucket", "ops": 8, "successful_ops": 8 }, { "bytes_received": 6889056, "bytes_sent": 0, "category": "put_obj", "ops": 46, "successful_ops": 6 }], "total": { "bytes_received": 6889056, "bytes_sent": 2141912, "ops": 102, "successful_ops": 106 }, "user": "test-user"}] return multi_metric_sample_list class TestNonOpenStackApisDynamicPollster(base.BaseTestCase): class FakeManager: _keystone = None class FakeResponse: status_code = None json_object = None def json(self): return self.json_object def raise_for_status(self): raise requests.HTTPError("Mock HTTP error.", response=self) def setUp(self): super().setUp() self.pollster_definition_only_openstack_required_single_metric = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", "endpoint_type": "type", 'url_path': "v1/test/endpoint/fake"} self.pollster_definition_only_openstack_required_multi_metric = { 'name': "test-pollster.{category}", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "[categories].ops", 'url_path': "v1/test/endpoint/fake", "endpoint_type": "type"} self.pollster_definition_only_required_fields = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'url_path': "http://server.com/v1/test/endpoint/fake", 'module': "module-name", 'authentication_object': "authentication_object"} self.pollster_definition_all_fields = { 'name': "test-pollster", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "volume", 'url_path': "v1/test/endpoint/fake", 'module': "module-name", 'authentication_object': "authentication_object", 'user_id_attribute': 'user_id', 'project_id_attribute': 'project_id', 'resource_id_attribute': 'id', 'barbican_secret_id': 'barbican_id', 'authentication_parameters': 'parameters'} self.pollster_definition_all_fields_multi_metrics = { 'name': "test-pollster.{category}", 'sample_type': "gauge", 'unit': "test", 'value_attribute': "[categories].ops", 'url_path': "v1/test/endpoint/fake", 'module': "module-name", 'authentication_object': "authentication_object", 'user_id_attribute': 'user_id', 'project_id_attribute': 'project_id', 'resource_id_attribute': 'id', 'barbican_secret_id': 'barbican_id', 'authentication_parameters': 'parameters'} def test_all_fields(self): all_required = ['module', 'authentication_object', 'name', 'sample_type', 'unit', 'value_attribute', 'url_path'] all_optional = ['metadata_fields', 'skip_sample_values', 'value_mapping', 'default_value', 'metadata_mapping', 'preserve_mapped_metadata', 'user_id_attribute', 'project_id_attribute', 'resource_id_attribute', 'barbican_secret_id', 'authentication_parameters', 'response_entries_key'] + all_required for field in all_required: self.assertIn(field, REQUIRED_POLLSTER_FIELDS) for field in all_optional: self.assertIn(field, ALL_POLLSTER_FIELDS) def test_all_required_fields_exceptions(self): for key in REQUIRED_POLLSTER_FIELDS: if key == 'module': continue pollster_definition = copy.deepcopy( self.pollster_definition_only_required_fields) pollster_definition.pop(key) exception = self.assertRaises( DynamicPollsterDefinitionException, DynamicPollster, pollster_definition, None, [NonOpenStackApisPollsterDefinition]) self.assertEqual("Required fields ['%s'] not specified." % key, exception.brief_message) def test_set_default_values(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) pollster_definitions = pollster.pollster_definitions self.assertEqual("user_id", pollster_definitions['user_id_attribute']) self.assertEqual("project_id", pollster_definitions['project_id_attribute']) self.assertEqual("id", pollster_definitions['resource_id_attribute']) self.assertEqual('', pollster_definitions['barbican_secret_id']) self.assertEqual('', pollster_definitions['authentication_parameters']) def test_user_set_optional_parameters(self): pollster = DynamicPollster( self.pollster_definition_all_fields) pollster_definitions = pollster.pollster_definitions self.assertEqual('user_id', pollster_definitions['user_id_attribute']) self.assertEqual('project_id', pollster_definitions['project_id_attribute']) self.assertEqual('id', pollster_definitions['resource_id_attribute']) self.assertEqual('barbican_id', pollster_definitions['barbican_secret_id']) self.assertEqual('parameters', pollster_definitions['authentication_parameters']) def test_default_discovery_empty_secret_id(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) self.assertEqual("barbican:", pollster.definitions.sample_gatherer. default_discovery) def test_default_discovery_not_empty_secret_id(self): pollster = DynamicPollster( self.pollster_definition_all_fields) self.assertEqual("barbican:barbican_id", pollster.definitions. sample_gatherer.default_discovery) @mock.patch('requests.get') def test_internal_execute_request_get_samples_status_code_ok( self, get_mock): sys.modules['module-name'] = mock.MagicMock() pollster = DynamicPollster( self.pollster_definition_only_required_fields) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value.json_object = {} return_value.reason = "Ok" get_mock.return_value = return_value kwargs = {'resource': "credentials"} resp, url = pollster.definitions.sample_gatherer.\ _internal_execute_request_get_samples( pollster.definitions.configurations, **kwargs) self.assertEqual( self.pollster_definition_only_required_fields['url_path'], url) self.assertEqual(return_value, resp) @mock.patch('requests.get') def test_internal_execute_request_get_samples_status_code_not_ok( self, get_mock): sys.modules['module-name'] = mock.MagicMock() pollster = DynamicPollster( self.pollster_definition_only_required_fields) for http_status_code in requests.status_codes._codes.keys(): if http_status_code >= 400: return_value = self.FakeResponse() return_value.status_code = http_status_code return_value.json_object = {} return_value.reason = requests.status_codes._codes[ http_status_code][0] get_mock.return_value = return_value kwargs = {'resource': "credentials"} exception = self.assertRaises( NonOpenStackApisDynamicPollsterException, pollster.definitions.sample_gatherer. _internal_execute_request_get_samples, pollster.definitions.configurations, **kwargs) self.assertEqual( "NonOpenStackApisDynamicPollsterException" " None: Error while executing request[%s]." " Status[%s] and reason [%s]." % (self.pollster_definition_only_required_fields['url_path'], http_status_code, return_value.reason), str(exception)) def test_generate_new_attributes_in_sample_attribute_key_none(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) sample = {"test": "2"} new_key = "new-key" pollster.definitions.sample_gatherer. \ generate_new_attributes_in_sample(sample, None, new_key) pollster.definitions.sample_gatherer. \ generate_new_attributes_in_sample(sample, "", new_key) self.assertNotIn(new_key, sample) def test_generate_new_attributes_in_sample(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) sample = {"test": "2"} new_key = "new-key" pollster.definitions.sample_gatherer. \ generate_new_attributes_in_sample(sample, "test", new_key) self.assertIn(new_key, sample) self.assertEqual(sample["test"], sample[new_key]) def test_execute_request_get_samples_non_empty_keys(self): sample = {'user_id_attribute': "123456789", 'project_id_attribute': "dfghyt432345t", 'resource_id_attribute': "sdfghjt543"} def internal_execute_request_get_samples_mock( self, definitions, **kwargs): class Response: @property def text(self): return json.dumps([sample]) def json(self): return [sample] return Response(), "url" original_method = NonOpenStackApisSamplesGatherer. \ _internal_execute_request_get_samples try: NonOpenStackApisSamplesGatherer. \ _internal_execute_request_get_samples = \ internal_execute_request_get_samples_mock self.pollster_definition_all_fields[ 'user_id_attribute'] = 'user_id_attribute' self.pollster_definition_all_fields[ 'project_id_attribute'] = 'project_id_attribute' self.pollster_definition_all_fields[ 'resource_id_attribute'] = 'resource_id_attribute' pollster = DynamicPollster( self.pollster_definition_all_fields) params = {"d": "d"} response = pollster.definitions.sample_gatherer. \ execute_request_get_samples(**params) self.assertEqual(sample['user_id_attribute'], response[0]['user_id']) self.assertEqual(sample['project_id_attribute'], response[0]['project_id']) self.assertEqual(sample['resource_id_attribute'], response[0]['id']) finally: NonOpenStackApisSamplesGatherer. \ _internal_execute_request_get_samples = original_method def test_execute_request_get_samples_empty_keys(self): sample = {'user_id_attribute': "123456789", 'project_id_attribute': "dfghyt432345t", 'resource_id_attribute': "sdfghjt543"} def execute_request_get_samples_mock(self, **kwargs): samples = [sample] return samples DynamicPollster.execute_request_get_samples = \ execute_request_get_samples_mock self.pollster_definition_all_fields[ 'user_id_attribute'] = None self.pollster_definition_all_fields[ 'project_id_attribute'] = None self.pollster_definition_all_fields[ 'resource_id_attribute'] = None pollster = DynamicPollster( self.pollster_definition_all_fields) params = {"d": "d"} response = pollster.execute_request_get_samples(**params) self.assertNotIn('user_id', response[0]) self.assertNotIn('project_id', response[0]) self.assertNotIn('id', response[0]) def test_pollster_defintions_instantiation(self): def validate_definitions_instance(instance, isNonOpenstack, isMultiMetric, isSingleMetric): self.assertIs( isinstance(instance, NonOpenStackApisPollsterDefinition), isNonOpenstack) self.assertIs(isinstance(instance, MultiMetricPollsterDefinitions), isMultiMetric) self.assertIs( isinstance(instance, SingleMetricPollsterDefinitions), isSingleMetric) pollster = DynamicPollster( self.pollster_definition_all_fields_multi_metrics) validate_definitions_instance(pollster.definitions, True, True, False) pollster = DynamicPollster( self.pollster_definition_all_fields) validate_definitions_instance(pollster.definitions, True, False, True) pollster = DynamicPollster( self.pollster_definition_only_openstack_required_multi_metric) validate_definitions_instance(pollster.definitions, False, True, False) pollster = DynamicPollster( self.pollster_definition_only_openstack_required_single_metric) validate_definitions_instance(pollster.definitions, False, False, True) @mock.patch.object( PollsterSampleGatherer, 'execute_request_get_samples', fake_sample_multi_metric) def test_get_samples_multi_metric_pollster(self): pollster = DynamicPollster( self.pollster_definition_all_fields_multi_metrics) fake_manager = self.FakeManager() samples = pollster.get_samples( fake_manager, None, ["https://endpoint.server.name.com/"]) samples_list = list(samples) self.assertEqual(4, len(samples_list)) create_bucket_sample = [ s for s in samples_list if s.name == "test-pollster.create_bucket"][0] get_obj_sample = [ s for s in samples_list if s.name == "test-pollster.get_obj"][0] list_bucket_sample = [ s for s in samples_list if s.name == "test-pollster.list_bucket"][0] put_obj_sample = [ s for s in samples_list if s.name == "test-pollster.put_obj"][0] self.assertEqual(2, create_bucket_sample.volume) self.assertEqual(46, get_obj_sample.volume) self.assertEqual(8, list_bucket_sample.volume) self.assertEqual(46, put_obj_sample.volume) def test_get_request_linked_samples_url_no_next_sample(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) expected_url = self.pollster_definition_only_required_fields[ 'url_path'] kwargs = {'resource': "non-openstack-resource"} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url( kwargs, pollster.definitions.configurations) self.assertEqual(expected_url, url) def test_get_request_linked_samples_url_next_sample_url(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) base_url = self.pollster_definition_only_required_fields['url_path'] next_sample_path = "/next_page" expected_url = urlparse.urljoin(base_url, next_sample_path) kwargs = {'next_sample_url': expected_url} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url(kwargs, pollster.definitions) self.assertEqual(expected_url, url) def test_get_request_linked_samples_url_next_sample_only_url_path(self): pollster = DynamicPollster( self.pollster_definition_only_required_fields) base_url = self.pollster_definition_only_required_fields['url_path'] next_sample_path = "/next_page" expected_url = urlparse.urljoin(base_url, next_sample_path) kwargs = {'next_sample_url': next_sample_path} url = pollster.definitions.sample_gatherer\ .get_request_linked_samples_url( kwargs, pollster.definitions.configurations) self.assertEqual(expected_url, url) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2741275 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/0000775000175000017500000000000015250267567021603 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2223568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/__init__.py0000664000175000017500000000000015250267452023673 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2233567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_file.py0000664000175000017500000001306015250267452024124 0ustar00zuulzuul# # Copyright 2013-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/file.py """ import json import logging.handlers import os import tempfile from oslo_utils import netutils from oslo_utils import timeutils from ceilometer.publisher import file from ceilometer import sample from ceilometer import service from ceilometer.tests import base class TestFilePublisher(base.BaseTestCase): test_data = [ sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), ] def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def test_file_publisher_maxbytes(self): # Test valid configurations tempdir = tempfile.mkdtemp() name = '%s/log_file' % tempdir parsed_url = netutils.urlsplit('file://%s?max_bytes=50&backup_count=3' % name) publisher = file.FilePublisher(self.CONF, parsed_url) publisher.publish_samples(self.test_data) handler = publisher.publisher_logger.handlers[0] self.assertIsInstance(handler, logging.handlers.RotatingFileHandler) self.assertEqual([50, name, 3], [handler.maxBytes, handler.baseFilename, handler.backupCount]) # The rotating file gets created since only allow 50 bytes. self.assertTrue(os.path.exists('%s.1' % name)) def test_file_publisher(self): # Test missing max bytes, backup count configurations tempdir = tempfile.mkdtemp() name = '%s/log_file_plain' % tempdir parsed_url = netutils.urlsplit('file://%s' % name) publisher = file.FilePublisher(self.CONF, parsed_url) publisher.publish_samples(self.test_data) handler = publisher.publisher_logger.handlers[0] self.assertIsInstance(handler, logging.handlers.RotatingFileHandler) self.assertEqual([0, name, 0], [handler.maxBytes, handler.baseFilename, handler.backupCount]) # Test the content is corrected saved in the file self.assertTrue(os.path.exists(name)) with open(name) as f: content = f.read() for sample_item in self.test_data: self.assertIn(sample_item.id, content) self.assertIn(sample_item.timestamp, content) def test_file_publisher_invalid(self): # Test invalid max bytes, backup count configurations tempdir = tempfile.mkdtemp() parsed_url = netutils.urlsplit( 'file://%s/log_file_bad' '?max_bytes=yus&backup_count=5y' % tempdir) publisher = file.FilePublisher(self.CONF, parsed_url) publisher.publish_samples(self.test_data) self.assertIsNone(publisher.publisher_logger) def test_file_publisher_json(self): tempdir = tempfile.mkdtemp() name = '%s/log_file_json' % tempdir parsed_url = netutils.urlsplit('file://%s?json' % name) publisher = file.FilePublisher(self.CONF, parsed_url) publisher.publish_samples(self.test_data) handler = publisher.publisher_logger.handlers[0] self.assertIsInstance(handler, logging.handlers.RotatingFileHandler) self.assertEqual([0, name, 0], [handler.maxBytes, handler.baseFilename, handler.backupCount]) self.assertTrue(os.path.exists(name)) with open(name) as f: content = f.readlines() self.assertEqual(len(self.test_data), len(content)) for index, line in enumerate(content): try: json_data = json.loads(line) except ValueError: self.fail("File written is not valid json") self.assertEqual(self.test_data[index].id, json_data['id']) self.assertEqual(self.test_data[index].timestamp, json_data['timestamp']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2233567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_gnocchi.py0000664000175000017500000011150315250267452024620 0ustar00zuulzuul# # Copyright 2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os from unittest import mock import uuid import fixtures from gnocchiclient import exceptions as gnocchi_exc from keystoneauth1 import exceptions as ka_exceptions from oslo_config import fixture as config_fixture from oslo_utils import fileutils from oslo_utils import fixture as utils_fixture from oslo_utils import netutils from oslo_utils import timeutils import requests from stevedore import extension import testscenarios from ceilometer.event import models from ceilometer.publisher import gnocchi from ceilometer import sample from ceilometer import service as ceilometer_service from ceilometer.tests import base from ceilometer.tests.unit import fakes load_tests = testscenarios.load_tests_apply_scenarios INSTANCE_DELETE_START = models.Event( event_type='compute.instance.delete.start', traits=[models.Trait('state', 1, 'active'), models.Trait( 'user_id', 1, '1e3ce043029547f1a61c1996d1a531a2'), models.Trait('service', 1, 'compute'), models.Trait('availability_zone', 1, 'zone1'), models.Trait('disk_gb', 2, 0), models.Trait('instance_type', 1, 'm1.tiny'), models.Trait('tenant_id', 1, '7c150a59fe714e6f9263774af9688f0e'), models.Trait('root_gb', 2, 0), models.Trait('ephemeral_gb', 2, 0), models.Trait('instance_type_id', 2, '2'), models.Trait('instance_flavor_id', 1, '2'), models.Trait('vcpus', 2, 1), models.Trait('memory_mb', 2, 512), models.Trait( 'instance_id', 1, '9f9d01b9-4a58-4271-9e27-398b21ab20d1'), models.Trait('host', 1, 'vagrant-precise'), models.Trait( 'request_id', 1, 'req-fb3c4546-a2e5-49b7-9fd2-a63bd658bc39'), models.Trait('project_id', 1, '7c150a59fe714e6f9263774af9688f0e'), models.Trait('launched_at', 4, '2012-05-08T20:23:47')], raw={}, generated='2012-05-08T20:24:14.824743', message_id='a15b94ee-cb8e-4c71-9abe-14aa80055fb4', ) INSTANCE_CREATE_END = models.Event( event_type='compute.instance.create.end', traits=[models.Trait('state', 1, 'active'), models.Trait( 'user_id', 1, '1e3ce043029547f1a61c1996d1a531a2'), models.Trait('service', 1, 'compute'), models.Trait('availability_zone', 1, 'zone1'), models.Trait('disk_gb', 2, 0), models.Trait('instance_type', 1, 'm1.tiny'), models.Trait('tenant_id', 1, '7c150a59fe714e6f9263774af9688f0e'), models.Trait('root_gb', 2, 0), models.Trait('ephemeral_gb', 2, 0), models.Trait('instance_type_id', 2, '2'), models.Trait('instance_flavor_id', 1, '2'), models.Trait('vcpus', 2, 1), models.Trait('memory_mb', 2, 512), models.Trait( 'instance_id', 1, '9f9d01b9-4a58-4271-9e27-398b21ab20d1'), models.Trait('host', 1, 'vagrant-precise'), models.Trait( 'request_id', 1, 'req-fb3c4546-a2e5-49b7-9fd2-a63bd658bc39'), models.Trait('project_id', 1, '7c150a59fe714e6f9263774af9688f0e'), models.Trait('launched_at', 4, '2012-05-08T20:23:47')], raw={}, generated='2012-05-08T20:24:14.824743', message_id='202f745e-4913-11e9-affe-9797342bd3a8', ) IMAGE_DELETE_START = models.Event( event_type='image.delete', traits=[models.Trait('status', 1, 'deleted'), models.Trait('deleted_at', 1, '2016-11-04T04:25:56Z'), models.Trait('user_id', 1, 'e97ef33a20ed4843b520d223f3cc33d4'), models.Trait('name', 1, 'cirros'), models.Trait('service', 1, 'image.localhost'), models.Trait( 'resource_id', 1, 'dc337359-de70-4044-8e2c-80573ba6e577'), models.Trait('created_at', 1, '2016-11-04T04:24:36Z'), models.Trait( 'project_id', 1, 'e97ef33a20ed4843b520d223f3cc33d4'), models.Trait('size', 1, '13287936')], raw={}, generated='2016-11-04T04:25:56.493820', message_id='7f5280f7-1d10-46a5-ba58-4d5508e49f99' ) VOLUME_DELETE_END = models.Event( event_type='volume.delete.end', traits=[models.Trait('availability_zone', 1, 'nova'), models.Trait('created_at', 1, '2016-11-28T13:19:53+00:00'), models.Trait('display_name', 1, 'vol-001'), models.Trait( 'host', 1, 'zhangguoqing-dev@lvmdriver-1#lvmdriver-1'), models.Trait( 'project_id', 1, 'd53fcc7dc53c4662ad77822c36a21f00'), models.Trait('replication_status', 1, 'disabled'), models.Trait( 'request_id', 1, 'req-f44df096-50d4-4211-95ea-64be6f5e4f60'), models.Trait( 'resource_id', 1, '6cc6e7dd-d17d-460f-ae79-7e08a216ce96'), models.Trait( 'service', 1, 'volume.zhangguoqing-dev@lvmdriver-1'), models.Trait('size', 1, '1'), models.Trait('status', 1, 'deleting'), models.Trait('tenant_id', 1, 'd53fcc7dc53c4662ad77822c36a21f00'), models.Trait('type', 1, 'af6271fa-13c4-44e6-9246-754ce9dc7df8'), models.Trait('user_id', 1, '819bbd28f5374506b8502521c89430b5')], raw={}, generated='2016-11-28T13:42:15.484674', message_id='a15b94ee-cb8e-4c71-9abe-14aa80055fb4', ) FLOATINGIP_DELETE_END = models.Event( event_type='floatingip.delete.end', traits=[models.Trait('service', 1, 'network.zhangguoqing-dev'), models.Trait( 'project_id', 1, 'd53fcc7dc53c4662ad77822c36a21f00'), models.Trait( 'request_id', 1, 'req-443ddb77-31f7-41fe-abbf-921107dd9f00'), models.Trait( 'resource_id', 1, '705e2c08-08e8-45cb-8673-5c5be955569b'), models.Trait('tenant_id', 1, 'd53fcc7dc53c4662ad77822c36a21f00'), models.Trait('user_id', 1, '819bbd28f5374506b8502521c89430b5')], raw={}, generated='2016-11-29T09:25:55.474710', message_id='a15b94ee-cb8e-4c71-9abe-14aa80055fb4' ) VOLUME_TRANSFER_ACCEPT_END = models.Event( event_type='volume.transfer.accept.end', traits=[models.Trait('tenant_id', 1, '945e7d09220e4308abe4b3b734bf5fce>'), models.Trait('project_id', 1, '85bc015f7a2342348593077a927c4aaa'), models.Trait('user_id', 1, '945e7d09220e4308abe4b3b734bf5fce'), models.Trait('service', 1, 'volume.controller-0'), models.Trait( 'request_id', 1, 'req-71dd1ae4-81ca-431a-b9fd-ac833eba889f'), models.Trait( 'resource_id', 1, '156b8d3f-ad99-429b-b84c-3f263fb2a801'), models.Trait( 'display_name', 1, 'test-vol'), models.Trait( 'type', 1, 'req-71dd1ae4-81ca-431a-b9fd-ac833eba889f'), models.Trait('host', 1, 'hostgroup@tripleo_iscsi#tripleo_iscsi'), models.Trait('created_at', 4, '2020-08-28 12:51:52'), models.Trait('size', 2, 1)], raw={}, generated='2020-08-28T12:52:22.930413', message_id='9fc4ceee-d980-4098-a685-2ad660838ac1' ) SNAPSHOT_TRANSFER_ACCEPT_END = models.Event( event_type='snapshot.transfer.accept.end', traits=[models.Trait('tenant_id', 1, '945e7d09220e4308abe4b3b734bf5fce>'), models.Trait('project_id', 1, '85bc015f7a2342348593077a927c4aaa'), models.Trait('user_id', 1, '945e7d09220e4308abe4b3b734bf5fce'), models.Trait('service', 1, 'volume.controller-0'), models.Trait( 'request_id', 1, 'req-71dd1ae4-81ca-431a-b9fd-ac833eba889f'), models.Trait( 'resource_id', 1, '156b8d3f-ad99-429b-b84c-3f263fb2a801'), models.Trait( 'display_name', 1, 'test-vol'), models.Trait( 'type', 1, 'req-71dd1ae4-81ca-431a-b9fd-ac833eba889f'), models.Trait('host', 1, 'hostgroup@tripleo_iscsi#tripleo_iscsi'), models.Trait('created_at', 4, '2020-08-28 12:51:52'), models.Trait('size', 2, 1)], raw={}, generated='2020-08-28T12:52:22.930413', message_id='9fc4ceee-d980-4098-a685-2ad660838ac1' ) class PublisherTest(base.BaseTestCase): def setUp(self): super().setUp() conf = ceilometer_service.prepare_service(argv=[], config_files=[]) self.conf = self.useFixture(config_fixture.Config(conf)) self.resource_id = str(uuid.uuid4()) self.samples = [sample.Sample( name='disk.root.size', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2012-05-08 20:23:48.028195', resource_id=self.resource_id, resource_metadata={ 'host': 'foo', 'image_ref': 'imageref!', 'instance_flavor_id': 1234, 'display_name': 'myinstance', } ), sample.Sample( name='disk.root.size', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2014-05-08 20:23:48.028195', resource_id=self.resource_id, resource_metadata={ 'host': 'foo', 'image_ref': 'imageref!', 'instance_flavor_id': 1234, 'display_name': 'myinstance', }, ), ] self.useFixture(fixtures.MockPatch( 'gnocchiclient.v1.client.Client', return_value=mock.Mock())) self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_session', return_value=mock.Mock())) def test_config_load(self): url = netutils.urlsplit("gnocchi://") d = gnocchi.GnocchiPublisher(self.conf.conf, url) names = [rd.cfg['resource_type'] for rd in d.resources_definition] self.assertIn('instance', names) self.assertIn('volume', names) def test_match(self): resource = { 'metrics': ['image', 'image.size', 'image.download', 'image.serve'], 'attributes': {'container_format': 'resource_metadata.container_format', 'disk_format': 'resource_metadata.disk_format', 'name': 'resource_metadata.name'}, 'event_delete': 'image.delete', 'event_attributes': {'id': 'resource_id'}, 'resource_type': 'image'} plugin_manager = extension.ExtensionManager( namespace='ceilometer.event.trait.trait_plugin') rd = gnocchi.ResourcesDefinition( resource, "high", "low", plugin_manager) operation = rd.event_match("image.delete") self.assertEqual('delete', operation) def test_metric_match(self): pub = gnocchi.GnocchiPublisher(self.conf.conf, netutils.urlsplit("gnocchi://")) self.assertIn('image.size', pub.metric_map['image.size'].metrics) @mock.patch('ceilometer.publisher.gnocchi.LOG') def test_broken_config_load(self, mylog): contents = [("---\n" "resources:\n" " - resource_type: foobar\n"), ("---\n" "resources:\n" " - resource_type: 0\n"), ("---\n" "resources:\n" " - sample_types: ['foo', 'bar']\n"), ("---\n" "resources:\n" " - sample_types: foobar\n" " - resource_type: foobar\n"), ] for content in contents: content = content.encode('utf-8') temp = fileutils.write_to_tempfile(content=content, prefix='gnocchi_resources', suffix='.yaml') self.addCleanup(os.remove, temp) url = netutils.urlsplit( "gnocchi://?resources_definition_file=" + temp) d = gnocchi.GnocchiPublisher(self.conf.conf, url) self.assertTrue(mylog.exception.called) self.assertEqual(0, len(d.resources_definition)) @mock.patch('ceilometer.publisher.gnocchi.GnocchiPublisher' '._if_not_cached', mock.Mock()) @mock.patch('ceilometer.publisher.gnocchi.GnocchiPublisher' '.batch_measures') def _do_test_activity_filter(self, expected_measures, fake_batch, params=None): url_str = "gnocchi://" if params: url_str += "?" + "&".join(f"{k}={v}" for k, v in params.items()) url = netutils.urlsplit(url_str) d = gnocchi.GnocchiPublisher(self.conf.conf, url) d._already_checked_archive_policies = True d.publish_samples(self.samples) self.assertEqual(1, len(fake_batch.mock_calls)) measures = fake_batch.mock_calls[0][1][0] self.assertEqual( expected_measures, sum(len(m["measures"]) for rid in measures for m in measures[rid].values())) def test_activity_filter_match_project_id(self): self.samples[0].project_id = ( 'a2d42c23-d518-46b6-96ab-3fba2e146859') self._do_test_activity_filter(1) @mock.patch('ceilometer.publisher.gnocchi.LOG') def test_activity_gnocchi_project_not_found(self, logger): self.setup_connection(projects=[]) self._do_test_activity_filter(2) log_called_args = logger.warning.call_args_list self.assertEqual( 'Filtered project [service] not found in keystone, ignoring the ' 'filter_project option', log_called_args[0][0][0] % log_called_args[0][0][1]) @mock.patch('ceilometer.publisher.gnocchi.LOG') def test_activity_gnocchi_project_filter_disabled(self, logger): self.samples[0].project_id = ( 'a2d42c23-d518-46b6-96ab-3fba2e146859') self._do_test_activity_filter( 2, params={"enable_filter_project": "false"}) self.assertIn( mock.call("Filtering Gnocchi project is disabled, " "ignoring filter_project option"), logger.debug.call_args_list, ) @mock.patch('ceilometer.publisher.gnocchi.GnocchiPublisher' '._get_gnocchi_client') def test_get_gnocchi_client(self, gnocchi_cli): url = netutils.urlsplit("gnocchi://") gnocchi_cli.side_effect = ka_exceptions.DiscoveryFailure cfg = self.conf.conf publisher = gnocchi.GnocchiPublisher self.assertRaises(ka_exceptions.DiscoveryFailure, publisher, cfg, url) def test_activity_filter_match_swift_event(self): self.samples[0].name = 'storage.objects.outgoing.bytes' self.samples[0].resource_id = 'a2d42c23-d518-46b6-96ab-3fba2e146859' self._do_test_activity_filter(1) def test_activity_filter_nomatch(self): self._do_test_activity_filter(2) @mock.patch('ceilometer.publisher.gnocchi.GnocchiPublisher' '.batch_measures') def test_unhandled_meter(self, fake_batch): samples = [sample.Sample( name='unknown.meter', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2014-05-08 20:23:48.028195', resource_id='randomid', resource_metadata={} )] url = netutils.urlsplit("gnocchi://") d = gnocchi.GnocchiPublisher(self.conf.conf, url) d._already_checked_archive_policies = True d.publish_samples(samples) self.assertEqual(0, len(fake_batch.call_args[0][1])) @mock.patch('ceilometer.publisher.gnocchi.GnocchiPublisher' '.batch_measures') def test_unhandled_meter_with_no_resource_id(self, fake_batch): samples = [ sample.Sample( name='unknown.meter', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2014-05-08 20:23:48.028195', resource_id=None, resource_metadata={}), sample.Sample( name='unknown.meter', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2014-05-08 20:23:48.028195', resource_id="Some-other-resource-id", resource_metadata={}) ] url = netutils.urlsplit("gnocchi://") d = gnocchi.GnocchiPublisher(self.conf.conf, url) d._already_checked_archive_policies = True d.publish_samples(samples) self.assertEqual(0, len(fake_batch.call_args[0][1])) @mock.patch('ceilometer.publisher.gnocchi.LOG') @mock.patch('gnocchiclient.v1.client.Client') def test__set_update_attributes_non_existent_resource(self, fakeclient_cls, logger): url = netutils.urlsplit("gnocchi://") self.publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value fakeclient.resource.update.side_effect = [ gnocchi_exc.ResourceNotFound(404)] non_existent_resource = { 'type': 'volume', 'id': self.resource_id, } self.publisher._set_update_attributes(non_existent_resource) logger.debug.assert_called_with( "Update event received on unexisting resource (%s), ignore it.", self.resource_id) def test_stable_resource_attributes_hash(self): url = netutils.urlsplit("gnocchi://") publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) attributes = {'hello': 'world', 'foo': 'bar'} hash = publisher._hash_resource(attributes) self.assertEqual(hash, publisher._hash_resource(attributes)) class MockResponse(mock.NonCallableMock): def __init__(self, code): text = {500: 'Internal Server Error', 404: 'Not Found', 204: 'Created', 409: 'Conflict', }.get(code) super().__init__(spec=requests.Response, status_code=code, text=text) class PublisherWorkflowTest(base.BaseTestCase, testscenarios.TestWithScenarios): sample_scenarios = [ ('cpu', dict( sample=sample.Sample( resource_id=str(uuid.uuid4()) + "_foobar", name='cpu', unit='ns', type=sample.TYPE_CUMULATIVE, volume=500, user_id='test_user', project_id='test_project', source='openstack', timestamp='2012-05-08 20:23:48.028195', resource_metadata={ 'host': 'foo', 'image_ref': 'imageref!', 'instance_flavor_id': 1234, 'display_name': 'myinstance', }, ), metric_attributes={ "archive_policy_name": "ceilometer-low-rate", "unit": "ns", "measures": [{ 'timestamp': '2012-05-08 20:23:48.028195', 'value': 500 }] }, postable_attributes={ 'user_id': 'test_user', 'project_id': 'test_project', }, patchable_attributes={ 'host': 'foo', 'image_ref': 'imageref!', 'flavor_id': 1234, 'display_name': 'myinstance', }, resource_type='instance')), ('disk.root.size', dict( sample=sample.Sample( resource_id=str(uuid.uuid4()) + "_foobar", name='disk.root.size', unit='GiB', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2012-05-08 20:23:48.028195', resource_metadata={ 'host': 'foo', 'image_ref': 'imageref!', 'instance_flavor_id': 1234, 'display_name': 'myinstance', }, ), metric_attributes={ "archive_policy_name": "ceilometer-low", "unit": "GiB", "measures": [{ 'timestamp': '2012-05-08 20:23:48.028195', 'value': 2 }] }, postable_attributes={ 'user_id': 'test_user', 'project_id': 'test_project', }, patchable_attributes={ 'host': 'foo', 'image_ref': 'imageref!', 'flavor_id': 1234, 'display_name': 'myinstance', }, resource_type='instance')), ('hardware.ipmi.node.power', dict( sample=sample.Sample( resource_id=str(uuid.uuid4()) + "_foobar", name='hardware.ipmi.node.power', unit='W', type=sample.TYPE_GAUGE, volume=2, user_id='test_user', project_id='test_project', source='openstack', timestamp='2012-05-08 20:23:48.028195', resource_metadata={ 'useless': 'not_used', }, ), metric_attributes={ "archive_policy_name": "ceilometer-low", "unit": "W", "measures": [{ 'timestamp': '2012-05-08 20:23:48.028195', 'value': 2 }] }, postable_attributes={ 'user_id': 'test_user', 'project_id': 'test_project', }, patchable_attributes={ }, resource_type='ipmi')), ] default_workflow = dict(resource_exists=True, post_measure_fail=False, create_resource_fail=False, create_resource_race=False, update_resource_fail=False, retry_post_measures_fail=False) workflow_scenarios = [ ('normal_workflow', {}), ('new_resource', dict(resource_exists=False)), ('new_resource_compat', dict(resource_exists=False)), ('new_resource_fail', dict(resource_exists=False, create_resource_fail=True)), ('new_resource_race', dict(resource_exists=False, create_resource_race=True)), ('resource_update_fail', dict(update_resource_fail=True)), ('retry_fail', dict(resource_exists=False, retry_post_measures_fail=True)), ('measure_fail', dict(post_measure_fail=True)), ] @classmethod def generate_scenarios(cls): workflow_scenarios = [] for name, wf_change in cls.workflow_scenarios: wf = cls.default_workflow.copy() wf.update(wf_change) workflow_scenarios.append((name, wf)) cls.scenarios = testscenarios.multiply_scenarios(cls.sample_scenarios, workflow_scenarios) def setUp(self): super().setUp() conf = ceilometer_service.prepare_service(argv=[], config_files=[]) self.conf = self.useFixture(config_fixture.Config(conf)) self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_session', return_value=mock.Mock())) self.setup_connection(projects=[fakes.PROJECT_SERVICE_sdk]) @mock.patch('gnocchiclient.v1.client.Client') def test_delete_event_workflow(self, fakeclient_cls): url = netutils.urlsplit("gnocchi://") self.publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value fakeclient.resource.search.side_effect = [ [{"id": "b26268d6-8bb5-11e6-baff-00224d8226cd", "type": "instance_disk", "instance_id": "9f9d01b9-4a58-4271-9e27-398b21ab20d1"}], [{"id": "b1c7544a-8bb5-11e6-850e-00224d8226cd", "type": "instance_network_interface", "instance_id": "9f9d01b9-4a58-4271-9e27-398b21ab20d1"}], ] search_params = { '=': {'instance_id': '9f9d01b9-4a58-4271-9e27-398b21ab20d1'} } now = timeutils.utcnow() self.useFixture(utils_fixture.TimeFixture(now)) expected_calls = [ mock.call.resource.search('instance_network_interface', search_params), mock.call.resource.search('instance_disk', search_params), mock.call.resource.update( 'instance', '9f9d01b9-4a58-4271-9e27-398b21ab20d1', {'ended_at': now.isoformat()}), mock.call.resource.update( 'instance_disk', 'b26268d6-8bb5-11e6-baff-00224d8226cd', {'ended_at': now.isoformat()}), mock.call.resource.update( 'instance_network_interface', 'b1c7544a-8bb5-11e6-850e-00224d8226cd', {'ended_at': now.isoformat()}), mock.call.resource.update( 'image', 'dc337359-de70-4044-8e2c-80573ba6e577', {'ended_at': now.isoformat()}), mock.call.resource.update( 'volume', '6cc6e7dd-d17d-460f-ae79-7e08a216ce96', {'ended_at': now.isoformat()}), mock.call.resource.update( 'network', '705e2c08-08e8-45cb-8673-5c5be955569b', {'ended_at': now.isoformat()}) ] self.publisher.publish_events([INSTANCE_DELETE_START, IMAGE_DELETE_START, VOLUME_DELETE_END, FLOATINGIP_DELETE_END]) self.assertEqual(8, len(fakeclient.mock_calls)) for call in expected_calls: self.assertIn(call, fakeclient.mock_calls) @mock.patch('gnocchiclient.v1.client.Client') def test_create_event_workflow(self, fakeclient_cls): url = netutils.urlsplit("gnocchi://") self.publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value now = timeutils.utcnow() self.useFixture(utils_fixture.TimeFixture(now)) expected_calls = [ mock.call.resource.create( 'instance', {'id': '9f9d01b9-4a58-4271-9e27-398b21ab20d1', 'user_id': '1e3ce043029547f1a61c1996d1a531a2', 'project_id': '7c150a59fe714e6f9263774af9688f0e', 'availability_zone': 'zone1', 'flavor_name': 'm1.tiny', 'flavor_id': '2', 'host': 'vagrant-precise'}), ] self.publisher.publish_events([INSTANCE_CREATE_END]) self.assertEqual(1, len(fakeclient.mock_calls)) for call in expected_calls: self.assertIn(call, fakeclient.mock_calls) @mock.patch('gnocchiclient.v1.client.Client') def test_update_event_workflow(self, fakeclient_cls): url = netutils.urlsplit("gnocchi://") self.publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value now = timeutils.utcnow() self.useFixture(utils_fixture.TimeFixture(now)) expected_calls = [ mock.call.resource.update( 'volume', '156b8d3f-ad99-429b-b84c-3f263fb2a801', {'project_id': '85bc015f7a2342348593077a927c4aaa'}), ] self.publisher.publish_events([VOLUME_TRANSFER_ACCEPT_END]) self.assertEqual(1, len(fakeclient.mock_calls)) for call in expected_calls: self.assertIn(call, fakeclient.mock_calls) @mock.patch('gnocchiclient.v1.client.Client') def test_update_snapshot_event_workflow(self, fakeclient_cls): url = netutils.urlsplit("gnocchi://") self.publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value now = timeutils.utcnow() self.useFixture(utils_fixture.TimeFixture(now)) expected_calls = [ mock.call.resource.update( 'volume', '156b8d3f-ad99-429b-b84c-3f263fb2a801', {'project_id': '85bc015f7a2342348593077a927c4aaa'}), ] self.publisher.publish_events([SNAPSHOT_TRANSFER_ACCEPT_END]) self.assertEqual(1, len(fakeclient.mock_calls)) for call in expected_calls: self.assertIn(call, fakeclient.mock_calls) @mock.patch('ceilometer.cache_utils.get_client', mock.Mock()) @mock.patch('ceilometer.publisher.gnocchi.LOG') @mock.patch('gnocchiclient.v1.client.Client') def test_workflow(self, fakeclient_cls, logger): url = netutils.urlsplit("gnocchi://") publisher = gnocchi.GnocchiPublisher(self.conf.conf, url) fakeclient = fakeclient_cls.return_value resource_id = self.sample.resource_id.replace("/", "_") metric_name = self.sample.name gnocchi_id = uuid.uuid4() expected_calls = [ mock.call.archive_policy.create({"name": "ceilometer-low", "back_window": 0, "aggregation_methods": ["mean"], "definition": mock.ANY}), mock.call.archive_policy.create({"name": "ceilometer-low-rate", "back_window": 0, "aggregation_methods": [ "mean", "rate:mean"], "definition": mock.ANY}), mock.call.archive_policy.create({"name": "ceilometer-high", "back_window": 0, "aggregation_methods": ["mean"], "definition": mock.ANY}), mock.call.archive_policy.create({"name": "ceilometer-high-rate", "back_window": 0, "aggregation_methods": [ "mean", "rate:mean"], "definition": mock.ANY}), mock.call.metric.batch_resources_metrics_measures( {resource_id: {metric_name: self.metric_attributes}}, create_metrics=True) ] resource_definition = publisher.metric_map.get(self.sample.name) expected_measures_in_log = {resource_id: {self.sample.name: { 'measures': [{'timestamp': self.sample.timestamp, 'value': self.sample.volume}], 'archive_policy_name': resource_definition.metrics[ metric_name]["archive_policy_name"], 'unit': self.sample.unit}}} resource_type = resource_definition.cfg['resource_type'] expected_debug = [ mock.call('Filtered project [%s] found with ID [%s].', 'service', 'a2d42c23-d518-46b6-96ab-3fba2e146859'), mock.call('Sample [%s] is not a Gnocchi activity; therefore, we ' 'do not filter it out and push it to Gnocchi.', self.sample), mock.call('Processing sample [%s] for resource ID [%s].', self.sample, resource_id), mock.call('Executing batch resource metrics measures for resource ' '[%s] and measures [%s].', mock.ANY, expected_measures_in_log)] measures_posted = False batch_side_effect = [] if self.post_measure_fail: batch_side_effect += [Exception('boom!')] elif not self.resource_exists: batch_side_effect += [ gnocchi_exc.BadRequest( 400, {"cause": "Unknown resources", 'detail': [{ 'resource_id': gnocchi_id, 'original_resource_id': resource_id}]})] attributes = self.postable_attributes.copy() attributes.update(self.patchable_attributes) attributes['id'] = self.sample.resource_id expected_calls.append(mock.call.resource.create( self.resource_type, attributes)) if self.create_resource_fail: fakeclient.resource.create.side_effect = [Exception('boom!')] elif self.create_resource_race: fakeclient.resource.create.side_effect = [ gnocchi_exc.ResourceAlreadyExists(409)] else: # not resource_exists expected_debug.append(mock.call( 'Resource %s created', self.sample.resource_id)) if not self.create_resource_fail: expected_calls.append( mock.call.metric.batch_resources_metrics_measures( {resource_id: {metric_name: self.metric_attributes}}, create_metrics=True) ) if self.retry_post_measures_fail: batch_side_effect += [Exception('boom!')] else: measures_posted = True else: measures_posted = True if measures_posted: batch_side_effect += [None] expected_debug.append( mock.call("%d measures posted against %d metrics through %d " "resources", len(self.metric_attributes["measures"]), 1, 1) ) update_attributes = {**self.postable_attributes, **self.patchable_attributes} del update_attributes["user_id"] # Only user_id is not updated. if self.patchable_attributes != update_attributes: expected_calls.append(mock.call.resource.update( self.resource_type, resource_id, update_attributes)) if self.update_resource_fail: fakeclient.resource.update.side_effect = [Exception('boom!')] else: expected_debug.append(mock.call( 'Resource %s updated', self.sample.resource_id)) batch = fakeclient.metric.batch_resources_metrics_measures batch.side_effect = batch_side_effect publisher.publish_samples([self.sample]) # Check that the last log message is the expected one if (self.post_measure_fail or self.create_resource_fail or self.retry_post_measures_fail or (self.update_resource_fail and update_attributes)): if self.update_resource_fail and update_attributes: logger.exception.assert_called_with( 'Unexpected exception updating resource type [%s] with ' 'ID [%s] for resource data [%s]: [%s].', resource_type, resource_id, mock.ANY, 'boom!') else: logger.exception.assert_called_with( 'Unexpected exception while pushing measures [%s] for ' 'gnocchi data [%s]: [%s].', expected_measures_in_log, mock.ANY, 'boom!') else: self.assertEqual(0, logger.error.call_count) self.assertEqual(expected_calls, fakeclient.mock_calls) self.assertEqual(expected_debug, logger.debug.mock_calls) PublisherWorkflowTest.generate_scenarios() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2233567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_http.py0000664000175000017500000002435315250267452024173 0ustar00zuulzuul# # Copyright 2016 IBM # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/http.py""" import datetime from unittest import mock import uuid from oslo_utils import timeutils import requests from urllib import parse as urlparse from ceilometer.event import models as event from ceilometer.publisher import http from ceilometer import sample from ceilometer import service from ceilometer.tests import base class TestHttpPublisher(base.BaseTestCase): resource_id = str(uuid.uuid4()) sample_data = [ sample.Sample( name='alpha', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='beta', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='gamma', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=datetime.datetime.now().isoformat(), resource_metadata={'name': 'TestPublish'}, ), ] event_data = [event.Event( message_id=str(uuid.uuid4()), event_type='event_%d' % i, generated=timeutils.utcnow().isoformat(), traits=[], raw={'payload': {'some': 'aa'}}) for i in range(3)] def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def test_http_publisher_config(self): """Test publisher config parameters.""" # invalid hostname, the given url, results in an empty hostname parsed_url = urlparse.urlparse('http:/aaa.bb/path') self.assertRaises(ValueError, http.HttpPublisher, self.CONF, parsed_url) # invalid port parsed_url = urlparse.urlparse('http://aaa:bb/path') self.assertRaises(ValueError, http.HttpPublisher, self.CONF, parsed_url) parsed_url = urlparse.urlparse('http://localhost:90/path1') publisher = http.HttpPublisher(self.CONF, parsed_url) # By default, timeout and retry_count should be set to 5 and 2 # respectively self.assertEqual(5, publisher.timeout) self.assertEqual(2, publisher.max_retries) parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'timeout=19&max_retries=4') publisher = http.HttpPublisher(self.CONF, parsed_url) self.assertEqual(19, publisher.timeout) self.assertEqual(4, publisher.max_retries) parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'timeout=19') publisher = http.HttpPublisher(self.CONF, parsed_url) self.assertEqual(19, publisher.timeout) self.assertEqual(2, publisher.max_retries) parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'max_retries=6') publisher = http.HttpPublisher(self.CONF, parsed_url) self.assertEqual(5, publisher.timeout) self.assertEqual(6, publisher.max_retries) @mock.patch('ceilometer.publisher.http.LOG') def test_http_post_samples(self, thelog): """Test publisher post.""" parsed_url = urlparse.urlparse('http://localhost:90/path1') publisher = http.HttpPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) self.assertEqual(1, m_req.call_count) self.assertFalse(thelog.exception.called) res = requests.Response() res.status_code = 401 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) self.assertEqual(1, m_req.call_count) self.assertTrue(thelog.exception.called) @mock.patch('ceilometer.publisher.http.LOG') def test_http_post_events(self, thelog): """Test publisher post.""" parsed_url = urlparse.urlparse('http://localhost:90/path1') publisher = http.HttpPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_events(self.event_data) self.assertEqual(1, m_req.call_count) self.assertFalse(thelog.exception.called) res = requests.Response() res.status_code = 401 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_events(self.event_data) self.assertEqual(1, m_req.call_count) self.assertTrue(thelog.exception.called) @mock.patch('ceilometer.publisher.http.LOG') def test_http_post_empty_data(self, thelog): parsed_url = urlparse.urlparse('http://localhost:90/path1') publisher = http.HttpPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_events([]) self.assertEqual(0, m_req.call_count) self.assertTrue(thelog.debug.called) def _post_batch_control_test(self, method, data, batch): parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'batch=%s' % batch) publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: getattr(publisher, method)(data) self.assertEqual(1 if batch else 3, post.call_count) def test_post_batch_sample(self): self._post_batch_control_test('publish_samples', self.sample_data, 1) def test_post_no_batch_sample(self): self._post_batch_control_test('publish_samples', self.sample_data, 0) def test_post_batch_event(self): self._post_batch_control_test('publish_events', self.event_data, 1) def test_post_no_batch_event(self): self._post_batch_control_test('publish_events', self.event_data, 0) def test_post_verify_ssl_default(self): parsed_url = urlparse.urlparse('http://localhost:90/path1') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertTrue(post.call_args[1]['verify']) def test_post_verify_ssl_True(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'verify_ssl=True') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertTrue(post.call_args[1]['verify']) def test_post_verify_ssl_False(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'verify_ssl=False') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertFalse(post.call_args[1]['verify']) def test_post_verify_ssl_path(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'verify_ssl=/path/to/cert.crt') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertEqual('/path/to/cert.crt', post.call_args[1]['verify']) def test_post_basic_auth(self): parsed_url = urlparse.urlparse( 'http://alice:l00kingGla$$@localhost:90/path1?') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertEqual(('alice', 'l00kingGla$$'), post.call_args[1]['auth']) def test_post_client_cert_auth(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?' 'clientcert=/path/to/cert.crt&' 'clientkey=/path/to/cert.key') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_samples(self.sample_data) self.assertEqual(('/path/to/cert.crt', '/path/to/cert.key'), post.call_args[1]['cert']) def test_post_raw_only(self): parsed_url = urlparse.urlparse('http://localhost:90/path1?raw_only=1') publisher = http.HttpPublisher(self.CONF, parsed_url) with mock.patch.object(requests.Session, 'post') as post: publisher.publish_events(self.event_data) self.assertEqual( '[{"some": "aa"}, {"some": "aa"}, {"some": "aa"}]', post.call_args[1]['data']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2243567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_messaging_publisher.py0000664000175000017500000003467715250267452027260 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/messaging.py""" from unittest import mock import uuid import oslo_messaging from oslo_messaging._drivers import impl_kafka as kafka_driver from oslo_utils import netutils from oslo_utils import timeutils import testscenarios.testcase from ceilometer.event import models as event from ceilometer.publisher import messaging as msg_publisher from ceilometer import sample from ceilometer import service from ceilometer.tests import base as tests_base class BasePublisherTestCase(tests_base.BaseTestCase): test_event_data = [ event.Event(message_id=uuid.uuid4(), event_type='event_%d' % i, generated=timeutils.utcnow(), traits=[], raw={}) for i in range(0, 5) ] test_sample_data = [ sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='test3', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), ] def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.setup_messaging(self.CONF) class NotifierOnlyPublisherTest(BasePublisherTestCase): @mock.patch('oslo_messaging.Notifier') def test_publish_topic_override(self, notifier): msg_publisher.SampleNotifierPublisher( self.CONF, netutils.urlsplit('notifier://?topic=custom_topic')) notifier.assert_called_with(mock.ANY, topics=['custom_topic'], driver=mock.ANY, retry=mock.ANY, publisher_id=mock.ANY) msg_publisher.EventNotifierPublisher( self.CONF, netutils.urlsplit('notifier://?topic=custom_event_topic')) notifier.assert_called_with(mock.ANY, topics=['custom_event_topic'], driver=mock.ANY, retry=mock.ANY, publisher_id=mock.ANY) @mock.patch('ceilometer.messaging.get_transport') def test_publish_other_host(self, cgt): msg_publisher.SampleNotifierPublisher( self.CONF, netutils.urlsplit('notifier://foo:foo@127.0.0.1:1234')) cgt.assert_called_with(self.CONF, 'rabbit://foo:foo@127.0.0.1:1234') msg_publisher.EventNotifierPublisher( self.CONF, netutils.urlsplit('notifier://foo:foo@127.0.0.1:1234')) cgt.assert_called_with(self.CONF, 'rabbit://foo:foo@127.0.0.1:1234') @mock.patch('ceilometer.messaging.get_transport') def test_publish_other_host_vhost_and_query(self, cgt): msg_publisher.SampleNotifierPublisher( self.CONF, netutils.urlsplit('notifier://foo:foo@127.0.0.1:1234/foo' '?driver=amqp&amqp_auto_delete=true')) cgt.assert_called_with(self.CONF, 'amqp://foo:foo@127.0.0.1:1234/foo' '?amqp_auto_delete=true') msg_publisher.EventNotifierPublisher( self.CONF, netutils.urlsplit('notifier://foo:foo@127.0.0.1:1234/foo' '?driver=amqp&amqp_auto_delete=true')) cgt.assert_called_with(self.CONF, 'amqp://foo:foo@127.0.0.1:1234/foo' '?amqp_auto_delete=true') @mock.patch('ceilometer.messaging.get_transport') def test_publish_with_none_rabbit_driver(self, cgt): sample_publisher = msg_publisher.SampleNotifierPublisher( self.CONF, netutils.urlsplit('notifier://127.0.0.1:9092?driver=kafka')) cgt.assert_called_with(self.CONF, 'kafka://127.0.0.1:9092') transport = oslo_messaging.get_transport(self.CONF, 'kafka://127.0.0.1:9092') self.assertIsInstance(transport._driver, kafka_driver.KafkaDriver) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(sample_publisher, '_send') as fake_send: fake_send.side_effect = side_effect self.assertRaises( msg_publisher.DeliveryFailure, sample_publisher.publish_samples, self.test_sample_data) self.assertEqual(0, len(sample_publisher.local_queue)) self.assertEqual(100, len(fake_send.mock_calls)) fake_send.assert_called_with('metering', mock.ANY) event_publisher = msg_publisher.EventNotifierPublisher( self.CONF, netutils.urlsplit('notifier://127.0.0.1:9092?driver=kafka')) cgt.assert_called_with(self.CONF, 'kafka://127.0.0.1:9092') with mock.patch.object(event_publisher, '_send') as fake_send: fake_send.side_effect = side_effect self.assertRaises( msg_publisher.DeliveryFailure, event_publisher.publish_events, self.test_event_data) self.assertEqual(0, len(event_publisher.local_queue)) self.assertEqual(100, len(fake_send.mock_calls)) fake_send.assert_called_with('event', mock.ANY) class TestPublisher(testscenarios.testcase.WithScenarios, BasePublisherTestCase): scenarios = [ ('notifier', dict(protocol="notifier", publisher_cls=msg_publisher.SampleNotifierPublisher, test_data=BasePublisherTestCase.test_sample_data, pub_func='publish_samples', attr='source')), ('event_notifier', dict(protocol="notifier", publisher_cls=msg_publisher.EventNotifierPublisher, test_data=BasePublisherTestCase.test_event_data, pub_func='publish_events', attr='event_type')), ] def setUp(self): super().setUp() self.topic = (self.CONF.publisher_notifier.event_topic if self.pub_func == 'publish_events' else self.CONF.publisher_notifier.metering_topic) class TestPublisherPolicy(TestPublisher): @mock.patch('ceilometer.publisher.messaging.LOG') def test_published_with_no_policy(self, mylog): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect self.assertRaises( msg_publisher.DeliveryFailure, getattr(publisher, self.pub_func), self.test_data) self.assertTrue(mylog.info.called) self.assertEqual('default', publisher.policy) self.assertEqual(0, len(publisher.local_queue)) self.assertEqual(100, len(fake_send.mock_calls)) fake_send.assert_called_with( self.topic, mock.ANY) @mock.patch('ceilometer.publisher.messaging.LOG') def test_published_with_policy_block(self, mylog): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=default' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect self.assertRaises( msg_publisher.DeliveryFailure, getattr(publisher, self.pub_func), self.test_data) self.assertTrue(mylog.info.called) self.assertEqual(0, len(publisher.local_queue)) self.assertEqual(100, len(fake_send.mock_calls)) fake_send.assert_called_with( self.topic, mock.ANY) @mock.patch('ceilometer.publisher.messaging.LOG') def test_published_with_policy_incorrect(self, mylog): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=notexist' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect self.assertRaises( msg_publisher.DeliveryFailure, getattr(publisher, self.pub_func), self.test_data) self.assertTrue(mylog.warning.called) self.assertEqual('default', publisher.policy) self.assertEqual(0, len(publisher.local_queue)) self.assertEqual(100, len(fake_send.mock_calls)) fake_send.assert_called_with( self.topic, mock.ANY) @mock.patch('ceilometer.publisher.messaging.LOG', mock.Mock()) class TestPublisherPolicyReactions(TestPublisher): def test_published_with_policy_drop_and_rpc_down(self): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=drop' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(0, len(publisher.local_queue)) fake_send.assert_called_once_with( self.topic, mock.ANY) def test_published_with_policy_queue_and_rpc_down(self): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=queue' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(1, len(publisher.local_queue)) fake_send.assert_called_once_with( self.topic, mock.ANY) def test_published_with_policy_queue_and_rpc_down_up(self): self.rpc_unreachable = True publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=queue' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(1, len(publisher.local_queue)) fake_send.side_effect = mock.MagicMock() getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(0, len(publisher.local_queue)) topic = self.topic expected = [mock.call(topic, mock.ANY), mock.call(topic, mock.ANY), mock.call(topic, mock.ANY)] self.assertEqual(expected, fake_send.mock_calls) def test_published_with_policy_sized_queue_and_rpc_down(self): publisher = self.publisher_cls(self.CONF, netutils.urlsplit( '%s://?policy=queue&max_queue_length=3' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect for i in range(0, 5): for s in self.test_data: setattr(s, self.attr, 'test-%d' % i) getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(3, len(publisher.local_queue)) self.assertEqual( 'test-2', publisher.local_queue[0][1][0][self.attr] ) self.assertEqual( 'test-3', publisher.local_queue[1][1][0][self.attr] ) self.assertEqual( 'test-4', publisher.local_queue[2][1][0][self.attr] ) def test_published_with_policy_default_sized_queue_and_rpc_down(self): publisher = self.publisher_cls( self.CONF, netutils.urlsplit('%s://?policy=queue' % self.protocol)) side_effect = msg_publisher.DeliveryFailure() with mock.patch.object(publisher, '_send') as fake_send: fake_send.side_effect = side_effect for i in range(0, 2000): for s in self.test_data: setattr(s, self.attr, 'test-%d' % i) getattr(publisher, self.pub_func)(self.test_data) self.assertEqual(1024, len(publisher.local_queue)) self.assertEqual( 'test-976', publisher.local_queue[0][1][0][self.attr] ) self.assertEqual( 'test-1999', publisher.local_queue[1023][1][0][self.attr] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2243567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_opentelemetry_http.py0000664000175000017500000001475115250267452027150 0ustar00zuulzuul# # Copyright 2024 cmss, inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/opentelemetry.py""" import json import time from unittest import mock import uuid from oslo_utils import timeutils import requests from urllib import parse as urlparse from ceilometer.publisher import opentelemetry_http from ceilometer import sample from ceilometer import service from ceilometer.tests import base class TestOpentelemetryHttpPublisher(base.BaseTestCase): resource_id = str(uuid.uuid4()) format_time = timeutils.utcnow().isoformat() sample_data = [ sample.Sample( name='alpha', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=format_time, resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='beta', type=sample.TYPE_DELTA, unit='', volume=3, user_id='test', project_id='test', resource_id=resource_id, timestamp=format_time, resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='gamma', type=sample.TYPE_GAUGE, unit='', volume=5, user_id='test', project_id='test', resource_id=resource_id, timestamp=format_time, resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='delta.epsilon', type=sample.TYPE_GAUGE, unit='', volume=7, user_id='test', project_id='test', resource_id=resource_id, timestamp=format_time, resource_metadata={'name': 'TestPublish'}, ), ] @staticmethod def _make_fake_json(sample, format_time): struct_time = timeutils.parse_isotime(format_time).timetuple() unix_time = int(time.mktime(struct_time)) if sample.type == "cumulative": metric_type = "counter" else: metric_type = "gauge" return {"resource_metrics": [{ "scope_metrics": [{ "scope": { "name": "ceilometer", "version": "v1" }, "metrics": [{ "name": sample.name.replace(".", "_"), "description": sample.name + " unit:", "unit": "", metric_type: { "data_points": [{ "attributes": [{ "key": "resource_id", "value": { "string_value": sample.resource_id } }, { "key": "user_id", "value": { "string_value": "test" } }, { "key": "project_id", "value": { "string_value": "test" } }], "start_time_unix_nano": unix_time, "time_unix_nano": unix_time, "as_double": sample.volume, "flags": 0 }]}}]}]}]} def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def test_post_samples(self): """Test publisher post.""" parsed_url = urlparse.urlparse( 'opentelemetryhttp://localhost:4318/v1/metrics') publisher = opentelemetry_http.OpentelemetryHttpPublisher( self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) data = [] for s in self.sample_data: data.append(self._make_fake_json(s, self.format_time)) expected = [] for d in data: expected.append(mock.call('http://localhost:4318/v1/metrics', auth=None, cert=None, data=json.dumps(d), headers={'Content-type': 'application/json'}, timeout=5, verify=True)) self.assertEqual(expected, m_req.mock_calls) def test_post_samples_ssl(self): """Test publisher post.""" parsed_url = urlparse.urlparse( 'opentelemetryhttp://localhost:4318/v1/metrics?ssl=1') publisher = opentelemetry_http.OpentelemetryHttpPublisher( self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) data = [] for s in self.sample_data: data.append(self._make_fake_json(s, self.format_time)) expected = [] for d in data: expected.append(mock.call('https://localhost:4318/v1/metrics', auth=None, cert=None, data=json.dumps(d), headers={'Content-type': 'application/json'}, timeout=5, verify=True)) self.assertEqual(expected, m_req.mock_calls) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2253568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_prometheus.py0000664000175000017500000001611015250267452025377 0ustar00zuulzuul# # Copyright 2016 IBM # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/prometheus.py""" import datetime from unittest import mock import uuid from oslo_utils import timeutils import requests from urllib import parse as urlparse from ceilometer.publisher import prometheus from ceilometer import sample from ceilometer import service from ceilometer.tests import base class TestPrometheusPublisher(base.BaseTestCase): resource_id = str(uuid.uuid4()) sample_data = [ sample.Sample( name='alpha', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='beta', type=sample.TYPE_DELTA, unit='', volume=3, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='gamma', type=sample.TYPE_GAUGE, unit='', volume=5, user_id='test', project_id='test', resource_id=resource_id, timestamp=datetime.datetime.now().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='delta.epsilon', type=sample.TYPE_GAUGE, unit='', volume=7, user_id='test', project_id='test', resource_id=resource_id, timestamp=datetime.datetime.now().isoformat(), resource_metadata={'name': 'TestPublish'}, ), ] def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def test_post_samples(self): """Test publisher post.""" parsed_url = urlparse.urlparse( 'prometheus://localhost:90/metrics/job/os') publisher = prometheus.PrometheusPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) data = """# TYPE alpha counter alpha{{resource_id="{}", user_id="test", project_id="test"}} 1 beta{{resource_id="{}", user_id="test", project_id="test"}} 3 # TYPE gamma gauge gamma{{resource_id="{}", user_id="test", project_id="test"}} 5 # TYPE delta_epsilon gauge delta_epsilon{{resource_id="{}", user_id="test", project_id="test"}} 7 """.format(self.resource_id, self.resource_id, self.resource_id, self.resource_id) rid_q = urlparse.quote(self.resource_id, safe='') expected = [ mock.call( 'http://localhost:90/metrics/job/os/resource_id/%s' % rid_q, auth=None, cert=None, data=data, headers={ 'Content-type': 'plain/text'}, timeout=5, verify=True)] self.assertEqual(expected, m_req.mock_calls) def test_post_samples_ssl(self): """Test publisher post.""" parsed_url = urlparse.urlparse( 'prometheus://localhost:90/metrics/job/os?ssl=1') publisher = prometheus.PrometheusPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) data = """# TYPE alpha counter alpha{{resource_id="{}", user_id="test", project_id="test"}} 1 beta{{resource_id="{}", user_id="test", project_id="test"}} 3 # TYPE gamma gauge gamma{{resource_id="{}", user_id="test", project_id="test"}} 5 # TYPE delta_epsilon gauge delta_epsilon{{resource_id="{}", user_id="test", project_id="test"}} 7 """.format(self.resource_id, self.resource_id, self.resource_id, self.resource_id) rid_q = urlparse.quote(self.resource_id, safe='') expected = [ mock.call( 'https://localhost:90/metrics/job/os/resource_id/%s' % rid_q, auth=None, cert=None, data=data, headers={ 'Content-type': 'plain/text'}, timeout=5, verify=True)] self.assertEqual(expected, m_req.mock_calls) def test_post_samples_with_grouping(self): """When grouping labels are in the URL path, a single POST is made.""" parsed_url = urlparse.urlparse( 'prometheus://localhost:90/metrics/job/os/instance/myhost') publisher = prometheus.PrometheusPublisher(self.CONF, parsed_url) self.assertTrue(publisher._has_grouping) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(self.sample_data) self.assertEqual(1, len(m_req.mock_calls)) call_url = m_req.call_args[0][0] self.assertEqual( 'http://localhost:90/metrics/job/os/instance/myhost', call_url) self.assertNotIn('/resource_id/', call_url) def test_metric_name_hyphen_sanitized(self): """Hyphens in metric names must be replaced with underscores.""" hyphen_sample = [ sample.Sample( name='some-metric-name', type=sample.TYPE_GAUGE, unit='', volume=42, user_id='test', project_id='test', resource_id=self.resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={}, ), ] parsed_url = urlparse.urlparse( 'prometheus://localhost:90/metrics/job/os') publisher = prometheus.PrometheusPublisher(self.CONF, parsed_url) res = requests.Response() res.status_code = 200 with mock.patch.object(requests.Session, 'post', return_value=res) as m_req: publisher.publish_samples(hyphen_sample) posted_data = m_req.call_args[1]['data'] self.assertIn('some_metric_name', posted_data) self.assertNotIn('some-metric-name', posted_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2253568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_tcp.py0000664000175000017500000001517015250267452023777 0ustar00zuulzuul# # Copyright 2022 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/tcp.py""" from unittest import mock import msgpack from oslo_utils import netutils from oslo_utils import timeutils from ceilometer.publisher.tcp import TCPPublisher from ceilometer.publisher import utils from ceilometer import sample from ceilometer import service from ceilometer.tests import base COUNTER_SOURCE = 'testsource' class TestTCPPublisher(base.BaseTestCase): test_data = [ sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test3', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), ] @staticmethod def _make_fake_socket(published): def _fake_socket_create_connection(inet_addr): def record_data(msg): msg_length = int.from_bytes(msg[0:8], "little") published.append(msg[8:msg_length + 8]) tcp_socket = mock.Mock() tcp_socket.send = record_data return tcp_socket return _fake_socket_create_connection def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.CONF.publisher.telemetry_secret = 'not-so-secret' def test_published(self): self.data_sent = [] with mock.patch('ceilometer.publisher.tcp.socket.create_connection', self._make_fake_socket(self.data_sent)): publisher = TCPPublisher(self.CONF, netutils.urlsplit('tcp://somehost')) publisher.publish_samples(self.test_data) self.assertEqual(5, len(self.data_sent)) sent_counters = [] for data in self.data_sent: counter = msgpack.loads(data, raw=False) sent_counters.append(counter) # Check that counters are equal def sort_func(counter): return counter['counter_name'] counters = [utils.meter_message_from_counter(d, "not-so-secret", publisher.conf.host) for d in self.test_data] counters.sort(key=sort_func) sent_counters.sort(key=sort_func) self.assertEqual(counters, sent_counters) def _make_disconnecting_socket(self): def _fake_socket_create_connection(inet_addr): def record_data(msg): if not self.connections: self.connections = True raise OSError msg_length = int.from_bytes(msg[0:8], "little") self.data_sent.append(msg[8:msg_length + 8]) tcp_socket = mock.MagicMock() tcp_socket.send = record_data return tcp_socket return _fake_socket_create_connection def test_reconnect(self): self.data_sent = [] self.connections = False with mock.patch('ceilometer.publisher.tcp.socket.create_connection', self._make_disconnecting_socket()): publisher = TCPPublisher(self.CONF, netutils.urlsplit('tcp://somehost')) publisher.publish_samples(self.test_data) sent_counters = [] for data in self.data_sent: counter = msgpack.loads(data, raw=False) sent_counters.append(counter) # Check that counters are equal def sort_func(counter): return counter['counter_name'] counters = [utils.meter_message_from_counter(d, "not-so-secret", publisher.conf.host) for d in self.test_data] counters.sort(key=sort_func) sent_counters.sort(key=sort_func) self.assertEqual(counters, sent_counters) @staticmethod def _raise_OSError(*args): raise OSError def _make_broken_socket(self, inet_addr): tcp_socket = mock.Mock() tcp_socket.send = self._raise_OSError return tcp_socket def test_publish_error(self): with mock.patch('ceilometer.publisher.tcp.socket.create_connection', self._make_broken_socket): publisher = TCPPublisher(self.CONF, netutils.urlsplit('tcp://localhost')) publisher.publish_samples(self.test_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2253568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_udp.py0000664000175000017500000001156515250267452024005 0ustar00zuulzuul# # Copyright 2013-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/udp.py""" from unittest import mock import msgpack from oslo_utils import netutils from oslo_utils import timeutils from ceilometer.publisher import udp from ceilometer.publisher import utils from ceilometer import sample from ceilometer import service from ceilometer.tests import base COUNTER_SOURCE = 'testsource' class TestUDPPublisher(base.BaseTestCase): test_data = [ sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test2', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), sample.Sample( name='test3', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id='test_run_tasks', timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, source=COUNTER_SOURCE, ), ] @staticmethod def _make_fake_socket(published): def _fake_socket_socket(family, type): def record_data(msg, dest): published.append((msg, dest)) udp_socket = mock.Mock() udp_socket.sendto = record_data return udp_socket return _fake_socket_socket def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.CONF.publisher.telemetry_secret = 'not-so-secret' def test_published(self): self.data_sent = [] with mock.patch('socket.socket', self._make_fake_socket(self.data_sent)): publisher = udp.UDPPublisher( self.CONF, netutils.urlsplit('udp://somehost')) publisher.publish_samples(self.test_data) self.assertEqual(5, len(self.data_sent)) sent_counters = [] for data, dest in self.data_sent: counter = msgpack.loads(data, raw=False) sent_counters.append(counter) # Check destination self.assertEqual(('somehost', 4952), dest) # Check that counters are equal def sort_func(counter): return counter['counter_name'] counters = [utils.meter_message_from_counter(d, "not-so-secret") for d in self.test_data] counters.sort(key=sort_func) sent_counters.sort(key=sort_func) self.assertEqual(counters, sent_counters) @staticmethod def _raise_ioerror(*args): raise OSError def _make_broken_socket(self, family, type): udp_socket = mock.Mock() udp_socket.sendto = self._raise_ioerror return udp_socket def test_publish_error(self): with mock.patch('socket.socket', self._make_broken_socket): publisher = udp.UDPPublisher( self.CONF, netutils.urlsplit('udp://localhost')) publisher.publish_samples(self.test_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2253568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_utils.py0000664000175000017500000001437615250267452024360 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/publisher/utils.py """ import json from ceilometer.publisher import utils from ceilometer.tests import base class TestSignature(base.BaseTestCase): def test_compute_signature_change_key(self): sig1 = utils.compute_signature({'a': 'A', 'b': 'B'}, 'not-so-secret') sig2 = utils.compute_signature({'A': 'A', 'b': 'B'}, 'not-so-secret') self.assertNotEqual(sig1, sig2) def test_compute_signature_change_value(self): sig1 = utils.compute_signature({'a': 'A', 'b': 'B'}, 'not-so-secret') sig2 = utils.compute_signature({'a': 'a', 'b': 'B'}, 'not-so-secret') self.assertNotEqual(sig1, sig2) def test_compute_signature_same(self): sig1 = utils.compute_signature({'a': 'A', 'b': 'B'}, 'not-so-secret') sig2 = utils.compute_signature({'a': 'A', 'b': 'B'}, 'not-so-secret') self.assertEqual(sig1, sig2) def test_compute_signature_signed(self): data = {'a': 'A', 'b': 'B'} sig1 = utils.compute_signature(data, 'not-so-secret') data['message_signature'] = sig1 sig2 = utils.compute_signature(data, 'not-so-secret') self.assertEqual(sig1, sig2) def test_compute_signature_use_configured_secret(self): data = {'a': 'A', 'b': 'B'} sig1 = utils.compute_signature(data, 'not-so-secret') sig2 = utils.compute_signature(data, 'different-value') self.assertNotEqual(sig1, sig2) def test_verify_signature_signed(self): data = {'a': 'A', 'b': 'B'} sig1 = utils.compute_signature(data, 'not-so-secret') data['message_signature'] = sig1 self.assertTrue(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_unsigned(self): data = {'a': 'A', 'b': 'B'} self.assertFalse(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_incorrect(self): data = {'a': 'A', 'b': 'B', 'message_signature': 'Not the same'} self.assertFalse(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_invalid_encoding(self): data = {'a': 'A', 'b': 'B', 'message_signature': ''} self.assertFalse(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_unicode(self): data = {'a': 'A', 'b': 'B', 'message_signature': ''} self.assertFalse(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_nested(self): data = {'a': 'A', 'b': 'B', 'nested': {'a': 'A', 'b': 'B', }, } data['message_signature'] = utils.compute_signature( data, 'not-so-secret') self.assertTrue(utils.verify_signature(data, 'not-so-secret')) def test_verify_signature_nested_json(self): data = {'a': 'A', 'b': 'B', 'nested': {'a': 'A', 'b': 'B', 'c': ('c',), 'd': ['d'] }, } data['message_signature'] = utils.compute_signature( data, 'not-so-secret') jsondata = json.loads(json.dumps(data)) self.assertTrue(utils.verify_signature(jsondata, 'not-so-secret')) def test_verify_unicode_symbols(self): data = {'a\xe9\u0437': 'A', 'b': 'B\xe9\u0437' } data['message_signature'] = utils.compute_signature( data, 'not-so-secret') jsondata = json.loads(json.dumps(data)) self.assertTrue(utils.verify_signature(jsondata, 'not-so-secret')) def test_verify_no_secret(self): data = {'a': 'A', 'b': 'B'} self.assertTrue(utils.verify_signature(data, '')) class TestUtils(base.BaseTestCase): def test_recursive_keypairs(self): data = {'a': 'A', 'b': 'B', 'nested': {'a': 'A', 'b': 'B'}} pairs = list(utils.recursive_keypairs(data)) self.assertEqual([('a', 'A'), ('b', 'B'), ('nested:a', 'A'), ('nested:b', 'B')], pairs) def test_recursive_keypairs_with_separator(self): data = {'a': 'A', 'b': 'B', 'nested': {'a': 'A', 'b': 'B', }, } separator = '.' pairs = list(utils.recursive_keypairs(data, separator)) self.assertEqual([('a', 'A'), ('b', 'B'), ('nested.a', 'A'), ('nested.b', 'B')], pairs) def test_recursive_keypairs_with_list_of_dict(self): small = 1 big = 1 << 64 expected = [('a', 'A'), ('b', 'B'), ('nested:list', [{small: 99, big: 42}])] data = {'a': 'A', 'b': 'B', 'nested': {'list': [{small: 99, big: 42}]}} pairs = list(utils.recursive_keypairs(data)) self.assertEqual(len(expected), len(pairs)) for k, v in pairs: # the keys 1 and 1<<64 cause a hash collision on 64bit platforms if k == 'nested:list': self.assertIn(v, [[{small: 99, big: 42}], [{big: 42, small: 99}]]) else: self.assertIn((k, v), expected) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2253568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/publisher/test_zaqar.py0000664000175000017500000001063715250267452024332 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime from unittest import mock import uuid from oslo_utils import timeutils from urllib import parse as urlparse from ceilometer.event import models as event from ceilometer.publisher import zaqar from ceilometer import sample from ceilometer import service from ceilometer.tests import base class TestZaqarPublisher(base.BaseTestCase): resource_id = str(uuid.uuid4()) sample_data = [ sample.Sample( name='alpha', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='beta', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=timeutils.utcnow().isoformat(), resource_metadata={'name': 'TestPublish'}, ), sample.Sample( name='gamma', type=sample.TYPE_CUMULATIVE, unit='', volume=1, user_id='test', project_id='test', resource_id=resource_id, timestamp=datetime.datetime.now().isoformat(), resource_metadata={'name': 'TestPublish'}, ), ] event_data = [event.Event( message_id=str(uuid.uuid4()), event_type='event_%d' % i, generated=timeutils.utcnow().isoformat(), traits=[], raw={'payload': {'some': 'aa'}}) for i in range(3)] def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) def test_zaqar_publisher_config(self): """Test publisher config parameters.""" parsed_url = urlparse.urlparse('zaqar://') self.assertRaises(ValueError, zaqar.ZaqarPublisher, self.CONF, parsed_url) parsed_url = urlparse.urlparse('zaqar://?queue=foo&ttl=bar') self.assertRaises(ValueError, zaqar.ZaqarPublisher, self.CONF, parsed_url) parsed_url = urlparse.urlparse('zaqar://?queue=foo&ttl=60') publisher = zaqar.ZaqarPublisher(self.CONF, parsed_url) self.assertEqual(60, publisher.ttl) parsed_url = urlparse.urlparse('zaqar://?queue=foo') publisher = zaqar.ZaqarPublisher(self.CONF, parsed_url) self.assertEqual(3600, publisher.ttl) self.assertEqual('foo', publisher.queue_name) @mock.patch('zaqarclient.queues.v2.queues.Queue') def test_zaqar_post_samples(self, mock_queue): """Test publisher post.""" parsed_url = urlparse.urlparse('zaqar://?queue=foo') publisher = zaqar.ZaqarPublisher(self.CONF, parsed_url) mock_post = mock.Mock() mock_queue.return_value = mock_post publisher.publish_samples(self.sample_data) mock_queue.assert_called_once_with(mock.ANY, 'foo') self.assertEqual( 3, len(mock_post.post.call_args_list[0][0][0])) self.assertEqual( mock_post.post.call_args_list[0][0][0][0]['body'], self.sample_data[0].as_dict()) @mock.patch('zaqarclient.queues.v2.queues.Queue') def test_zaqar_post_events(self, mock_queue): """Test publisher post.""" parsed_url = urlparse.urlparse('zaqar://?queue=foo') publisher = zaqar.ZaqarPublisher(self.CONF, parsed_url) mock_post = mock.Mock() mock_queue.return_value = mock_post publisher.publish_events(self.event_data) mock_queue.assert_called_once_with(mock.ANY, 'foo') self.assertEqual( 3, len(mock_post.post.call_args_list[0][0][0])) self.assertEqual( mock_post.post.call_args_list[0][0][0][0]['body'], self.event_data[0].serialize()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2751276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/share/0000775000175000017500000000000015250267567020710 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/share/__init__.py0000664000175000017500000000000015250267452023000 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/share/test_manila.py0000664000175000017500000000747015250267452023563 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer.polling import manager from ceilometer import service from ceilometer.share import discovery from ceilometer.share import manila from ceilometer.tests import base from ceilometer.tests.unit import fakes class _BaseTestSharePollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) class TestShareStatusPollster(_BaseTestSharePollster): def setUp(self): super().setUp() self.pollster = manila.ShareStatusPollster(self.CONF) self.shares = fakes.FakeSDKManilaClient.default_shares def test_share_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.shares)) self.assertEqual(len(self.shares), len(samples)) # Verify metadata fields are correctly extracted self.assertEqual('my-share-1', samples[0].resource_metadata['name']) self.assertEqual('az-1', samples[0].resource_metadata['availability_zone']) # Verify share_protocol is renamed to protocol self.assertEqual('NFS', samples[0].resource_metadata['protocol']) self.assertNotIn('share_protocol', samples[0].resource_metadata) def test_share_status_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.shares)) # available = 1, creating = 2, error = 4 self.assertEqual(1, samples[0].volume) self.assertEqual(2, samples[1].volume) self.assertEqual(4, samples[2].volume) def test_get_share_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.shares)) self.assertEqual({'manila.share.status'}, {s.name for s in samples}) def test_share_discovery(self): discovered_shares = discovery.ShareDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(fakes.FakeSDKManilaClient.default_shares), len(list(discovered_shares))) class TestShareSizePollster(_BaseTestSharePollster): def setUp(self): super().setUp() self.pollster = manila.ShareSizePollster(self.CONF) self.shares = fakes.FakeSDKManilaClient.default_shares def test_share_size_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.shares)) self.assertEqual(100, samples[0].volume) self.assertEqual(50, samples[1].volume) self.assertEqual(200, samples[2].volume) def test_get_share_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.shares)) self.assertEqual({'manila.share.size'}, {s.name for s in samples}) class TestSharePollsterUnknownStatus(_BaseTestSharePollster): def test_unknown_share_status(self): pollster = manila.ShareStatusPollster(self.CONF) samples = list(pollster.get_samples( self.manager, {}, resources=[fakes.SHARE_UNKNOWN_STATUS])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_bin.py0000664000175000017500000000775415250267452021775 0ustar00zuulzuul# Copyright 2012 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import subprocess import time from oslo_utils import fileutils from ceilometer.tests import base class BinTestCase(base.BaseTestCase): def setUp(self): super().setUp() content = ("[DEFAULT]\n" "transport_url = fake://\n") content = content.encode('utf-8') self.tempfile = fileutils.write_to_tempfile(content=content, prefix='ceilometer', suffix='.conf') def tearDown(self): super().tearDown() os.remove(self.tempfile) def test_upgrade_run(self): subp = subprocess.Popen(['ceilometer-upgrade', '--skip-gnocchi-resource-types', "--config-file=%s" % self.tempfile]) self.assertEqual(0, subp.wait()) class BinSendSampleTestCase(base.BaseTestCase): def setUp(self): super().setUp() pipeline_cfg_file = self.path_get( 'ceilometer/pipeline/data/pipeline.yaml') content = ("[DEFAULT]\n" "transport_url = fake://\n" "pipeline_cfg_file={}\n".format(pipeline_cfg_file)) content = content.encode('utf-8') self.tempfile = fileutils.write_to_tempfile(content=content, prefix='ceilometer', suffix='.conf') def tearDown(self): super().tearDown() os.remove(self.tempfile) def test_send_counter_run(self): subp = subprocess.Popen(['ceilometer-send-sample', "--config-file=%s" % self.tempfile, "--sample-resource=someuuid", "--sample-name=mycounter"]) self.assertEqual(0, subp.wait()) class BinCeilometerPollingServiceTestCase(base.BaseTestCase): def setUp(self): super().setUp() self.tempfile = None self.subp = None def tearDown(self): if self.subp: try: self.subp.kill() except OSError: pass os.remove(self.tempfile) super().tearDown() def test_starting_with_duplication_namespaces(self): content = ("[DEFAULT]\n" "transport_url = fake://\n") content = content.encode('utf-8') self.tempfile = fileutils.write_to_tempfile(content=content, prefix='ceilometer', suffix='.conf') self.subp = subprocess.Popen(['ceilometer-polling', "--config-file=%s" % self.tempfile, "--polling-namespaces", "compute", "compute"], stderr=subprocess.PIPE) expected = (b'Duplicated values: [\'compute\', \'compute\'] ' b'found in CLI options, auto de-duplicated') # NOTE(gordc): polling process won't quit so wait for a bit and check start = time.time() while time.time() - start < 5: output = self.subp.stderr.readline() if expected in output: break else: self.fail('Did not detect expected warning: %s' % expected) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_cache_utils.py0000664000175000017500000001004715250267452023475 0ustar00zuulzuul# # Copyright 2022 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import cache_utils from ceilometer import service as ceilometer_service from ceilometer.tests import base from oslo_cache.backends import dictionary from oslo_cache import core as cache from oslo_config import fixture as config_fixture class CacheConfFixture(config_fixture.Config): def setUp(self): super().setUp() self.conf = ceilometer_service.\ prepare_service(argv=[], config_files=[]) cache.configure(self.conf) class TestOsloCache(base.BaseTestCase): def setUp(self): super().setUp() conf = ceilometer_service.prepare_service(argv=[], config_files=[]) dict_conf_fixture = CacheConfFixture(conf) self.useFixture(dict_conf_fixture) dict_conf_fixture.config(enabled=True, group='cache') dict_conf_fixture.config(expiration_time=600, backend='oslo_cache.dict', group='cache') self.dict_conf = dict_conf_fixture.conf # enable_retry_client is only supported by # 'dogpile.cache.pymemcache' backend which makes this # incorrect config faulty_conf_fixture = CacheConfFixture(conf) self.useFixture(faulty_conf_fixture) faulty_conf_fixture.config(enabled=True, group='cache') faulty_conf_fixture.config(expiration_time=600, backend='dogpile.cache.memcached', group='cache', enable_retry_client='true') self.faulty_conf = faulty_conf_fixture.conf no_cache_fixture = CacheConfFixture(conf) self.useFixture(no_cache_fixture) # no_cache_fixture.config() self.no_cache_conf = no_cache_fixture.conf def test_get_cache_region(self): self.assertIsNotNone(cache_utils.get_cache_region(self.dict_conf)) # having invalid configurations will return None with self.assertLogs('ceilometer.cache_utils', level='ERROR') as logs: self.assertIsNone( cache_utils.get_cache_region(self.faulty_conf) ) cache_configure_failed = logs.output self.assertIn( 'ERROR:ceilometer.cache_utils:' 'failed to configure oslo_cache: ' 'Retry client is only supported by ' 'the \'dogpile.cache.pymemcache\' backend.', cache_configure_failed) def test_get_client(self): dict_cache_client = cache_utils.get_client(self.dict_conf) self.assertIsNotNone(dict_cache_client) self.assertIsInstance(dict_cache_client.region.backend, dictionary.DictCacheBackend) no_cache_config = cache_utils.get_client(self.no_cache_conf) self.assertIsNotNone(no_cache_config) self.assertIsInstance(dict_cache_client.region.backend, dictionary.DictCacheBackend) # having invalid configurations will return None with self.assertLogs('ceilometer.cache_utils', level='ERROR') as logs: cache_client = cache_utils.get_client(self.faulty_conf) cache_configure_failed = logs.output self.assertIsNone(cache_client) self.assertIn( 'ERROR:ceilometer.cache_utils:' 'failed to configure oslo_cache: ' 'Retry client is only supported by ' 'the \'dogpile.cache.pymemcache\' backend.', cache_configure_failed) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_cinder_client.py0000664000175000017500000001326715250267452024023 0ustar00zuulzuul# Copyright (C) 2026 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from openstack import exceptions as os_exceptions from oslo_config import fixture as config_fixture from ceilometer import cinder_client from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes class TestCinderClient(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.conf = self.useFixture(config_fixture.Config(self.CONF)) self.conf.config( group='service_credentials', region_name='RegionOne', interface='publicURL') self.conf.config(group='service_types', cinder='volumev3') self.mock_get_session = self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_session')) self.client = cinder_client.Client(self.CONF) def test_init_creates_connection_with_session(self): self.fake_conn_class_mock.assert_called_once_with( block_storage_api_version='3.64', session=self.mock_get_session.mock.return_value, oslo_conf=self.CONF, region_name='RegionOne', block_storage_interface='publicURL', service_types={'volumev3'}) def test_list_volumes_returns_volumes(self): result = self.client.list_volumes(search_opts={'all_projects': True}) self.assertEqual(fakes.VOLUME_LIST, result) def test_list_volumes_passes_search_opts(self): self.client.list_volumes(search_opts={'all_projects': False}) self.fake_conn.block_storage.volumes.assert_called_once_with( details=True, all_projects=False) def test_list_volumes_default_opts(self): self.client.list_volumes() self.fake_conn.block_storage.volumes.assert_called_once_with( details=True, all_projects=True) def test_list_volumes_propagates_exception(self): self.fake_conn.block_storage.volumes = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.client.list_volumes) def test_list_volume_snapshots_returns_snapshots(self): result = self.client.list_volume_snapshots( search_opts={'all_projects': True}) self.assertEqual(fakes.SNAPSHOT_LIST, result) def test_list_volume_snapshots_passes_search_opts(self): self.client.list_volume_snapshots(search_opts={'all_projects': False}) self.fake_conn.block_storage.snapshots.assert_called_once_with( details=True, all_projects=False) def test_list_volume_snapshots_default_opts(self): self.client.list_volume_snapshots() self.fake_conn.block_storage.snapshots.assert_called_once_with( details=True, all_projects=True) def test_list_volume_snapshots_propagates_exception(self): self.fake_conn.block_storage.snapshots = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.client.list_volume_snapshots) def test_list_backups_returns_backups(self): result = self.client.list_backups(search_opts={'all_projects': True}) self.assertEqual(fakes.BACKUP_LIST, result) def test_list_backups_passes_search_opts(self): self.client.list_backups(search_opts={'all_projects': False}) self.fake_conn.block_storage.backups.assert_called_once_with( details=True, all_projects=False) def test_list_backups_default_opts(self): self.client.list_backups() self.fake_conn.block_storage.backups.assert_called_once_with( details=True, all_projects=True) def test_list_backups_propagates_exception(self): self.fake_conn.block_storage.backups = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.client.list_backups) def test_list_pools_returns_pools(self): result = self.client.list_pools() self.assertEqual(fakes.POOL_LIST, result) def test_list_pools_calls_backend_pools(self): self.client.list_pools() self.fake_conn.block_storage.backend_pools.assert_called_once_with() def test_list_pools_propagates_exception(self): self.fake_conn.block_storage.backend_pools = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.client.list_pools) def test_list_services(self): result = self.client.list_services() self.assertIsInstance(result, list) self.assertEqual(fakes.SERVICE_LIST, result) def test_list_services_calls_services(self): self.client.list_services() self.fake_conn.block_storage.services.assert_called_once_with() def test_list_services_propagates_exception(self): self.fake_conn.block_storage.services = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.client.list_services) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2263567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_declarative.py0000664000175000017500000000313115250267452023471 0ustar00zuulzuul# # Copyright 2016 Mirantis, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from ceilometer import declarative from ceilometer.tests import base class TestDefinition(base.BaseTestCase): def setUp(self): super().setUp() self.configs = [ "_field1", "_field2|_field3", {'fields': 'field4.`split(., 1, 1)`'}, {'fields': ['field5.arg', 'field6'], 'type': 'text'} ] self.parser = mock.MagicMock() parser_patch = fixtures.MockPatch( "jsonpath_rw_ext.parser.ExtentedJsonPathParser.parse", new=self.parser) self.useFixture(parser_patch) def test_caching_parsers(self): for config in self.configs * 2: declarative.Definition("test", config, mock.MagicMock()) self.assertEqual(4, self.parser.call_count) self.parser.assert_has_calls([ mock.call("_field1"), mock.call("_field2|_field3"), mock.call("field4.`split(., 1, 1)`"), mock.call("(field5.arg)|(field6)"), ]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2283566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_decoupled_pipeline.py0000664000175000017500000001630715250267452025050 0ustar00zuulzuul# # Copyright 2014 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.pipeline import base from ceilometer.pipeline import sample as pipeline from ceilometer import sample from ceilometer.tests.unit import pipeline_base class TestDecoupledPipeline(pipeline_base.BasePipelineTestCase): def _setup_pipeline_cfg(self): source = {'name': 'test_source', 'meters': ['a'], 'sinks': ['test_sink']} sink = {'name': 'test_sink', 'publishers': ['test://']} self.pipeline_cfg = {'sources': [source], 'sinks': [sink]} def _augment_pipeline_cfg(self): self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'meters': ['b'], 'sinks': ['second_sink'] }) self.pipeline_cfg['sinks'].append({ 'name': 'second_sink', 'publishers': ['new'], }) def _break_pipeline_cfg(self): self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'meters': ['b'], 'sinks': ['second_sink'] }) self.pipeline_cfg['sinks'].append({ 'name': 'second_sink', 'publishers': ['except'], }) def _dup_pipeline_name_cfg(self): self.pipeline_cfg['sources'].append({ 'name': 'test_source', 'meters': ['b'], 'sinks': ['test_sink'] }) def _set_pipeline_cfg(self, field, value): if field in self.pipeline_cfg['sources'][0]: self.pipeline_cfg['sources'][0][field] = value else: self.pipeline_cfg['sinks'][0][field] = value def _extend_pipeline_cfg(self, field, value): if field in self.pipeline_cfg['sources'][0]: self.pipeline_cfg['sources'][0][field].extend(value) else: self.pipeline_cfg['sinks'][0][field].extend(value) def _unset_pipeline_cfg(self, field): if field in self.pipeline_cfg['sources'][0]: del self.pipeline_cfg['sources'][0][field] else: del self.pipeline_cfg['sinks'][0][field] def test_source_no_sink(self): del self.pipeline_cfg['sinks'] self._exception_create_pipelinemanager() def test_source_dangling_sink(self): self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'meters': ['b'], 'sinks': ['second_sink'] }) self._exception_create_pipelinemanager() def test_sink_no_source(self): del self.pipeline_cfg['sources'] self._exception_create_pipelinemanager() def test_source_with_multiple_sinks(self): meter_cfg = ['a', 'b'] self._set_pipeline_cfg('meters', meter_cfg) self.pipeline_cfg['sinks'].append({ 'name': 'second_sink', 'publishers': ['new'], }) self.pipeline_cfg['sources'][0]['sinks'].append('second_sink') self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) self.test_counter = sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([self.test_counter]) self.assertEqual(2, len(pipeline_manager.pipelines)) self.assertEqual('test_source:test_sink', str(pipeline_manager.pipelines[0])) self.assertEqual('test_source:second_sink', str(pipeline_manager.pipelines[1])) test_publisher = pipeline_manager.pipelines[0].publishers[0] new_publisher = pipeline_manager.pipelines[1].publishers[0] for publisher in (test_publisher, new_publisher): self.assertEqual(2, len(publisher.samples)) self.assertEqual(2, publisher.calls) self.assertEqual('a', getattr(publisher.samples[0], "name")) self.assertEqual('b', getattr(publisher.samples[1], "name")) def test_multiple_sources_with_single_sink(self): self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'meters': ['b'], 'sinks': ['test_sink'] }) self._build_and_set_new_pipeline() pipeline_manager = pipeline.SamplePipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_counter]) self.test_counter = sample.Sample( name='b', type=self.test_counter.type, volume=self.test_counter.volume, unit=self.test_counter.unit, user_id=self.test_counter.user_id, project_id=self.test_counter.project_id, resource_id=self.test_counter.resource_id, timestamp=self.test_counter.timestamp, resource_metadata=self.test_counter.resource_metadata, ) with pipeline_manager.publisher() as p: p([self.test_counter]) self.assertEqual(2, len(pipeline_manager.pipelines)) self.assertEqual('test_source:test_sink', str(pipeline_manager.pipelines[0])) self.assertEqual('second_source:test_sink', str(pipeline_manager.pipelines[1])) test_publisher = pipeline_manager.pipelines[0].publishers[0] another_publisher = pipeline_manager.pipelines[1].publishers[0] for publisher in [test_publisher, another_publisher]: self.assertEqual(2, len(publisher.samples)) self.assertEqual(2, publisher.calls) self.assertEqual('a', getattr(publisher.samples[0], "name")) self.assertEqual('b', getattr(publisher.samples[1], "name")) def test_duplicated_sinks_names(self): self.pipeline_cfg['sinks'].append({ 'name': 'test_sink', 'publishers': ['except'], }) self._build_and_set_new_pipeline() self.assertRaises(base.PipelineException, pipeline.SamplePipelineManager, self.CONF) def test_duplicated_source_names(self): self.pipeline_cfg['sources'].append({ 'name': 'test_source', 'meters': ['a'], 'sinks': ['test_sink'] }) self._build_and_set_new_pipeline() self.assertRaises(base.PipelineException, pipeline.SamplePipelineManager, self.CONF) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2283566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_event_pipeline.py0000664000175000017500000003354615250267452024231 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import traceback import uuid import fixtures from oslo_utils import timeutils from ceilometer.event import models from ceilometer.pipeline import base as pipeline from ceilometer.pipeline import event from ceilometer import publisher from ceilometer.publisher import test as test_publisher from ceilometer import service from ceilometer.tests import base class EventPipelineTestCase(base.BaseTestCase): def get_publisher(self, conf, url, namespace=''): fake_drivers = {'test://': test_publisher.TestPublisher, 'new://': test_publisher.TestPublisher, 'except://': self.PublisherClassException} return fake_drivers[url](conf, url) class PublisherClassException(publisher.ConfigPublisherBase): def publish_samples(self, samples): pass def publish_events(self, events): raise Exception() def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.test_event = models.Event( message_id=uuid.uuid4(), event_type='a', generated=timeutils.utcnow(), traits=[ models.Trait('t_text', 1, 'text_trait'), models.Trait('t_int', 2, 'int_trait'), models.Trait('t_float', 3, 'float_trait'), models.Trait('t_datetime', 4, 'datetime_trait') ], raw={'status': 'started'} ) self.test_event2 = models.Event( message_id=uuid.uuid4(), event_type='b', generated=timeutils.utcnow(), traits=[ models.Trait('t_text', 1, 'text_trait'), models.Trait('t_int', 2, 'int_trait'), models.Trait('t_float', 3, 'float_trait'), models.Trait('t_datetime', 4, 'datetime_trait') ], raw={'status': 'stopped'} ) self.useFixture(fixtures.MockPatchObject( publisher, 'get_publisher', side_effect=self.get_publisher)) self._setup_pipeline_cfg() self._reraise_exception = True self.useFixture(fixtures.MockPatch( 'ceilometer.pipeline.base.LOG.exception', side_effect=self._handle_reraise_exception)) def _handle_reraise_exception(self, *args, **kwargs): if self._reraise_exception: raise Exception(traceback.format_exc()) def _setup_pipeline_cfg(self): """Setup the appropriate form of pipeline config.""" source = {'name': 'test_source', 'events': ['a'], 'sinks': ['test_sink']} sink = {'name': 'test_sink', 'publishers': ['test://']} self.pipeline_cfg = {'sources': [source], 'sinks': [sink]} def _augment_pipeline_cfg(self): """Augment the pipeline config with an additional element.""" self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'events': ['b'], 'sinks': ['second_sink'] }) self.pipeline_cfg['sinks'].append({ 'name': 'second_sink', 'publishers': ['new://'], }) def _break_pipeline_cfg(self): """Break the pipeline config with a malformed element.""" self.pipeline_cfg['sources'].append({ 'name': 'second_source', 'events': ['b'], 'sinks': ['second_sink'] }) self.pipeline_cfg['sinks'].append({ 'name': 'second_sink', 'publishers': ['except'], }) def _dup_pipeline_name_cfg(self): """Break the pipeline config with duplicate pipeline name.""" self.pipeline_cfg['sources'].append({ 'name': 'test_source', 'events': ['a'], 'sinks': ['test_sink'] }) def _set_pipeline_cfg(self, field, value): if field in self.pipeline_cfg['sources'][0]: self.pipeline_cfg['sources'][0][field] = value else: self.pipeline_cfg['sinks'][0][field] = value def _extend_pipeline_cfg(self, field, value): if field in self.pipeline_cfg['sources'][0]: self.pipeline_cfg['sources'][0][field].extend(value) else: self.pipeline_cfg['sinks'][0][field].extend(value) def _unset_pipeline_cfg(self, field): if field in self.pipeline_cfg['sources'][0]: del self.pipeline_cfg['sources'][0][field] else: del self.pipeline_cfg['sinks'][0][field] def _build_and_set_new_pipeline(self): name = self.cfg2file(self.pipeline_cfg) self.CONF.set_override('event_pipeline_cfg_file', name) def _exception_create_pipelinemanager(self): self._build_and_set_new_pipeline() self.assertRaises(pipeline.PipelineException, event.EventPipelineManager, self.CONF) def test_no_events(self): self._unset_pipeline_cfg('events') self._exception_create_pipelinemanager() def test_no_name(self): self._unset_pipeline_cfg('name') self._exception_create_pipelinemanager() def test_name(self): self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) for pipe in pipeline_manager.pipelines: self.assertTrue(pipe.name.startswith('event:')) def test_no_publishers(self): self._unset_pipeline_cfg('publishers') self._exception_create_pipelinemanager() def test_check_events_include_exclude_same(self): event_cfg = ['a', '!a'] self._set_pipeline_cfg('events', event_cfg) self._exception_create_pipelinemanager() def test_check_events_include_exclude(self): event_cfg = ['a', '!b'] self._set_pipeline_cfg('events', event_cfg) self._exception_create_pipelinemanager() def test_check_events_wildcard_included(self): event_cfg = ['a', '*'] self._set_pipeline_cfg('events', event_cfg) self._exception_create_pipelinemanager() def test_check_publishers_invalid_publisher(self): publisher_cfg = ['test_invalid'] self._set_pipeline_cfg('publishers', publisher_cfg) def test_multiple_included_events(self): event_cfg = ['a', 'b'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.events)) with pipeline_manager.publisher() as p: p([self.test_event2]) self.assertEqual(2, len(publisher.events)) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) self.assertEqual('b', getattr(publisher.events[1], 'event_type')) def test_event_non_match(self): event_cfg = ['nomatch'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(0, len(publisher.events)) self.assertEqual(0, publisher.calls) def test_wildcard_event(self): event_cfg = ['*'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.events)) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) def test_wildcard_excluded_events(self): event_cfg = ['*', '!a'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_event('a')) def test_wildcard_excluded_events_not_excluded(self): event_cfg = ['*', '!b'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.events)) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) def test_all_excluded_events_not_excluded(self): event_cfg = ['!b', '!c'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.events)) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) def test_all_excluded_events_excluded(self): event_cfg = ['!a', '!c'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_event('a')) self.assertTrue(pipe.source.support_event('b')) self.assertFalse(pipe.source.support_event('c')) def test_wildcard_and_excluded_wildcard_events(self): event_cfg = ['*', '!compute.*'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source. support_event('compute.instance.create.start')) self.assertTrue(pipe.source.support_event('identity.user.create')) def test_included_event_and_wildcard_events(self): event_cfg = ['compute.instance.create.start', 'identity.*'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertTrue(pipe.source.support_event('identity.user.create')) self.assertTrue(pipe.source. support_event('compute.instance.create.start')) self.assertFalse(pipe.source. support_event('compute.instance.create.stop')) def test_excluded_event_and_excluded_wildcard_events(self): event_cfg = ['!compute.instance.create.start', '!identity.*'] self._set_pipeline_cfg('events', event_cfg) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) pipe = pipeline_manager.pipelines[0] self.assertFalse(pipe.source.support_event('identity.user.create')) self.assertFalse(pipe.source. support_event('compute.instance.create.start')) self.assertTrue(pipe.source. support_event('compute.instance.create.stop')) def test_multiple_pipeline(self): self._augment_pipeline_cfg() self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event, self.test_event2]) publisher = pipeline_manager.pipelines[0].publishers[0] self.assertEqual(1, len(publisher.events)) self.assertEqual(1, publisher.calls) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) new_publisher = pipeline_manager.pipelines[1].publishers[0] self.assertEqual(1, len(new_publisher.events)) self.assertEqual(1, new_publisher.calls) self.assertEqual('b', getattr(new_publisher.events[0], 'event_type')) def test_multiple_publisher(self): self._set_pipeline_cfg('publishers', ['test://', 'new://']) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[0] new_publisher = pipeline_manager.pipelines[0].publishers[1] self.assertEqual(1, len(publisher.events)) self.assertEqual(1, len(new_publisher.events)) self.assertEqual('a', getattr(new_publisher.events[0], 'event_type')) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) def test_multiple_publisher_isolation(self): self._reraise_exception = False self._set_pipeline_cfg('publishers', ['except://', 'new://']) self._build_and_set_new_pipeline() pipeline_manager = event.EventPipelineManager(self.CONF) with pipeline_manager.publisher() as p: p([self.test_event]) publisher = pipeline_manager.pipelines[0].publishers[1] self.assertEqual(1, len(publisher.events)) self.assertEqual('a', getattr(publisher.events[0], 'event_type')) def test_unique_pipeline_names(self): self._dup_pipeline_name_cfg() self._exception_create_pipelinemanager() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2293568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_exceptions.py0000664000175000017500000000457315250267452023402 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer.exceptions""" from ceilometer import exceptions from ceilometer.tests import base class TestExceptions(base.BaseTestCase): def test_ceilometer_exception_with_none(self): """Test CeilometerException with None returns class name.""" exc = exceptions.CeilometerException() self.assertEqual('CeilometerException', str(exc)) self.assertEqual('CeilometerException', exc.message) def test_ceilometer_exception_with_message(self): """Test CeilometerException with message returns message.""" exc = exceptions.CeilometerException('Test message') self.assertEqual('Test message', str(exc)) self.assertEqual('Test message', exc.message) def test_not_found_with_message(self): """Test NotFound with message returns message.""" exc = exceptions.NotFound('test msg') self.assertEqual('test msg', str(exc)) self.assertEqual('test msg', exc.message) def test_not_found_with_details(self): """Test NotFound stores details correctly.""" exc = exceptions.NotFound('Resource not found', details={'id': 123}) self.assertEqual('Resource not found', str(exc)) self.assertEqual('Resource not found', exc.message) self.assertEqual({'id': 123}, exc.details) def test_not_found_without_details(self): """Test NotFound without details defaults to empty dict.""" exc = exceptions.NotFound('Resource not found') self.assertEqual('Resource not found', str(exc)) self.assertEqual({}, exc.details) def test_no_unique_match_inherits_from_base(self): """Test NoUniqueMatch is a CeilometerException.""" exc = exceptions.NoUniqueMatch('Multiple matches') self.assertIsInstance(exc, exceptions.CeilometerException) self.assertEqual('Multiple matches', str(exc)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2293568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_keystone_client.py0000664000175000017500000007612115250267452024416 0ustar00zuulzuul# Copyright 2025 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from keystoneauth1.access import service_catalog from keystoneauth1.exceptions import catalog as catalog_exceptions from oslo_config import cfg from oslo_config import fixture as config_fixture from ceilometer import exceptions as ceilo_exc from ceilometer import keystone_client from ceilometer import service as ceilo_service from ceilometer.tests import base from ceilometer.tests.unit import fakes CONF = cfg.CONF class FakeSession: """A fake keystone auth session.""" pass class FakeAuthPlugin: """A fake keystone Auth Plugin.""" pass class TestKeystoneClient(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = ceilo_service.prepare_service([], []) # V3 catalog format catalog_data = [ { 'type': 'compute', 'name': 'nova', 'endpoints': [ { 'region_id': 'RegionOne', 'interface': 'public', 'url': 'http://nova.public/v2.1' }, { 'region_id': 'RegionOne', 'interface': 'internal', 'url': 'http://nova.internal/v2.1' }, ] }, { 'type': 'identity', 'name': 'keystone', 'endpoints': [ { 'region_id': 'RegionOne', 'interface': 'public', 'url': 'http://keystone.public/v3' }, ] }, { 'type': 'network', 'name': 'neutron', 'endpoints': [ { 'region_id': 'RegionOne', 'interface': 'public', 'url': 'http://neutron.public' } ] }, { 'type': 'radosgw', 'name': 'radosgw', 'endpoints': [ { 'region_id': 'RegionOne', 'interface': 'public', 'url': 'http://radosgw.public' } ] }, { 'type': 'radosgw', 'name': 'swift', 'endpoints': [ { 'region_id': 'RegionOne', 'interface': 'public', 'url': 'http://swift.public' } ] } ] self.fake_catalog = service_catalog.ServiceCatalogV3(catalog_data) @mock.patch( 'keystoneauth1.loading.load_session_from_conf_options', return_value=FakeSession(), autospec=True) @mock.patch( 'keystoneauth1.loading.load_auth_from_conf_options', return_value=FakeAuthPlugin(), autospec=True) def test_get_session(self, mock_load_auth, mock_load_session): session = keystone_client.get_session(self.CONF) mock_load_auth.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP) mock_load_session.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP, auth=mock_load_auth.return_value, session=None) self.assertIsInstance(session, FakeSession) @mock.patch( 'keystoneauth1.loading.load_session_from_conf_options', return_value=FakeSession(), autospec=True) @mock.patch( 'keystoneauth1.loading.load_auth_from_conf_options', return_value=FakeAuthPlugin(), autospec=True) def test_get_session_with_group(self, mock_load_auth, mock_load_session): session = keystone_client.get_session( self.CONF, group="some_other_group") mock_load_auth.assert_called_once_with(self.CONF, "some_other_group") mock_load_session.assert_called_once_with( self.CONF, "some_other_group", auth=mock_load_auth.return_value, session=None) self.assertIsInstance(session, FakeSession) @mock.patch( 'keystoneauth1.loading.load_session_from_conf_options', return_value=FakeSession(), autospec=True) @mock.patch( 'keystoneauth1.loading.load_auth_from_conf_options', return_value=FakeAuthPlugin(), autospec=True) def test_get_session_with_session(self, mock_load_auth, mock_load_session): fakeSession = FakeSession() session = keystone_client.get_session(self.CONF, fakeSession) mock_load_auth.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP) mock_load_session.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP, auth=mock_load_auth.return_value, session=fakeSession) self.assertIsInstance(session, FakeSession) @mock.patch( 'keystoneauth1.loading.load_session_from_conf_options', return_value=FakeSession(), autospec=True) @mock.patch( 'keystoneauth1.loading.load_auth_from_conf_options', return_value=FakeAuthPlugin(), autospec=True) def test_get_session_with_timeout(self, mock_load_auth, mock_load_session): session = keystone_client.get_session(self.CONF, timeout=100) mock_load_auth.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP) mock_load_session.assert_called_once_with( self.CONF, keystone_client.DEFAULT_GROUP, auth=mock_load_auth.return_value, session=None, timeout=100) self.assertIsInstance(session, FakeSession) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_connection(self, mock_get_session): actual_conn = keystone_client.get_connection(self.CONF) mock_get_session.assert_called_once_with( self.CONF, requests_session=None, group=keystone_client.DEFAULT_GROUP) self.fake_conn_class_mock.assert_called_once_with( session=mock_get_session.return_value, oslo_conf=self.CONF, service_types={"identity"}) self.assertEqual(actual_conn, self.fake_conn) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_connection_service_type(self, mock_get_session): actual_conn = keystone_client.get_connection( self.CONF, service_type="other_service_type") mock_get_session.assert_called_once_with( self.CONF, requests_session=None, group=keystone_client.DEFAULT_GROUP) self.fake_conn_class_mock.assert_called_once_with( session=mock_get_session.return_value, oslo_conf=self.CONF, service_types={"other_service_type"}) self.assertEqual(actual_conn, self.fake_conn) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_connection_existing_session(self, mock_get_session): mock_session = mock.Mock() actual_conn = keystone_client.get_connection( self.CONF, requests_session=mock_session) mock_get_session.assert_called_once_with( self.CONF, requests_session=mock_session, group=keystone_client.DEFAULT_GROUP) self.fake_conn_class_mock.assert_called_once_with( session=mock_get_session.return_value, oslo_conf=self.CONF, service_types={"identity"}) self.assertEqual(actual_conn, self.fake_conn) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_connection_new_group(self, mock_get_session): actual_conn = keystone_client.get_connection( self.CONF, group="some_group") mock_get_session.assert_called_once_with( self.CONF, requests_session=None, group="some_group") self.fake_conn_class_mock.assert_called_once_with( session=mock_get_session.return_value, oslo_conf=self.CONF, service_types={"identity"}) self.assertEqual(actual_conn, self.fake_conn) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_client(self, mock_get_session): mock_session = FakeSession() mock_get_session.return_value = mock_session conf = self.useFixture(config_fixture.Config(self.CONF)) conf.config(group=keystone_client.DEFAULT_GROUP, interface="internal") conf.config( group=keystone_client.DEFAULT_GROUP, region_name="expected_region") self.setup_connection(domains=[], projects=[]) result = keystone_client.get_client(conf.conf) self.assertIsInstance(result, keystone_client.Client) mock_get_session.assert_called_once_with( self.CONF, requests_session=None, group=keystone_client.DEFAULT_GROUP) self.fake_conn_class_mock.assert_called_once_with( session=mock_get_session.return_value, service_types={"identity"}, interface='internal', region_name='expected_region') def test_get_service_catalog(self): mock_client = mock.Mock() mock_catalog = [ {'name': 'keystone', 'type': 'identity'}, {'name': 'nova', 'type': 'compute'} ] mock_access = mock.Mock() mock_access.service_catalog = mock_catalog mock_client.session.auth.get_access.return_value = mock_access result = keystone_client.get_service_catalog(mock_client) mock_client.session.auth.get_access.assert_called_once_with( mock_client.session) self.assertEqual(result, mock_catalog) def test_get_service_catalog_with_real_client(self): mock_session = mock.Mock() client = keystone_client.Client(session=mock_session) result = keystone_client.get_service_catalog(client) client.session.auth.get_access.assert_called_once_with(client.session) mock_session.auth.get_access.assert_called_once_with(mock_session) self.assertEqual( result, client.session.auth.get_access.return_value.service_catalog) def test_get_auth_token_with_real_client(self): mock_session = mock.Mock() client = keystone_client.Client(session=mock_session) result = keystone_client.get_auth_token(client) client.session.auth.get_access.assert_called_once_with(client.session) self.assertEqual( result, mock_session.auth.get_access.return_value.auth_token) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for(mock.Mock()) self.assertEqual(url, "http://nova.public/v2.1") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_type(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for(mock.Mock(), service_type="network") self.assertEqual(url, "http://neutron.public") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_name(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for(mock.Mock(), service_name="neutron") self.assertEqual(url, "http://neutron.public") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_name_and_type_agree(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), service_type="compute", service_name="nova") self.assertEqual(url, "http://nova.public/v2.1") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_name_and_type_disagree(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog self.assertRaises( catalog_exceptions.EndpointNotFound, keystone_client.url_for, mock.Mock(), service_type="network", service_name="nova") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_non_existant_service(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog self.assertRaises( catalog_exceptions.EndpointNotFound, keystone_client.url_for, mock.Mock(), service_type="rating") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_interface(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for(mock.Mock(), interface="public") self.assertEqual(url, "http://nova.public/v2.1") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_region_name(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), region_name="RegionOne") self.assertEqual(url, "http://nova.public/v2.1") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_region_name_non_existant(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog self.assertRaises( catalog_exceptions.EndpointNotFound, keystone_client.url_for, mock.Mock(), region_name="UnknownRegion") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_interface_internal(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), service_type="compute", interface="internal") self.assertEqual(url, "http://nova.internal/v2.1") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_interface_non_existant(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog self.assertRaises( catalog_exceptions.EndpointNotFound, keystone_client.url_for, mock.Mock(), service_type="network", interface="internal") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_type_and_interface(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), service_type="identity", interface="public") self.assertEqual(url, "http://keystone.public/v3") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_empty_catalog(self, mock_get_catalog): mock_get_catalog.return_value = service_catalog.ServiceCatalogV3([]) self.assertRaises( catalog_exceptions.EndpointNotFound, keystone_client.url_for, mock.Mock(), service_type="compute") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_type_multiple_matches(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), service_type="radosgw") self.assertEqual(url, "http://radosgw.public") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_service_name_service_type_multi_match( self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog url = keystone_client.url_for( mock.Mock(), service_type="radosgw", service_name="swift") self.assertEqual(url, "http://swift.public") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_url_for_calls_get_service_catalog(self, mock_get_catalog): mock_catalog = mock.Mock() mock_catalog.url_for.return_value = "http://test.url" mock_get_catalog.return_value = mock_catalog mock_client = mock.Mock() result = keystone_client.url_for( mock_client, service_type="compute", service_name="nova", interface="public", region_name="RegionOne") mock_get_catalog.assert_called_once_with(mock_client) mock_catalog.url_for.assert_called_once_with( service_type="compute", service_name="nova", interface="public", region_name="RegionOne") self.assertEqual(result, "http://test.url") @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="public") self.assertEqual(urls, ("http://nova.public/v2.1",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_type(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="network", interface="public") self.assertEqual(urls, ("http://neutron.public",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_name(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_name="neutron", interface="public") self.assertEqual(urls, ("http://neutron.public",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_name_and_type_agree(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", service_name="nova", interface="public") self.assertEqual(urls, ("http://nova.public/v2.1",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_name_and_type_disagree(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="network", service_name="nova", interface="public") self.assertEqual(urls, ()) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_non_existant_service(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="rating", interface="public") self.assertEqual(urls, ()) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_interface(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="public") self.assertEqual(urls, ("http://nova.public/v2.1",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_region_name(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="public", region_name="RegionOne") self.assertEqual(urls, ("http://nova.public/v2.1",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_region_name_non_existant(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="public", region_name="UnknownRegion") self.assertEqual(urls, ()) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_interface_internal(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="internal") self.assertEqual(urls, ("http://nova.internal/v2.1",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_interface_non_existant(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="network", interface="internal") self.assertEqual(urls, ()) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_type_and_interface(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="identity", interface="public") self.assertEqual(urls, ("http://keystone.public/v3",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_empty_catalog(self, mock_get_catalog): mock_get_catalog.return_value = service_catalog.ServiceCatalogV3([]) urls = keystone_client.get_urls( mock.Mock(), service_type="compute", interface="public") self.assertEqual(urls, ()) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_type_multiple_matches(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="radosgw") self.assertEqual( urls, ("http://radosgw.public", "http://swift.public")) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_service_name_service_type_multi(self, mock_get_catalog): mock_get_catalog.return_value = self.fake_catalog urls = keystone_client.get_urls( mock.Mock(), service_type="radosgw", service_name="swift") self.assertEqual( urls, ("http://swift.public",)) @mock.patch( 'ceilometer.keystone_client.get_service_catalog', autospec=True) def test_get_urls_calls_get_service_catalog(self, mock_get_catalog): mock_catalog = mock.Mock() mock_catalog.get_urls.return_value = ["http://test.url"] mock_get_catalog.return_value = mock_catalog mock_client = mock.Mock() result = keystone_client.get_urls( mock_client, service_type="compute", service_name="nova", interface="public", region_name="RegionOne") mock_get_catalog.assert_called_once_with(mock_client) mock_catalog.get_urls.assert_called_once_with( service_type="compute", service_name="nova", interface="public", region_name="RegionOne") self.assertEqual(result, ["http://test.url"]) def test_get_auth_token(self): mock_client = mock.Mock() mock_access = mock.Mock() mock_access.auth_token = 'auth_token' mock_client.session.auth.get_access.return_value = mock_access result = keystone_client.get_auth_token(mock_client) mock_client.session.auth.get_access.assert_called_once_with( mock_client.session) self.assertIsInstance(result, str) self.assertEqual(result, 'auth_token') def test_get_auth_token_from_openstacksdk_connection(self): self.fake_conn.session = mock.Mock() mock_access = mock.Mock() mock_access.auth_token = 'my_connection_auth_token' self.fake_conn.session.auth.get_access.return_value = mock_access result = keystone_client.get_auth_token(self.fake_conn) self.fake_conn.session.auth.get_access.assert_called_once_with( self.fake_conn.session) self.assertIsInstance(result, str) self.assertEqual(result, 'my_connection_auth_token') class TestKeystoneClientClientClass(base.BaseTestCase): def setUp(self): super().setUp() domains_sdk = fakes.DEFAULT_DOMAINS_sdk + [fakes.DOMAIN_DISABLED_sdk] self.setup_connection(domains=domains_sdk) self.client = keystone_client.Client(session=mock.Mock()) def assertReturnsListOfType(self, result_list, expected_type): """Assert that the result_list is filled with items of expected_type. Used to assert the right types are returned from the Client """ for r in result_list: self.assertIsInstance(r, expected_type) def test_find_project(self): result = self.client.find_project(name='admin') self.assertIsInstance(result, keystone_client.Project) self.assertEqual(fakes.PROJECT_ADMIN_ceilo, result) def test_find_project_not_found(self): self.assertRaises( ceilo_exc.NotFound, self.client.find_project, name='nonexistent') def test_find_project_not_found_message(self): try: self.client.find_project(name='nonexistant') except ceilo_exc.NotFound as e: self.assertEqual( e.details, "No Project matching {'name': 'nonexistant'}.") def test_find_project_no_unique_match(self): self.setup_connection( domains=[], projects=[fakes.PROJECT_ADMIN_sdk, fakes.PROJECT_ADMIN_sdk]) client = keystone_client.Client(session=mock.Mock()) self.assertRaises( ceilo_exc.NoUniqueMatch, client.find_project, name='admin') def test_find_project_no_unique_match_message(self): self.setup_connection( domains=[], projects=[fakes.PROJECT_ADMIN_sdk, fakes.PROJECT_ADMIN_sdk]) client = keystone_client.Client(session=mock.Mock()) self.assertRaisesRegex( ceilo_exc.NoUniqueMatch, "ClientException", client.find_project, name='admin') def test_list_projects(self): result = self.client.list_projects(fakes.DOMAIN_DEFAULT_ceilo) self.assertReturnsListOfType(result, keystone_client.Project) self.assertEqual( [fakes.PROJECT_ADMIN_ceilo, fakes.PROJECT_SERVICE_ceilo, fakes.PROJECT_DISABLED_ceilo], result) def test_list_projects_domain_id(self): result = self.client.list_projects(fakes.DOMAIN_DEFAULT_ceilo.id) self.assertEqual( [fakes.PROJECT_ADMIN_ceilo, fakes.PROJECT_SERVICE_ceilo, fakes.PROJECT_DISABLED_ceilo], result) def test_list_projects_filter_enabled(self): result = self.client.list_projects( fakes.DOMAIN_DEFAULT_ceilo, enabled=True) self.assertReturnsListOfType(result, keystone_client.Project) self.assertEqual( [fakes.PROJECT_ADMIN_ceilo, fakes.PROJECT_SERVICE_ceilo], result) def test_list_projects_no_match(self): # Use a domain that none of the projects belong to result = self.client.list_projects(fakes.DOMAIN_DISABLED_ceilo) self.assertEqual([], result) def test_find_domain(self): result = self.client.find_domain(name='Default') self.assertIsInstance(result, keystone_client.Domain) self.assertEqual(fakes.DOMAIN_DEFAULT_ceilo, result) def test_find_domain_not_found(self): self.assertRaises( ceilo_exc.NotFound, self.client.find_domain, name='nonexistent') def test_find_domain_not_found_message(self): try: self.client.find_domain(name='nonexistant') except ceilo_exc.NotFound as e: self.assertEqual( e.details, "No Domain matching {'name': 'nonexistant'}.") def test_find_domain_no_unique_match(self): self.setup_connection( domains=[fakes.DOMAIN_DEFAULT_sdk, fakes.DOMAIN_DEFAULT_sdk], projects=[]) client = keystone_client.Client(session=mock.Mock()) self.assertRaises( ceilo_exc.NoUniqueMatch, client.find_domain, name='Default') def test_find_domain_no_unique_match_message(self): self.setup_connection( domains=[fakes.DOMAIN_DEFAULT_sdk, fakes.DOMAIN_DEFAULT_sdk], projects=[]) client = keystone_client.Client(session=mock.Mock()) self.assertRaisesRegex( ceilo_exc.NoUniqueMatch, "ClientException", client.find_domain, name='Default') def test_list_domains(self): result = self.client.list_domains() self.assertReturnsListOfType(result, keystone_client.Domain) self.assertEqual( [fakes.DOMAIN_HEAT_ceilo, fakes.DOMAIN_DEFAULT_ceilo, fakes.DOMAIN_DISABLED_ceilo], result) def test_list_domains_filter_by_name(self): result = self.client.list_domains(name='Default') self.assertEqual([fakes.DOMAIN_DEFAULT_ceilo], result) def test_list_domains_filter_enabled(self): result = self.client.list_domains(enabled=True) self.assertReturnsListOfType(result, keystone_client.Domain) self.assertIn(fakes.DOMAIN_DEFAULT_ceilo, result) self.assertIn(fakes.DOMAIN_HEAT_ceilo, result) self.assertNotIn(fakes.DOMAIN_DISABLED_ceilo, result) def test_list_domains_no_match(self): result = self.client.list_domains(name='NonExistent') self.assertEqual([], result) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2303567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_messaging.py0000664000175000017500000000517215250267452023172 0ustar00zuulzuul# Copyright (C) 2014 eNovance SAS # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import oslo_messaging.conffixture from ceilometer import messaging from ceilometer import service from ceilometer.tests import base class MessagingTests(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.useFixture(oslo_messaging.conffixture.ConfFixture(self.CONF)) def test_get_transport_invalid_url(self): self.assertRaises(oslo_messaging.InvalidTransportURL, messaging.get_transport, self.CONF, "notvalid!") def test_get_transport_url_caching(self): t1 = messaging.get_transport(self.CONF, 'fake://') t2 = messaging.get_transport(self.CONF, 'fake://') self.assertEqual(t1, t2) def test_get_transport_default_url_caching(self): t1 = messaging.get_transport(self.CONF) t2 = messaging.get_transport(self.CONF) self.assertEqual(t1, t2) def test_get_transport_default_url_no_caching(self): t1 = messaging.get_transport(self.CONF, cache=False) t2 = messaging.get_transport(self.CONF, cache=False) self.assertNotEqual(t1, t2) def test_get_transport_url_no_caching(self): t1 = messaging.get_transport(self.CONF, 'fake://', cache=False) t2 = messaging.get_transport(self.CONF, 'fake://', cache=False) self.assertNotEqual(t1, t2) def test_get_transport_default_url_caching_mix(self): t1 = messaging.get_transport(self.CONF) t2 = messaging.get_transport(self.CONF, cache=False) self.assertNotEqual(t1, t2) def test_get_transport_url_caching_mix(self): t1 = messaging.get_transport(self.CONF, 'fake://') t2 = messaging.get_transport(self.CONF, 'fake://', cache=False) self.assertNotEqual(t1, t2) def test_get_transport_optional(self): self.CONF.set_override('transport_url', 'non-url') self.assertIsNone(messaging.get_transport(self.CONF, optional=True, cache=False)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2303567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_middleware.py0000664000175000017500000000776515250267452023344 0ustar00zuulzuul# # Copyright 2013-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from ceilometer import middleware from ceilometer import service from ceilometer.tests import base HTTP_REQUEST = { 'ctxt': {'auth_token': '3d8b13de1b7d499587dfc69b77dc09c2', 'is_admin': True, 'project_id': '7c150a59fe714e6f9263774af9688f0e', 'quota_class': None, 'read_deleted': 'no', 'remote_address': '10.0.2.15', 'request_id': 'req-d68b36e0-9233-467f-9afb-d81435d64d66', 'roles': ['admin'], 'timestamp': '2012-05-08T20:23:41.425105', 'user_id': '1e3ce043029547f1a61c1996d1a531a2'}, 'event_type': 'http.request', 'payload': {'request': {'HTTP_X_FOOBAR': 'foobaz', 'HTTP_X_USER_ID': 'jd-x32', 'HTTP_X_PROJECT_ID': 'project-id', 'HTTP_X_SERVICE_NAME': 'nova'}}, 'priority': 'INFO', 'publisher_id': 'compute.vagrant-precise', 'metadata': {'message_id': 'dae6f69c-00e0-41c0-b371-41ec3b7f4451', 'timestamp': '2012-05-08 20:23:48.028195'}, } HTTP_RESPONSE = { 'ctxt': {'auth_token': '3d8b13de1b7d499587dfc69b77dc09c2', 'is_admin': True, 'project_id': '7c150a59fe714e6f9263774af9688f0e', 'quota_class': None, 'read_deleted': 'no', 'remote_address': '10.0.2.15', 'request_id': 'req-d68b36e0-9233-467f-9afb-d81435d64d66', 'roles': ['admin'], 'timestamp': '2012-05-08T20:23:41.425105', 'user_id': '1e3ce043029547f1a61c1996d1a531a2'}, 'event_type': 'http.response', 'payload': {'request': {'HTTP_X_FOOBAR': 'foobaz', 'HTTP_X_USER_ID': 'jd-x32', 'HTTP_X_PROJECT_ID': 'project-id', 'HTTP_X_SERVICE_NAME': 'nova'}, 'response': {'status': '200 OK'}}, 'priority': 'INFO', 'publisher_id': 'compute.vagrant-precise', 'metadata': {'message_id': 'dae6f69c-00e0-41c0-b371-41ec3b7f4451', 'timestamp': '2012-05-08 20:23:48.028195'}, } class TestNotifications(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.setup_messaging(self.CONF) def test_process_request_notification(self): sample = list(middleware.HTTPRequest( mock.Mock(), mock.Mock()).build_sample(HTTP_REQUEST))[0] self.assertEqual(HTTP_REQUEST['payload']['request']['HTTP_X_USER_ID'], sample.user_id) self.assertEqual(HTTP_REQUEST['payload']['request'] ['HTTP_X_PROJECT_ID'], sample.project_id) self.assertEqual(HTTP_REQUEST['payload']['request'] ['HTTP_X_SERVICE_NAME'], sample.resource_id) self.assertEqual(1, sample.volume) def test_process_response_notification(self): sample = list(middleware.HTTPResponse( mock.Mock(), mock.Mock()).build_sample(HTTP_RESPONSE))[0] self.assertEqual(HTTP_RESPONSE['payload']['request']['HTTP_X_USER_ID'], sample.user_id) self.assertEqual(HTTP_RESPONSE['payload']['request'] ['HTTP_X_PROJECT_ID'], sample.project_id) self.assertEqual(HTTP_RESPONSE['payload']['request'] ['HTTP_X_SERVICE_NAME'], sample.resource_id) self.assertEqual(1, sample.volume) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2303567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_neutronclient.py0000664000175000017500000001164115250267452024104 0ustar00zuulzuul# Copyright (C) 2026 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from openstack import exceptions as os_exc from ceilometer import neutron_client from ceilometer import service from ceilometer.tests import base class TestNeutronClient(base.BaseTestCase): """Tests for the Neutron client using openstacksdk.""" def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.nc = neutron_client.Client(self.CONF) def test_fip_get_all(self): """Test getting all floating IPs returns list of dicts.""" result = self.nc.fip_get_all() self.assertEqual(1, len(result)) self.assertIsInstance(result[0], dict) self.assertEqual('fip-123', result[0]['id']) self.assertEqual('192.168.1.100', result[0]['floating_ip_address']) def test_fip_get_all_empty(self): """Test getting floating IPs when none exist.""" self.nc.conn.network.ips = mock.Mock(return_value=iter([])) result = self.nc.fip_get_all() self.assertEqual([], result) def test_vpn_get_all(self): """Test getting all VPN services returns list of dicts.""" result = self.nc.vpn_get_all() self.assertEqual(1, len(result)) self.assertIsInstance(result[0], dict) self.assertEqual('vpn-123', result[0]['id']) self.assertEqual('my-vpn', result[0]['name']) def test_vpn_get_all_empty(self): """Test getting VPN services when none exist.""" self.nc.conn.network.vpn_services = mock.Mock( return_value=iter([])) result = self.nc.vpn_get_all() self.assertEqual([], result) def test_ipsec_site_connections_get_all(self): """Test getting all IPSec site connections returns list of dicts.""" result = self.nc.ipsec_site_connections_get_all() self.assertEqual(1, len(result)) self.assertIsInstance(result[0], dict) self.assertEqual('ipsec-123', result[0]['id']) def test_ipsec_site_connections_get_all_empty(self): """Test getting IPSec connections when none exist.""" self.nc.conn.network.vpn_ipsec_site_connections =\ mock.Mock(return_value=iter([])) result = self.nc.ipsec_site_connections_get_all() self.assertEqual([], result) def test_firewall_get_all(self): """Test getting all firewall groups returns list of dicts.""" result = self.nc.firewall_get_all() self.assertEqual(1, len(result)) self.assertIsInstance(result[0], dict) self.assertEqual('fw-123', result[0]['id']) self.assertEqual('my-firewall', result[0]['name']) def test_firewall_get_all_empty(self): """Test getting firewall groups when none exist.""" self.nc.conn.network.firewall_groups = mock.Mock( return_value=iter([])) result = self.nc.firewall_get_all() self.assertEqual([], result) def test_fw_policy_get_all(self): """Test getting all firewall policies returns list of dicts.""" result = self.nc.fw_policy_get_all() self.assertEqual(1, len(result)) self.assertIsInstance(result[0], dict) self.assertEqual('policy-123', result[0]['id']) self.assertEqual('my-policy', result[0]['name']) def test_fw_policy_get_all_empty(self): """Test getting firewall policies when none exist.""" self.nc.conn.network.firewall_policies = mock.Mock( return_value=iter([])) result = self.nc.fw_policy_get_all() self.assertEqual([], result) def test_fip_get_all_404_returns_empty(self): """Test that 404 errors return empty list.""" self.nc.conn.network.ips = mock.Mock( side_effect=os_exc.HttpException(http_status=404)) result = self.nc.fip_get_all() self.assertEqual([], result) def test_fip_get_all_other_http_error_returns_empty(self): """Test that other HTTP errors return empty list.""" self.nc.conn.network.ips = mock.Mock(side_effect=os_exc.HttpException( http_status=500)) result = self.nc.fip_get_all() self.assertEqual([], result) def test_fip_get_all_non_http_exception_raises(self): """Test that non-HTTP exceptions are re-raised.""" self.nc.conn.network.ips = mock.Mock(side_effect=RuntimeError( "Connection failed")) self.assertRaises(RuntimeError, self.nc.fip_get_all) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2303567 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_notification.py0000664000175000017500000002043415250267452023701 0ustar00zuulzuul# # Copyright 2012 New Dream Network, LLC (DreamHost) # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for Ceilometer notify daemon.""" import time from unittest import mock from oslo_utils import fileutils import yaml from ceilometer import messaging from ceilometer import notification from ceilometer.publisher import test as test_publisher from ceilometer import service from ceilometer.tests import base as tests_base TEST_NOTICE_CTXT = { 'auth_token': '3d8b13de1b7d499587dfc69b77dc09c2', 'is_admin': True, 'project_id': '7c150a59fe714e6f9263774af9688f0e', 'quota_class': None, 'read_deleted': 'no', 'remote_address': '10.0.2.15', 'request_id': 'req-d68b36e0-9233-467f-9afb-d81435d64d66', 'roles': ['admin'], 'timestamp': '2012-05-08T20:23:41.425105', 'user_id': '1e3ce043029547f1a61c1996d1a531a2', } TEST_NOTICE_METADATA = { 'message_id': 'dae6f69c-00e0-41c0-b371-41ec3b7f4451', 'timestamp': '2012-05-08 20:23:48.028195', } TEST_NOTICE_PAYLOAD = { 'created_at': '2012-05-08 20:23:41', 'deleted_at': '', 'disk_gb': 0, 'display_name': 'testme', 'fixed_ips': [{'address': '10.0.0.2', 'floating_ips': [], 'meta': {}, 'type': 'fixed', 'version': 4}], 'image_ref_url': 'http://10.0.2.15:9292/images/UUID', 'instance_id': '9f9d01b9-4a58-4271-9e27-398b21ab20d1', 'instance_type': 'm1.tiny', 'instance_type_id': 2, 'launched_at': '2012-05-08 20:23:47.985999', 'memory_mb': 512, 'state': 'active', 'state_description': '', 'tenant_id': '7c150a59fe714e6f9263774af9688f0e', 'user_id': '1e3ce043029547f1a61c1996d1a531a2', 'reservation_id': '1e3ce043029547f1a61c1996d1a531a3', 'vcpus': 1, 'root_gb': 0, 'ephemeral_gb': 0, 'host': 'compute-host-name', 'availability_zone': '1e3ce043029547f1a61c1996d1a531a4', 'os_type': 'linux?', 'architecture': 'x86', 'image_ref': 'UUID', 'kernel_id': '1e3ce043029547f1a61c1996d1a531a5', 'ramdisk_id': '1e3ce043029547f1a61c1996d1a531a6', } class BaseNotificationTest(tests_base.BaseTestCase): def run_service(self, srv): srv.run() self.addCleanup(srv.terminate) class TestNotification(BaseNotificationTest): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.setup_messaging(self.CONF) self.srv = notification.NotificationService(0, self.CONF) def test_targets(self): self.assertEqual(14, len(self.srv.get_targets())) def test_start_multiple_listeners(self): urls = ["fake://vhost1", "fake://vhost2"] self.CONF.set_override("messaging_urls", urls, group="notification") self.srv.run() self.addCleanup(self.srv.terminate) self.assertEqual(2, len(self.srv.listeners)) @mock.patch('oslo_messaging.get_batch_notification_listener') def test_unique_consumers(self, mock_listener): self.CONF.set_override('notification_control_exchanges', ['dup'] * 2, group='notification') self.run_service(self.srv) # 1 target, 1 listener self.assertEqual(1, len(mock_listener.call_args_list[0][0][1])) self.assertEqual(1, len(self.srv.listeners)) def test_select_pipelines(self): self.CONF.set_override('pipelines', ['event'], group='notification') self.srv.run() self.addCleanup(self.srv.terminate) self.assertEqual(1, len(self.srv.managers)) self.assertEqual(1, len(self.srv.listeners[0].dispatcher.endpoints)) @mock.patch('ceilometer.notification.LOG') def test_select_pipelines_missing(self, logger): self.CONF.set_override('pipelines', ['meter', 'event', 'bad'], group='notification') self.srv.run() self.addCleanup(self.srv.terminate) self.assertEqual(2, len(self.srv.managers)) logger.error.assert_called_with( 'Could not load the following pipelines: %s', {'bad'}) class BaseRealNotification(BaseNotificationTest): def setup_pipeline(self, counter_names): pipeline = yaml.dump({ 'sources': [{ 'name': 'test_pipeline', 'interval': 5, 'meters': counter_names, 'sinks': ['test_sink'] }], 'sinks': [{ 'name': 'test_sink', 'publishers': ['test://'] }] }) pipeline = pipeline.encode('utf-8') pipeline_cfg_file = fileutils.write_to_tempfile(content=pipeline, prefix="pipeline", suffix="yaml") return pipeline_cfg_file def setup_event_pipeline(self, event_names): ev_pipeline = yaml.dump({ 'sources': [{ 'name': 'test_event', 'events': event_names, 'sinks': ['test_sink'] }], 'sinks': [{ 'name': 'test_sink', 'publishers': ['test://'] }] }) ev_pipeline = ev_pipeline.encode('utf-8') ev_pipeline_cfg_file = fileutils.write_to_tempfile( content=ev_pipeline, prefix="event_pipeline", suffix="yaml") return ev_pipeline_cfg_file def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.setup_messaging(self.CONF, 'nova') pipeline_cfg_file = self.setup_pipeline(['vcpus', 'memory']) self.CONF.set_override("pipeline_cfg_file", pipeline_cfg_file) self.expected_samples = 2 ev_pipeline_cfg_file = self.setup_event_pipeline( ['compute.instance.*']) self.expected_events = 1 self.CONF.set_override("event_pipeline_cfg_file", ev_pipeline_cfg_file) self.publisher = test_publisher.TestPublisher(self.CONF, "") def _check_notification_service(self): self.run_service(self.srv) notifier = messaging.get_notifier(self.transport, "compute.vagrant-precise") notifier.info({}, 'compute.instance.create.end', TEST_NOTICE_PAYLOAD) start = time.time() while time.time() - start < 60: if (len(self.publisher.samples) >= self.expected_samples and len(self.publisher.events) >= self.expected_events): break resources = list({s.resource_id for s in self.publisher.samples}) self.assertEqual(self.expected_samples, len(self.publisher.samples)) self.assertEqual(self.expected_events, len(self.publisher.events)) self.assertEqual(["9f9d01b9-4a58-4271-9e27-398b21ab20d1"], resources) class TestRealNotification(BaseRealNotification): def setUp(self): super().setUp() self.srv = notification.NotificationService(0, self.CONF) @mock.patch('ceilometer.publisher.test.TestPublisher') def test_notification_service(self, fake_publisher_cls): fake_publisher_cls.return_value = self.publisher self._check_notification_service() @mock.patch('ceilometer.publisher.test.TestPublisher') def test_notification_service_error_topic(self, fake_publisher_cls): fake_publisher_cls.return_value = self.publisher self.run_service(self.srv) notifier = messaging.get_notifier(self.transport, 'compute.vagrant-precise') notifier.error({}, 'compute.instance.error', TEST_NOTICE_PAYLOAD) start = time.time() while time.time() - start < 60: if len(self.publisher.events) >= self.expected_events: break self.assertEqual(self.expected_events, len(self.publisher.events)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2313566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_novaclient.py0000664000175000017500000002561115250267452023357 0ustar00zuulzuul# Copyright 2013-2014 eNovance # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures import novaclient from ceilometer import nova_client from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes class TestNovaClient(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.mock_get_session = self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_session')) self.nv = nova_client.Client(self.CONF) self.mock_get_flavor = self.useFixture(fixtures.MockPatchObject( self.nv.nova_client.flavors, 'get', side_effect=self.fake_flavors_get)) def setup_connection(self, **kwargs): """Override to also update self.nv's client references.""" super().setup_connection(**kwargs) self.nv.image_client = self.fake_conn def fake_flavors_get(self, *args, **kwargs): a = mock.MagicMock() a.id = args[0] if a.id == 1: a.name = 'm1.tiny' elif a.id == 2: a.name = 'm1.large' else: raise novaclient.exceptions.NotFound('foobar') return a @staticmethod def fake_servers_list(*args, **kwargs): a = mock.MagicMock() a.id = 42 a.flavor = {'id': 1} a.image = {'id': 1} b = mock.MagicMock() b.id = 43 b.flavor = {'id': 2} b.image = {'id': 2} return [a, b] def test_instance_get_all_by_host(self): with mock.patch.object(self.nv.nova_client.servers, 'list', side_effect=self.fake_servers_list): instances = self.nv.instance_get_all_by_host('foobar') self.assertEqual(2, len(instances)) self.assertEqual('m1.tiny', instances[0].flavor['name']) self.assertEqual('ubuntu-12.04-x86', instances[0].image['name']) self.assertEqual(11, instances[0].kernel_id) self.assertEqual(21, instances[0].ramdisk_id) @staticmethod def fake_servers_list_unknown_flavor(*args, **kwargs): a = mock.MagicMock() a.id = 42 a.flavor = {'id': 666} a.image = {'id': 1} return [a] def test_instance_get_all_by_host_unknown_flavor(self): with mock.patch.object( self.nv.nova_client.servers, 'list', side_effect=self.fake_servers_list_unknown_flavor): instances = self.nv.instance_get_all_by_host('foobar') self.assertEqual(1, len(instances)) self.assertEqual('unknown-id-666', instances[0].flavor['name']) @staticmethod def fake_servers_list_unknown_image(*args, **kwargs): a = mock.MagicMock() a.id = 42 a.flavor = {'id': 1} a.image = {'id': 666} return [a] @staticmethod def fake_servers_list_image_missing_metadata(*args, **kwargs): a = mock.MagicMock() a.id = 42 a.flavor = {'id': 1} a.image = {'id': args[0]} return [a] @staticmethod def fake_instance_image_missing(*args, **kwargs): a = mock.MagicMock() a.id = 42 a.flavor = {'id': 666} a.image = None return [a] def test_instance_get_all_by_host_unknown_image(self): with mock.patch.object( self.nv.nova_client.servers, 'list', side_effect=self.fake_servers_list_unknown_image): instances = self.nv.instance_get_all_by_host('foobar') self.assertEqual(1, len(instances)) self.assertEqual('unknown-id-666', instances[0].image['name']) def test_with_flavor_and_image(self): results = self.nv._with_flavor_and_image(self.fake_servers_list()) instance = results[0] self.assertEqual(2, len(results)) self.assertEqual('ubuntu-12.04-x86', instance.image['name']) self.assertEqual('m1.tiny', instance.flavor['name']) self.assertEqual(11, instance.kernel_id) self.assertEqual(21, instance.ramdisk_id) self.assertEqual({'base_image_ref': 1, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '1', 'min_ram': '0', 'os_distro': 'ubuntu', 'os_type': 'linux'}, instance.image_meta) def test_with_flavor_and_image_unknown_image(self): instances = self.fake_servers_list_unknown_image() results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertEqual('unknown-id-666', instance.image['name']) self.assertNotEqual(instance.flavor['name'], 'unknown-id-666') self.assertIsNone(instance.kernel_id) self.assertIsNone(instance.ramdisk_id) self.assertEqual({}, instance.image_meta) def test_with_flavor_and_image_unknown_flavor(self): instances = self.fake_servers_list_unknown_flavor() results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertEqual('unknown-id-666', instance.flavor['name']) self.assertEqual(0, instance.flavor['vcpus']) self.assertEqual(0, instance.flavor['ram']) self.assertEqual(0, instance.flavor['disk']) self.assertNotEqual(instance.image['name'], 'unknown-id-666') self.assertEqual(11, instance.kernel_id) self.assertEqual(21, instance.ramdisk_id) self.assertEqual({'base_image_ref': 1, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '1', 'min_ram': '0', 'os_distro': 'ubuntu', 'os_type': 'linux'}, instance.image_meta) def test_with_flavor_and_image_none_metadata(self): self.setup_connection(images=[fakes.IMAGE_MISSING_METADATA]) instances = self.fake_servers_list_image_missing_metadata(3) results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertIsNone(instance.kernel_id) self.assertIsNone(instance.ramdisk_id) self.assertEqual({'base_image_ref': 3, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '0', 'min_ram': '0'}, instance.image_meta) def test_with_flavor_and_image_missing_metadata(self): self.setup_connection(images=[fakes.IMAGE_MISSING_KERNEL_RAMDISK]) instances = self.fake_servers_list_image_missing_metadata(4) results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertIsNone(instance.kernel_id) self.assertIsNone(instance.ramdisk_id) self.assertEqual({'base_image_ref': 4, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '0', 'min_ram': '0'}, instance.image_meta) def test_with_flavor_and_image_missing_ramdisk(self): self.setup_connection(images=[fakes.IMAGE_MISSING_RAMDISK]) instances = self.fake_servers_list_image_missing_metadata(5) results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertEqual(11, instance.kernel_id) self.assertIsNone(instance.ramdisk_id) self.assertEqual({'base_image_ref': 5, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '0', 'min_ram': '0'}, instance.image_meta) def test_with_flavor_and_image_missing_kernel(self): self.setup_connection(images=[fakes.IMAGE_MISSING_KERNEL]) instances = self.fake_servers_list_image_missing_metadata(6) results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertIsNone(instance.kernel_id) self.assertEqual(21, instance.ramdisk_id) self.assertEqual({'base_image_ref': 6, 'container_format': 'bare', 'disk_format': 'qcow2', 'min_disk': '0', 'min_ram': '0'}, instance.image_meta) def test_with_flavor_and_image_no_cache(self): instances = self.fake_servers_list() results = self.nv._with_flavor_and_image(instances) self.assertEqual(2, len(results)) self.assertEqual(2, self.mock_get_flavor.mock.call_count) self.assertEqual(2, self.fake_conn.image.get_image.call_count) def test_with_flavor_and_image_cache(self): instances = self.fake_servers_list() * 2 results = self.nv._with_flavor_and_image(instances) self.assertEqual(4, len(results)) self.assertEqual(2, self.mock_get_flavor.mock.call_count) self.assertEqual(2, self.fake_conn.image.get_image.call_count) def test_with_flavor_and_image_unknown_image_cache(self): instances = self.fake_servers_list_unknown_image() * 2 results = self.nv._with_flavor_and_image(instances) self.assertEqual(2, len(results)) self.assertEqual(1, self.mock_get_flavor.mock.call_count) self.assertEqual(1, self.fake_conn.image.get_image.call_count) for instance in results: self.assertEqual('unknown-id-666', instance.image['name']) self.assertNotEqual(instance.flavor['name'], 'unknown-id-666') self.assertIsNone(instance.kernel_id) self.assertIsNone(instance.ramdisk_id) self.assertEqual({}, instance.image_meta) def test_with_image_tags_excluded(self): instances = self.fake_servers_list_image_missing_metadata( fakes.IMAGE_AMPHORA.id) results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertNotIn('tags', instance.image_meta) def test_with_missing_image_instance(self): instances = self.fake_instance_image_missing() results = self.nv._with_flavor_and_image(instances) instance = results[0] self.assertIsNone(instance.kernel_id) self.assertIsNone(instance.image) self.assertIsNone(instance.ramdisk_id) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2313566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_polling.py0000664000175000017500000000707715250267452022667 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.polling import manager from ceilometer import service from ceilometer.tests import base class PollingTestCase(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.poll_cfg = {'sources': [{'name': 'test_source', 'interval': 600, 'meters': ['a']}]} def _build_and_set_new_polling(self): name = self.cfg2file(self.poll_cfg) self.CONF.set_override('cfg_file', name, group='polling') def test_no_name(self): del self.poll_cfg['sources'][0]['name'] self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_no_interval(self): del self.poll_cfg['sources'][0]['interval'] self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_invalid_string_interval(self): self.poll_cfg['sources'][0]['interval'] = 'string' self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_get_interval(self): self._build_and_set_new_polling() poll_manager = manager.PollingManager(self.CONF) source = poll_manager.sources[0] self.assertEqual(600, source.get_interval()) def test_invalid_resources(self): self.poll_cfg['sources'][0]['resources'] = {'invalid': 1} self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_resources(self): resources = ['test1://', 'test2://'] self.poll_cfg['sources'][0]['resources'] = resources self._build_and_set_new_polling() poll_manager = manager.PollingManager(self.CONF) self.assertEqual(resources, poll_manager.sources[0].resources) def test_no_resources(self): self._build_and_set_new_polling() poll_manager = manager.PollingManager(self.CONF) self.assertEqual(0, len(poll_manager.sources[0].resources)) def test_check_meters_include_exclude_same(self): self.poll_cfg['sources'][0]['meters'] = ['a', '!a'] self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_check_meters_include_exclude(self): self.poll_cfg['sources'][0]['meters'] = ['a', '!b'] self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) def test_check_meters_wildcard_included(self): self.poll_cfg['sources'][0]['meters'] = ['a', '*'] self._build_and_set_new_polling() self.assertRaises(manager.PollingException, manager.PollingManager, self.CONF) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2313566 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_prom_exporter.py0000664000175000017500000005407615250267452024131 0ustar00zuulzuul# # Copyright 2022 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/polling/prom_exporter.py""" from unittest import mock from unittest.mock import call from ceilometer.polling import manager from ceilometer.polling import prom_exporter from ceilometer import service from ceilometer.tests import base COUNTER_SOURCE = 'testsource' class TestPromExporter(base.BaseTestCase): test_disk_latency = [ { 'source': 'openstack', 'counter_name': 'disk.device.read.latency', 'counter_type': 'cumulative', 'counter_unit': 'ns', 'counter_volume': 132128682, 'user_id': '6e7d71415cd5401cbe103829c9c5dec2', 'user_name': None, 'project_id': 'd965489b7f894cbda89cd2e25bfd85a0', 'project_name': None, 'resource_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda', 'timestamp': '2024-06-20T09:32:36.521082', 'resource_metadata': { 'display_name': 'myserver', 'name': 'instance-00000002', 'instance_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'instance_type': 'tiny', 'host': 'e0d297f5df3b62ec73c8d42b', 'instance_host': 'devstack', 'flavor': { 'id': '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'name': 'tiny', 'vcpus': 1, 'ram': 512, 'disk': 1, 'ephemeral': 0, 'swap': 0 }, 'status': 'active', 'state': 'running', 'task_state': '', 'image': { 'id': '71860ed5-f66d-43e0-9514-f1d188106284' }, 'image_ref': '71860ed5-f66d-43e0-9514-f1d188106284', 'image_ref_url': None, 'architecture': 'x86_64', 'os_type': 'hvm', 'vcpus': 1, 'memory_mb': 512, 'disk_gb': 1, 'ephemeral_gb': 0, 'root_gb': 1, 'disk_name': 'vda', 'user_metadata': { 'custom_label': 'custom value' } }, 'message_id': '078029c7-2ee8-11ef-a915-bd45e2085de3', 'monotonic_time': 1819980.112406547, 'message_signature': 'f8d9a411b0cd0cb0d34e83' }, { 'source': 'openstack', 'counter_name': 'disk.device.read.latency', 'counter_type': 'cumulative', 'counter_unit': 'ns', 'counter_volume': 232128754, 'user_id': '6e7d71415cd5401cbe103829c9c5dec2', 'user_name': None, 'project_id': 'd965489b7f894cbda89cd2e25bfd85a0', 'project_name': None, 'resource_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda', 'timestamp': '2024-06-20T09:32:46.521082', 'resource_metadata': { 'display_name': 'myserver', 'name': 'instance-00000002', 'instance_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'instance_type': 'tiny', 'host': 'e0d297f5df3b62ec73c8d42b', 'instance_host': 'devstack', 'flavor': { 'id': '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'name': 'tiny', 'vcpus': 1, 'ram': 512, 'disk': 1, 'ephemeral': 0, 'swap': 0 }, 'status': 'active', 'state': 'running', 'task_state': '', 'image': { 'id': '71860ed5-f66d-43e0-9514-f1d188106284' }, 'image_ref': '71860ed5-f66d-43e0-9514-f1d188106284', 'image_ref_url': None, 'architecture': 'x86_64', 'os_type': 'hvm', 'vcpus': 1, 'memory_mb': 512, 'disk_gb': 1, 'ephemeral_gb': 0, 'root_gb': 1, 'disk_name': 'vda', 'user_metadata': { 'custom_label': 'custom value' } }, 'message_id': '078029c7-2ee8-11ef-a915-bd45e2085de4', 'monotonic_time': 1819990.112406547, 'message_signature': 'f8d9a411b0cd0cb0d34e84' } ] test_memory_usage = [ { 'source': 'openstack', 'counter_name': 'memory.usage', 'counter_type': 'gauge', 'counter_unit': 'MiB', 'counter_volume': 37.98046875, 'user_id': '6e7d71415cd5401cbe103829c9c5dec2', 'user_name': None, 'project_id': 'd965489b7f894cbda89cd2e25bfd85a0', 'project_name': None, 'resource_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'timestamp': '2024-06-20T09:32:36.515823', 'resource_metadata': { 'display_name': 'myserver', 'name': 'instance-00000002', 'instance_id': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'instance_type': 'tiny', 'host': 'e0d297f5df3b62ec73c8d42b', 'instance_host': 'devstack', 'flavor': { 'id': '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'name': 'tiny', 'vcpus': 1, 'ram': 512, 'disk': 1, 'ephemeral': 0, 'swap': 0 }, 'status': 'active', 'state': 'running', 'task_state': '', 'image': { 'id': '71860ed5-f66d-43e0-9514-f1d188106284' }, 'image_ref': '71860ed5-f66d-43e0-9514-f1d188106284', 'image_ref_url': None, 'architecture': 'x86_64', 'os_type': 'hvm', 'vcpus': 1, 'memory_mb': 512, 'disk_gb': 1, 'ephemeral_gb': 0, 'root_gb': 1 }, 'message_id': '078029bf-2ee8-11ef-a915-bd45e2085de3', 'monotonic_time': 1819980.131767362, 'message_signature': 'f8d9a411b0cd0cb0d34e83' } ] test_image_size = [ { 'source': 'openstack', 'counter_name': 'image.size', 'counter_type': 'gauge', 'counter_unit': 'B', 'counter_volume': 16344576, 'user_id': None, 'user_name': None, 'project_id': 'd965489b7f894cbda89cd2e25bfd85a0', 'project_name': None, 'resource_id': 'f9276c96-8a12-432b-96a1-559d70715f97', 'timestamp': '2024-06-20T09:40:17.118871', 'resource_metadata': { 'status': 'active', 'visibility': 'public', 'name': 'cirros2', 'container_format': 'bare', 'created_at': '2024-05-30T11:38:52Z', 'disk_format': 'qcow2', 'updated_at': '2024-05-30T11:38:52Z', 'min_disk': 0, 'protected': False, 'checksum': '7734eb3945297adc90ddc6cebe8bb082', 'min_ram': 0, 'tags': [], 'virtual_size': 117440512, 'user_metadata': { 'server_group': 'server_group123' } }, 'message_id': '19f8f78a-2ee9-11ef-a95f-bd45e2085de3', 'monotonic_time': None, 'message_signature': 'f8d9a411b0cd0cb0d34e83' } ] test_image_size_queued = [ { 'source': 'openstack', 'counter_name': 'image.size', 'counter_type': 'gauge', 'counter_unit': 'B', 'counter_volume': None, 'user_id': None, 'user_name': None, 'project_id': 'abd178778aa64cba96e1397475a2c0ed', 'project_name': None, 'resource_id': '62bd7745-db96-4274-9966-405d314e364b', 'timestamp': '2026-07-15T20:01:56.000000', 'resource_metadata': { 'status': 'queued', 'visibility': 'shared', 'name': 'cirros_queued_image', 'container_format': 'bare', 'created_at': '2026-07-15T20:01:24Z', 'disk_format': 'qcow2', 'updated_at': '2026-07-15T20:01:56Z', 'min_disk': 1, 'is_protected': False, 'checksum': None, 'min_ram': 1, 'tags': [], 'virtual_size': None }, 'message_id': '19f8f78a-2ee9-11ef-a95f-bd45e2085de4', 'monotonic_time': None, 'message_signature': 'f8d9a411b0cd0cb0d34e83' }, { 'source': 'openstack', 'counter_name': 'image.size', 'counter_type': 'gauge', 'counter_unit': 'B', 'counter_volume': 21692416, 'user_id': None, 'user_name': None, 'project_id': 'abd178778aa64cba96e1397475a2c0ed', 'project_name': None, 'resource_id': '73278fe8-de27-43ca-956e-f450b2d14bad', 'timestamp': '2026-07-13T21:43:17.000000', 'resource_metadata': { 'status': 'active', 'visibility': 'shared', 'name': 'cirros', 'container_format': 'bare', 'created_at': '2026-07-13T21:43:16Z', 'disk_format': 'qcow2', 'updated_at': '2026-07-13T21:43:17Z', 'min_disk': 0, 'is_protected': False, 'checksum': '87617e24a5e30cb3b87fda8c0764838f', 'min_ram': 0, 'tags': [], 'virtual_size': 117440512 }, 'message_id': '19f8f78a-2ee9-11ef-a95f-bd45e2085de5', 'monotonic_time': None, 'message_signature': 'f8d9a411b0cd0cb0d34e83' } ] @mock.patch('ceilometer.polling.prom_exporter.export') def test_prom_disabled(self, export): CONF = service.prepare_service([], []) manager.AgentManager(0, CONF) export.assert_not_called() @mock.patch('ceilometer.polling.prom_exporter.export') def test_export_called(self, export): CONF = service.prepare_service([], []) CONF.set_override('enable_prometheus_exporter', True, group='polling') CONF.set_override('prometheus_listen_addresses', [ '127.0.0.1:9101', '127.0.0.1:9102', '[::1]:9103', 'localhost:9104', ], group='polling') manager.AgentManager(0, CONF) export.assert_has_calls([ call('127.0.0.1', 9101, None, None), call('127.0.0.1', 9102, None, None), call('::1', 9103, None, None), call('localhost', 9104, None, None), ]) @mock.patch('ceilometer.polling.prom_exporter.export') def test_export_called_tls_disabled(self, export): CONF = service.prepare_service([], []) CONF.set_override('enable_prometheus_exporter', True, group='polling') CONF.set_override('prometheus_tls_enable', False, group='polling') CONF.set_override('prometheus_tls_certfile', "cert.pem", group='polling') CONF.set_override('prometheus_listen_addresses', [ '127.0.0.1:9101', '127.0.0.1:9102', '[::1]:9103', 'localhost:9104', ], group='polling') manager.AgentManager(0, CONF) export.assert_has_calls([ call('127.0.0.1', 9101, None, None), call('127.0.0.1', 9102, None, None), call('::1', 9103, None, None), call('localhost', 9104, None, None), ]) @mock.patch('ceilometer.polling.prom_exporter.export') def test_export_called_with_tls(self, export): CONF = service.prepare_service([], []) CONF.set_override('enable_prometheus_exporter', True, group='polling') CONF.set_override('prometheus_listen_addresses', [ '127.0.0.1:9101', '127.0.0.1:9102', '[::1]:9103', 'localhost:9104', ], group='polling') CONF.set_override('prometheus_tls_enable', True, group='polling') CONF.set_override('prometheus_tls_certfile', "cert.pem", group='polling') CONF.set_override('prometheus_tls_keyfile', "key.pem", group='polling') manager.AgentManager(0, CONF) export.assert_has_calls([ call('127.0.0.1', 9101, "cert.pem", "key.pem"), call('127.0.0.1', 9102, "cert.pem", "key.pem"), call('::1', 9103, "cert.pem", "key.pem"), call('localhost', 9104, "cert.pem", "key.pem"), ]) @mock.patch('ceilometer.polling.prom_exporter.export') def test_export_fails_if_incomplete_tls(self, export): CONF = service.prepare_service([], []) CONF.set_override('enable_prometheus_exporter', True, group='polling') CONF.set_override('prometheus_listen_addresses', ['127.0.0.1:9101'], group='polling') CONF.set_override('prometheus_tls_enable', True, group='polling') CONF.set_override('prometheus_tls_certfile', "cert.pem", group='polling') # prometheus_tls_keyfile defaults to None (missing key) self.assertRaises(ValueError, manager.AgentManager, 0, CONF) def test_collect_metrics(self): prom_exporter.collect_metrics(self.test_image_size) sample_dict_1 = {'counter': 'image.size', 'image': 'f9276c96-8a12-432b-96a1-559d70715f97', 'project': 'd965489b7f894cbda89cd2e25bfd85a0', 'publisher': 'ceilometer', 'resource': 'f9276c96-8a12-432b-96a1-559d70715f97', 'resource_name': 'cirros2', 'type': 'size', 'unit': 'B', 'server_group': 'server_group123'} self.assertEqual(16344576, prom_exporter.CEILOMETER_REGISTRY. get_sample_value('ceilometer_image_size', sample_dict_1)) prom_exporter.collect_metrics(self.test_memory_usage) sample_dict_2 = {'counter': 'memory.usage', 'memory': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'project': 'd965489b7f894cbda89cd2e25bfd85a0', 'publisher': 'ceilometer', 'resource': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'resource_name': 'myserver:instance-00000002', 'type': 'usage', 'unit': 'MiB', 'user': '6e7d71415cd5401cbe103829c9c5dec2', 'vm_instance': 'e0d297f5df3b62ec73c8d42b', 'server_group': 'none', 'flavor_id': '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'flavor_name': 'tiny'} self.assertEqual(37.98046875, prom_exporter.CEILOMETER_REGISTRY. get_sample_value('ceilometer_memory_usage', sample_dict_2)) prom_exporter.collect_metrics(self.test_disk_latency) sample_dict_3 = {'counter': 'disk.device.read.latency', 'disk': 'read', 'project': 'd965489b7f894cbda89cd2e25bfd85a0', 'publisher': 'ceilometer', 'resource': 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda', 'resource_name': 'myserver:instance-00000002', 'type': 'device', 'unit': 'ns', 'user': '6e7d71415cd5401cbe103829c9c5dec2', 'vm_instance': 'e0d297f5df3b62ec73c8d42b', 'server_group': 'none', 'flavor_id': '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'flavor_name': 'tiny'} # The value has to be of the second sample, as this is now a Gauge self.assertEqual(232128754, prom_exporter.CEILOMETER_REGISTRY. get_sample_value( 'ceilometer_disk_device_read_latency', sample_dict_3)) @mock.patch.object(prom_exporter.LOG, 'warning') def test_collect_metrics_null_counter_volume(self, mock_warning): prom_exporter.collect_metrics(self.test_image_size_queued) image_size_series = {} for metric in prom_exporter.CEILOMETER_REGISTRY.collect(): if metric.name == 'ceilometer_image_size': for sample in metric.samples: resource_id = sample.labels['resource'] image_size_series[resource_id] = sample.value queued_image_id = '62bd7745-db96-4274-9966-405d314e364b' self.assertNotIn(queued_image_id, image_size_series) active_image_id = '73278fe8-de27-43ca-956e-f450b2d14bad' self.assertIn(active_image_id, image_size_series) self.assertEqual( 21692416, image_size_series[active_image_id]) mock_warning.assert_called_once_with( "Skipping sample for metric %s with no " "counter_volume (resource: %s).", 'ceilometer_image_size', queued_image_id) def test_gen_labels(self): slabels1 = dict(keys=[], values=[]) slabels1['keys'] = ['disk', 'publisher', 'type', 'counter', 'project', 'user', 'unit', 'resource', 'vm_instance', 'resource_name', 'server_group', 'flavor_id', 'flavor_name'] slabels1['values'] = ['read', 'ceilometer', 'device', 'disk.device.read.latency', 'd965489b7f894cbda89cd2e25bfd85a0', '6e7d71415cd5401cbe103829c9c5dec2', 'ns', 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63-vda', 'e0d297f5df3b62ec73c8d42b', 'myserver:instance-00000002', 'none', '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'tiny'] label1 = prom_exporter._gen_labels(self.test_disk_latency[0]) self.assertDictEqual(label1, slabels1) slabels2 = dict(keys=[], values=[]) slabels2['keys'] = ['memory', 'publisher', 'type', 'counter', 'project', 'user', 'unit', 'resource', 'vm_instance', 'resource_name', 'server_group', 'flavor_id', 'flavor_name'] slabels2['values'] = ['e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'ceilometer', 'usage', 'memory.usage', 'd965489b7f894cbda89cd2e25bfd85a0', '6e7d71415cd5401cbe103829c9c5dec2', 'MiB', 'e536fff6-b20d-4aa5-ac2f-d15ac8b3af63', 'e0d297f5df3b62ec73c8d42b', 'myserver:instance-00000002', 'none', '4af9ac72-5787-4f86-8644-0faa87ce7c83', 'tiny'] label2 = prom_exporter._gen_labels(self.test_memory_usage[0]) self.assertDictEqual(label2, slabels2) slabels3 = dict(keys=[], values=[]) slabels3['keys'] = ['image', 'publisher', 'type', 'counter', 'project', 'unit', 'resource', 'resource_name', 'server_group'] slabels3['values'] = ['f9276c96-8a12-432b-96a1-559d70715f97', 'ceilometer', 'size', 'image.size', 'd965489b7f894cbda89cd2e25bfd85a0', 'B', 'f9276c96-8a12-432b-96a1-559d70715f97', 'cirros2', 'server_group123'] label3 = prom_exporter._gen_labels(self.test_image_size[0]) self.assertDictEqual(label3, slabels3) @mock.patch.object(prom_exporter.CEILOMETER_REGISTRY, 'unregister') def test_purge_stale_metrics_existing_metric(self, mock_unregister): mock_metric = mock.MagicMock() prom_exporter.CEILOMETER_REGISTRY._names_to_collectors = { 'ceilometer_test_metric': mock_metric } prom_exporter.purge_stale_metrics('test.metric') mock_unregister.assert_called_once_with(mock_metric) @mock.patch.object(prom_exporter.CEILOMETER_REGISTRY, 'unregister') def test_purge_stale_metrics_no_existing_metric(self, mock_unregister): prom_exporter.CEILOMETER_REGISTRY._names_to_collectors = {} prom_exporter.purge_stale_metrics('nonexistent.metric') mock_unregister.assert_not_called() @mock.patch.object(prom_exporter.CEILOMETER_REGISTRY, 'unregister') def test_purge_stale_metrics_name_transformation(self, mock_unregister): mock_metric = mock.MagicMock() prom_exporter.CEILOMETER_REGISTRY._names_to_collectors = { 'ceilometer_cpu_util': mock_metric } prom_exporter.purge_stale_metrics('cpu.util') mock_unregister.assert_called_once_with(mock_metric) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2323568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/test_sample.py0000664000175000017500000001077615250267452022504 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests for ceilometer/sample.py""" import datetime from ceilometer import sample from ceilometer.tests import base class TestSample(base.BaseTestCase): SAMPLE = sample.Sample( name='cpu', type=sample.TYPE_CUMULATIVE, unit='ns', volume='1234567', user_id='56c5692032f34041900342503fecab30', project_id='ac9494df2d9d4e709bac378cceabaf23', resource_id='1ca738a1-c49c-4401-8346-5c60ebdb03f4', timestamp=datetime.datetime(2014, 10, 29, 14, 12, 15, 485877), resource_metadata={} ) def test_sample_string_format(self): expected = ('') self.assertEqual(expected, str(self.SAMPLE)) def test_sample_invalid_type(self): self.assertRaises( ValueError, sample.Sample, name='cpu', type='invalid', unit='ns', volume='1234567', user_id='56c5692032f34041900342503fecab30', project_id='ac9494df2d9d4e709bac378cceabaf23', resource_id='1ca738a1-c49c-4401-8346-5c60ebdb03f4', timestamp=datetime.datetime(2014, 10, 29, 14, 12, 15, 485877), resource_metadata={} ) def test_sample_from_notifications_list(self): msg = { 'event_type': 'sample.create', 'metadata': { 'timestamp': '2015-06-19T09:19:35.786893', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': [{'counter_name': 'instance100'}], 'priority': 'info', 'publisher_id': 'ceilometer.api', } s = sample.Sample.from_notification( 'sample', sample.TYPE_GAUGE, 1.0, '%', 'user', 'project', 'res', msg) expected = {'event_type': msg['event_type'], 'host': msg['publisher_id']} self.assertEqual(expected, s.resource_metadata) def test_sample_from_notifications_dict(self): msg = { 'event_type': 'sample.create', 'metadata': { 'timestamp': '2015-06-19T09:19:35.786893', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': {'counter_name': 'instance100'}, 'priority': 'info', 'publisher_id': 'ceilometer.api', } s = sample.Sample.from_notification( 'sample', sample.TYPE_GAUGE, 1.0, '%', 'user', 'project', 'res', msg) msg['payload']['event_type'] = msg['event_type'] msg['payload']['host'] = msg['publisher_id'] self.assertEqual(msg['payload'], s.resource_metadata) def test_sample_from_notifications_assume_utc(self): msg = { 'event_type': 'sample.create', 'metadata': { 'timestamp': '2015-06-19T09:19:35.786893', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': {'counter_name': 'instance100'}, 'priority': 'info', 'publisher_id': 'ceilometer.api', } s = sample.Sample.from_notification( 'sample', sample.TYPE_GAUGE, 1.0, '%', 'user', 'project', 'res', msg) self.assertEqual('2015-06-19T09:19:35.786893+00:00', s.timestamp) def test_sample_from_notifications_keep_tz(self): msg = { 'event_type': 'sample.create', 'metadata': { 'timestamp': '2015-06-19T09:19:35.786893+01:00', 'message_id': '939823de-c242-45a2-a399-083f4d6a8c3e'}, 'payload': {'counter_name': 'instance100'}, 'priority': 'info', 'publisher_id': 'ceilometer.api', } s = sample.Sample.from_notification( 'sample', sample.TYPE_GAUGE, 1.0, '%', 'user', 'project', 'res', msg) self.assertEqual('2015-06-19T09:19:35.786893+01:00', s.timestamp) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2751276 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/volume/0000775000175000017500000000000015250267567021115 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2323568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/volume/__init__.py0000664000175000017500000000000015250267452023205 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2323568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/volume/test_cinder.py0000664000175000017500000005015615250267452023772 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer.polling import manager from ceilometer import service import ceilometer.tests.base as base from ceilometer.tests.unit import fakes from ceilometer.volume import cinder class TestVolumeSizePollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeSizePollster(conf) def test_volume_size_pollster(self): volume_size_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.VOLUME_LIST)) self.assertEqual(len(fakes.VOLUME_LIST), len(volume_size_samples)) self.assertEqual('volume.size', volume_size_samples[0].name) self.assertEqual(1, volume_size_samples[0].volume) self.assertEqual('6824974c08974d4db864bbaa6bc08303', volume_size_samples[0].project_id) self.assertEqual('d94c18fb-b680-4912-9741-da69ee83c94f', volume_size_samples[0].resource_id) self.assertEqual('f0019ee3-523c-45ab-b0b6-3adc529673e7', volume_size_samples[0].resource_metadata["image_id"]) self.assertEqual('1ae69721-d071-4156-a2bd-b11bb43ec2e3', volume_size_samples[0].resource_metadata ["instance_id"]) self.assertEqual('nova', volume_size_samples[0].resource_metadata ["availability_zone"]) def test_get_samples_volume_metadata_contains_expected_fields(self): """Test that the expected metadata field exist in the sample. Publishers and operators may expect certain metadata to exist. """ samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.VOLUME_LIST)) metadata = samples[0].resource_metadata self.assertIn('name', metadata) self.assertIn('status', metadata) self.assertIn('volume_type', metadata) self.assertIn('volume_type_id', metadata) self.assertIn('availability_zone', metadata) self.assertIn('os-vol-host-attr:host', metadata) self.assertIn('attachments', metadata) self.assertIn('snapshot_id', metadata) self.assertIn('source_volid', metadata) self.assertIn('image_id', metadata) self.assertIn('instance_id', metadata) def test_volume_metadata_contains_remapped_fields(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.VOLUME_LIST)) metadata = samples[0].resource_metadata self.assertIn('os-vol-host-attr:host', metadata) self.assertIn('source_volid', metadata) class TestVolumeSnapshotSizePollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeSnapshotSize(conf) def test_volume_snapshot_size_pollster(self): volume_snapshot_size_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.SNAPSHOT_LIST)) self.assertEqual(1, len(volume_snapshot_size_samples)) self.assertEqual('volume.snapshot.size', volume_snapshot_size_samples[0].name) self.assertEqual(1, volume_snapshot_size_samples[0].volume) self.assertEqual('be255bd31eb944578000fc762fde6dcf', volume_snapshot_size_samples[0].user_id) self.assertEqual('6824974c08974d4db864bbaa6bc08303', volume_snapshot_size_samples[0].project_id) self.assertEqual('b1ea6783-f952-491e-a4ed-23a6a562e1cf', volume_snapshot_size_samples[0].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.SNAPSHOT_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('name', metadata) self.assertIn('volume_id', metadata) self.assertIn('status', metadata) self.assertIn('description', metadata) self.assertIn('metadata', metadata) self.assertIn('os-extended-snapshot-attributes:progress', metadata) class TestVolumeBackupSizePollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeBackupSize(conf) def test_volume_backup_size_pollster(self): volume_backup_size_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.BACKUP_LIST)) self.assertEqual(1, len(volume_backup_size_samples)) self.assertEqual('volume.backup.size', volume_backup_size_samples[0].name) self.assertEqual(1, volume_backup_size_samples[0].volume) self.assertEqual('75a52125-85ff-4a8d-b2aa-580f3b22273f', volume_backup_size_samples[0].resource_id) self.assertEqual('6824974c08974d4db864bbaa6bc08303', volume_backup_size_samples[0].project_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.BACKUP_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('name', metadata) self.assertIn('is_incremental', metadata) self.assertIn('object_count', metadata) self.assertIn('container', metadata) self.assertIn('volume_id', metadata) self.assertIn('status', metadata) self.assertIn('description', metadata) class TestVolumeProviderPoolCapacityTotalPollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeProviderPoolCapacityTotal(conf) def test_volume_provider_pool_capacity_total_pollster(self): volume_pool_size_total_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) self.assertEqual(len(fakes.POOL_LIST), len(volume_pool_size_total_samples)) self.assertEqual('volume.provider.pool.capacity.total', volume_pool_size_total_samples[0].name) self.assertEqual(28.5, volume_pool_size_total_samples[0].volume) self.assertEqual('localhost.localdomain@lvmdriver-1#lvmdriver-1', volume_pool_size_total_samples[0].resource_id) self.assertEqual('volume.provider.pool.capacity.total', volume_pool_size_total_samples[1].name) self.assertEqual(85.0, volume_pool_size_total_samples[1].volume) self.assertEqual('cinder-3ceee-volume-ceph-0@ceph#ceph', volume_pool_size_total_samples[1].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('pool_name', metadata) self.assertEqual(s.resource_id.split('#')[0], metadata['provider']) def test_get_samples_no_capacity_in_capability_field(self): self.assertRaises( AttributeError, list, self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_CAPABILITIES]) ) class TestVolumeProviderPoolCapacityFreePollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeProviderPoolCapacityFree(conf) def test_volume_provider_pool_capacity_free_pollster(self): volume_pool_size_free_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) self.assertEqual(len(fakes.POOL_LIST), len(volume_pool_size_free_samples)) self.assertEqual('volume.provider.pool.capacity.free', volume_pool_size_free_samples[0].name) self.assertEqual(28.39, volume_pool_size_free_samples[0].volume) self.assertEqual('localhost.localdomain@lvmdriver-1#lvmdriver-1', volume_pool_size_free_samples[0].resource_id) self.assertEqual('volume.provider.pool.capacity.free', volume_pool_size_free_samples[1].name) self.assertEqual(85.0, volume_pool_size_free_samples[1].volume) self.assertEqual('cinder-3ceee-volume-ceph-0@ceph#ceph', volume_pool_size_free_samples[1].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('pool_name', metadata) self.assertEqual(s.resource_id.split('#')[0], metadata['provider']) def test_get_samples_no_free_capacity_gb(self): """Test behaviour when required attribute is not present. The free_capacity_gb attribute is needed to populate the volume of the sample. """ # No pool.free_capacity_gb i.e. the volume for the sample. self.assertRaises( AttributeError, list, self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_CAPABILITIES]) ) class TestVolumeProviderPoolCapacityProvisionedPollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeProviderPoolCapacityProvisioned(conf) def test_volume_provider_pool_capacity_provisioned_pollster(self): volume_pool_size_provisioned_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) self.assertEqual(1, len(volume_pool_size_provisioned_samples)) self.assertEqual('volume.provider.pool.capacity.provisioned', volume_pool_size_provisioned_samples[0].name) self.assertEqual(4.0, volume_pool_size_provisioned_samples[0].volume) self.assertEqual('localhost.localdomain@lvmdriver-1#lvmdriver-1', volume_pool_size_provisioned_samples[0].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('pool_name', metadata) self.assertEqual(s.resource_id.split('#')[0], metadata['provider']) class TestVolumeProviderPoolCapacityVirtualFreePollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeProviderPoolCapacityVirtualFree(conf) def test_volume_provider_pool_capacity_virtual_free_pollster(self): volume_pool_size_virtual_free_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) self.assertEqual(1, len(volume_pool_size_virtual_free_samples)) self.assertEqual('volume.provider.pool.capacity.virtual_free', volume_pool_size_virtual_free_samples[0].name) self.assertEqual(566.0, volume_pool_size_virtual_free_samples[0].volume) self.assertEqual('localhost.localdomain@lvmdriver-1#lvmdriver-1', volume_pool_size_virtual_free_samples[0].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('pool_name', metadata) self.assertEqual(s.resource_id.split('#')[0], metadata['provider']) def test_get_samples_missing_provisioned_capacity_skips_pool(self): """Verify pools without provisioned_capacity_gb are silently skipped. The pollster uses getattr(pool, 'provisioned_capacity_gb', None) as a guard, so pools missing this attribute yield no samples. """ samples = list(self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_PROVISIONED_CAPACITY])) self.assertEqual(0, len(samples)) def test_get_samples_missing_thin_provisioning_raises_err(self): """Verify missing thin_provisioning_support raises KeyError. When provisioned_capacity_gb IS set but thin_provisioning_support is missing, the pollster attempts to access the attribute and raises KeyError. This documents current behavior. """ self.assertRaises( KeyError, list, self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_THIN_PROVISIONING])) def test_get_samples_thick_provisioning_uses_ratio_one(self): """Verify thin_provisioning_support=False uses ratio 1.0. When thin_provisioning_support is False, max_over_subscription_ratio defaults to 1.0, resulting in volume = 1.0 * (28.5 - 0) - 4.0 = 24.5. """ samples = list(self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_THICK_PROVISIONING])) self.assertEqual(1, len(samples)) self.assertEqual(24.5, samples[0].volume) def test_get_samples_missing_reserved_percentage(self): """Verify the behaviour when reserved_percentage is not set. Setup: provisioned_capacity_gb set, but reserved_percentage missing Expected behaviour: there is an error """ self.assertRaises(KeyError, list, self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_RESERVED_PERCENTAGE])) class TestVolumeProviderPoolCapacityAllocatedPollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeProviderPoolCapacityAllocated(conf) def test_volume_provider_pool_capacity_allocated_pollster(self): volume_pool_size_allocated_samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) self.assertEqual(len(fakes.POOL_LIST), len(volume_pool_size_allocated_samples)) self.assertEqual('volume.provider.pool.capacity.allocated', volume_pool_size_allocated_samples[0].name) self.assertEqual(4, volume_pool_size_allocated_samples[0].volume) self.assertEqual('localhost.localdomain@lvmdriver-1#lvmdriver-1', volume_pool_size_allocated_samples[0].resource_id) self.assertEqual('volume.provider.pool.capacity.allocated', volume_pool_size_allocated_samples[1].name) self.assertEqual(1, volume_pool_size_allocated_samples[1].volume) self.assertEqual('cinder-3ceee-volume-ceph-0@ceph#ceph', volume_pool_size_allocated_samples[1].resource_id) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.POOL_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('pool_name', metadata) self.assertEqual(s.resource_id.split('#')[0], metadata['provider']) def test_get_samples_zero_allocated_cap_emits_sample(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_ZERO_ALLOCATED_CAPACITY])) self.assertEqual(1, len(samples)) self.assertEqual(0, samples[0].volume) def test_get_samples_missing_allocated_cap_raises_err(self): self.assertRaises( KeyError, list, self.pollster.get_samples( self.manager, {}, resources=[fakes.POOL_NO_CAPABILITIES])) class TestVolumeServiceHealthPollster(base.BaseTestCase): def setUp(self): super().setUp() conf = service.prepare_service([], []) self.manager = manager.AgentManager(0, conf) self.pollster = cinder.VolumeServiceHealthPollster(conf) def test_volume_service_health_pollster(self): samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.SERVICE_LIST)) self.assertEqual(len(fakes.SERVICE_LIST), len(samples)) self.assertEqual('volume.service.health', samples[0].name) self.assertEqual(1, samples[0].volume) self.assertEqual('cinder-volume@devstack', samples[0].resource_id) self.assertEqual('cinder-volume', samples[0].resource_metadata['binary']) self.assertEqual('nova', samples[0].resource_metadata['zone']) self.assertEqual('volume.service.health', samples[1].name) self.assertEqual(1, samples[1].volume) self.assertEqual('cinder-scheduler@devstack', samples[1].resource_id) self.assertEqual('cinder-scheduler', samples[1].resource_metadata['binary']) self.assertEqual('volume.service.health', samples[2].name) self.assertEqual(0, samples[2].volume) self.assertEqual('cinder-backup@devstack', samples[2].resource_id) self.assertEqual('cinder-backup', samples[2].resource_metadata['binary']) def test_get_samples_contains_expected_metadata(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=fakes.SERVICE_LIST)) for s in samples: metadata = s.resource_metadata self.assertIsNotNone(metadata) self.assertIn('binary', metadata) self.assertIn('host', metadata) self.assertIn('zone', metadata) self.assertIn('status', metadata) def test_zone_populated_from_availability_zone(self): samples = list( self.pollster.get_samples( self.manager, {}, resources=fakes.SERVICE_LIST)) self.assertIn('zone', samples[0].resource_metadata) self.assertNotIn('availability_zone', samples[0].resource_metadata) self.assertEqual('nova', samples[0].resource_metadata['zone']) def test_volume_service_health_unknown_state(self): self.assertRaises( ValueError, list, self.pollster.get_samples( self.manager, {}, resources=[ fakes.SERVICE_CINDER_UNKNOWN_STATUS])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2323568 ceilometer-27.0.0.0rc1/ceilometer/tests/unit/volume/test_discovery.py0000664000175000017500000001663715250267452024543 0ustar00zuulzuul# Copyright (C) 2026 Red Hat # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import fixtures from openstack import exceptions as os_exceptions from ceilometer import service from ceilometer.tests import base from ceilometer.tests.unit import fakes from ceilometer.volume import discovery class _BaseDiscoveryTestCase(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.useFixture( fixtures.MockPatch('ceilometer.keystone_client.get_session')) self.manager = mock.Mock() class TestVolumeDiscovery(_BaseDiscoveryTestCase): def setUp(self): super().setUp() self.discovery = discovery.VolumeDiscovery(self.CONF) def test_keystone_required_for_cinder_service(self): self.assertEqual( 'cinder', self.discovery.KEYSTONE_REQUIRED_FOR_SERVICE) def test_discover_returns_volumes(self): resources = self.discovery.discover(self.manager) self.assertEqual(fakes.VOLUME_LIST, resources) def test_discover_empty(self): self.setup_connection(volumes=[]) self.discovery = discovery.VolumeDiscovery(self.CONF) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_calls_list_volumes_with_all_projects(self): result = self.discovery.discover(self.manager) self.fake_conn.block_storage.volumes.assert_called_once_with( details=True, all_projects=True) self.assertEqual(fakes.VOLUME_LIST, result) def test_discover_propagates_exception(self): self.fake_conn.block_storage.volumes = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.discovery.discover, self.manager) class TestVolumeSnapshotsDiscovery(_BaseDiscoveryTestCase): def setUp(self): super().setUp() self.discovery = discovery.VolumeSnapshotsDiscovery(self.CONF) def test_keystone_required_for_cinder_service(self): self.assertEqual( 'cinder', self.discovery.KEYSTONE_REQUIRED_FOR_SERVICE) def test_discover_returns_snapshots(self): resources = self.discovery.discover(self.manager) self.assertEqual(fakes.SNAPSHOT_LIST, resources) def test_discover_empty(self): self.setup_connection(snapshots=[]) self.discovery = discovery.VolumeSnapshotsDiscovery(self.CONF) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_calls_list_volume_snapshots_with_all_projects(self): result = self.discovery.discover(self.manager) self.fake_conn.block_storage.snapshots.assert_called_once_with( details=True, all_projects=True) self.assertEqual(fakes.SNAPSHOT_LIST, result) def test_discover_propagates_exception(self): self.fake_conn.block_storage.snapshots = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.discovery.discover, self.manager) class TestVolumeBackupsDiscovery(_BaseDiscoveryTestCase): def setUp(self): super().setUp() self.discovery = discovery.VolumeBackupsDiscovery(self.CONF) def test_keystone_required_for_cinder_service(self): self.assertEqual( 'cinder', self.discovery.KEYSTONE_REQUIRED_FOR_SERVICE) def test_discover_returns_backups(self): resources = self.discovery.discover(self.manager) self.assertEqual(fakes.BACKUP_LIST, resources) def test_discover_empty(self): self.setup_connection(backups=[]) self.discovery = discovery.VolumeBackupsDiscovery(self.CONF) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_calls_list_backups_with_all_projects(self): result = self.discovery.discover(self.manager) self.fake_conn.block_storage.backups.assert_called_once_with( details=True, all_projects=True) self.assertEqual(fakes.BACKUP_LIST, result) def test_discover_propagates_exception(self): self.fake_conn.block_storage.backups = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.discovery.discover, self.manager) class TestVolumePoolsDiscovery(_BaseDiscoveryTestCase): def setUp(self): super().setUp() self.discovery = discovery.VolumePoolsDiscovery(self.CONF) def test_keystone_required_for_cinder_service(self): self.assertEqual( 'cinder', self.discovery.KEYSTONE_REQUIRED_FOR_SERVICE) def test_discover_returns_pools(self): resources = self.discovery.discover(self.manager) self.assertEqual(fakes.POOL_LIST, resources) def test_discover_empty(self): self.setup_connection(pools=[]) self.discovery = discovery.VolumePoolsDiscovery(self.CONF) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_calls_backend_pools(self): self.discovery.discover(self.manager) self.fake_conn.block_storage.backend_pools.assert_called_once_with() def test_discover_propagates_exception(self): self.fake_conn.block_storage.backend_pools = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.discovery.discover, self.manager) class TestVolumeServicesDiscovery(_BaseDiscoveryTestCase): def setUp(self): super().setUp() self.discovery = discovery.VolumeServicesDiscovery(self.CONF) def test_keystone_required_for_cinder_service(self): self.assertEqual( 'cinder', self.discovery.KEYSTONE_REQUIRED_FOR_SERVICE) def test_discover_returns_services(self): resources = self.discovery.discover(self.manager) self.assertEqual(fakes.SERVICE_LIST, resources) def test_discover_empty(self): self.setup_connection(services=[]) self.discovery = discovery.VolumeServicesDiscovery(self.CONF) resources = self.discovery.discover(self.manager) self.assertEqual([], resources) def test_discover_calls_list_with_no_args(self): result = self.discovery.discover(self.manager) self.fake_conn.block_storage.services.assert_called_once_with() self.assertEqual(fakes.SERVICE_LIST, result) def test_discover_propagates_exception(self): self.fake_conn.block_storage.services = mock.Mock( side_effect=os_exceptions.HttpException()) self.assertRaises( os_exceptions.HttpException, self.discovery.discover, self.manager) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2333567 ceilometer-27.0.0.0rc1/ceilometer/utils.py0000664000175000017500000000324315250267452017172 0ustar00zuulzuul# Copyright 2010 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # Copyright 2011 Justin Santa Barbara # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Utilities and helper functions.""" import threading from oslo_config import cfg from oslo_utils import timeutils OPTS = [ cfg.StrOpt('rootwrap_config', default='/etc/ceilometer/rootwrap.conf', help='Path to the rootwrap configuration file to ' 'use for running commands as root'), ] def get_root_helper(conf): return 'sudo ceilometer-rootwrap %s' % conf.rootwrap_config def spawn_thread(target, *args, **kwargs): t = threading.Thread(target=target, args=args, kwargs=kwargs) t.daemon = True t.start() return t def isotime(at=None): """Current time as ISO string, :returns: Current time in ISO format """ if not at: at = timeutils.utcnow() date_string = at.strftime("%Y-%m-%dT%H:%M:%S") tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' date_string += ('Z' if tz == 'UTC' else tz) return date_string ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2333567 ceilometer-27.0.0.0rc1/ceilometer/version.py0000664000175000017500000000121115250267452017510 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version version_info = pbr.version.VersionInfo('ceilometer') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2761276 ceilometer-27.0.0.0rc1/ceilometer/volume/0000775000175000017500000000000015250267567016774 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2333567 ceilometer-27.0.0.0rc1/ceilometer/volume/__init__.py0000664000175000017500000000000015250267452021064 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2343566 ceilometer-27.0.0.0rc1/ceilometer/volume/cinder.py0000664000175000017500000002441015250267452020604 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Common code for working with volumes """ import math from ceilometer.polling import plugin_base from ceilometer import sample SERVICE_STATE = { 'up': 1, 'down': 0, } class _Base(plugin_base.PollsterBase): FIELDS = [] def extract_metadata(self, obj): return {k: getattr(obj, k) for k in self.FIELDS} class VolumeSizePollster(_Base): @property def default_discovery(self): return 'volumes' FIELDS = ['name', 'status', 'volume_type', 'volume_type_id', 'availability_zone', 'migration_status', 'attachments', 'snapshot_id'] def extract_metadata(self, obj): metadata = super().extract_metadata(obj) if getattr(obj, "volume_image_metadata", None): metadata["image_id"] = obj.volume_image_metadata.get("image_id") else: metadata["image_id"] = None if obj.attachments: metadata["instance_id"] = obj.attachments[0]["server_id"] else: metadata["instance_id"] = None # Map SDK attribute names to preserve backward compatibility with # cinderclient metadata field names metadata["os-vol-host-attr:host"] = getattr(obj, "host") metadata["source_volid"] = getattr(obj, "source_volume_id") return metadata def get_samples(self, manager, cache, resources): for volume in resources: yield sample.Sample( name='volume.size', type=sample.TYPE_GAUGE, unit='GiB', volume=volume.size, user_id=volume.user_id, project_id=volume.project_id, resource_id=volume.id, resource_metadata=self.extract_metadata(volume), ) class VolumeSnapshotSize(_Base): @property def default_discovery(self): return 'volume_snapshots' FIELDS = ['name', 'volume_id', 'status', 'description', 'metadata', ] def extract_metadata(self, obj): metadata = super().extract_metadata(obj) # Map SDK attribute names to preserve backward compatibility with # cinderclient metadata field names metadata["os-extended-snapshot-attributes:progress"] = obj.progress return metadata def get_samples(self, manager, cache, resources): for snapshot in resources: yield sample.Sample( name='volume.snapshot.size', type=sample.TYPE_GAUGE, unit='GiB', volume=snapshot.size, user_id=snapshot.user_id, project_id=snapshot.project_id, resource_id=snapshot.id, resource_metadata=self.extract_metadata(snapshot), ) class VolumeBackupSize(_Base): @property def default_discovery(self): return 'volume_backups' FIELDS = ['name', 'is_incremental', 'object_count', 'container', 'volume_id', 'status', 'description'] def get_samples(self, manager, cache, resources): for backup in resources: yield sample.Sample( name='volume.backup.size', type=sample.TYPE_GAUGE, unit='GiB', volume=backup.size, user_id=backup.user_id, project_id=backup.project_id, resource_id=backup.id, resource_metadata=self.extract_metadata(backup), ) class _VolumeProviderPoolBase(_Base): def extract_metadata(self, obj): metadata = super().extract_metadata(obj) caps = obj.capabilities or {} metadata['pool_name'] = caps.get("pool_name") or None # A pool is named "@#". The volume_provider_pool # resource type in gnocchi_resources.yaml maps its ``provider`` # attribute from resource_metadata.provider, which the capacity.pool # notification meters derive the same way (name_to_id up to '#'). # Polled samples lacked it, so Gnocchi rejected every one of them # with "required key not provided @ data['provider']". metadata['provider'] = obj.name.split('#', 1)[0] return metadata class VolumeProviderPoolCapacityTotal(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: caps = pool.capabilities if caps.get('total_capacity_gb') is not None: yield sample.Sample( name='volume.provider.pool.capacity.total', type=sample.TYPE_GAUGE, unit='GiB', volume=caps.get('total_capacity_gb'), user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) class VolumeProviderPoolCapacityFree(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: caps = pool.capabilities if caps.get('free_capacity_gb') is not None: yield sample.Sample( name='volume.provider.pool.capacity.free', type=sample.TYPE_GAUGE, unit='GiB', volume=caps['free_capacity_gb'], user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) class VolumeProviderPoolCapacityProvisioned(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: caps = pool.capabilities if caps.get('provisioned_capacity_gb'): yield sample.Sample( name='volume.provider.pool.capacity.provisioned', type=sample.TYPE_GAUGE, unit='GiB', volume=caps['provisioned_capacity_gb'], user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) class VolumeProviderPoolCapacityVirtualFree(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: caps = pool.capabilities or {} if caps.get('provisioned_capacity_gb'): reserved_size = math.floor( (caps['reserved_percentage'] / 100) * caps['total_capacity_gb'] ) max_over_subscription_ratio = 1.0 if caps['thin_provisioning_support']: max_over_subscription_ratio = float( caps.get('max_over_subscription_ratio', 1.0) ) value = ( max_over_subscription_ratio * (caps['total_capacity_gb'] - reserved_size) - caps['provisioned_capacity_gb'] ) yield sample.Sample( name='volume.provider.pool.capacity.virtual_free', type=sample.TYPE_GAUGE, unit='GiB', volume=value, user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) class VolumeProviderPoolCapacityAllocated(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: caps = pool.capabilities or {} yield sample.Sample( name='volume.provider.pool.capacity.allocated', type=sample.TYPE_GAUGE, unit='GiB', volume=caps['allocated_capacity_gb'], user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) class VolumeServiceHealthPollster(_Base): @property def default_discovery(self): return 'volume_services' FIELDS = ['binary', 'host', 'status'] def extract_metadata(self, obj): metadata = super().extract_metadata(obj) # Map SDK attribute names to preserve backward compatibility with # cinderclient metadata field names metadata['zone'] = getattr(obj, 'availability_zone', None) return metadata def get_samples(self, manager, cache, resources): for svc in resources: svc_state = getattr(svc, 'state', '').lower() if svc_state not in SERVICE_STATE: raise ValueError( f"Unknown service state '{svc_state}' for " f"{svc.binary}@{svc.host}") state = SERVICE_STATE[svc_state] yield sample.Sample( name='volume.service.health', type=sample.TYPE_GAUGE, unit='health', volume=state, user_id=None, project_id=None, resource_id=f'{svc.binary}@{svc.host}', resource_metadata=self.extract_metadata(svc), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2343566 ceilometer-27.0.0.0rc1/ceilometer/volume/discovery.py0000664000175000017500000000337315250267452021354 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ceilometer import cinder_client from ceilometer.polling import plugin_base class _BaseDiscovery(plugin_base.DiscoveryBase): KEYSTONE_REQUIRED_FOR_SERVICE = 'cinder' def __init__(self, conf): super().__init__(conf) self.client = cinder_client.Client(conf) class VolumeDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover volume resources to monitor.""" return self.client.list_volumes() class VolumeSnapshotsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover snapshot resources to monitor.""" return self.client.list_volume_snapshots() class VolumeBackupsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover backup resources to monitor.""" return self.client.list_backups() class VolumePoolsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover volume resources to monitor.""" return self.client.list_pools() class VolumeServicesDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover cinder service resources to monitor.""" return self.client.list_services() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2331276 ceilometer-27.0.0.0rc1/ceilometer.egg-info/0000775000175000017500000000000015250267567017157 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.0621278 ceilometer-27.0.0.0rc1/ceilometer.egg-info/PKG-INFO0000644000175000017500000000717015250267567020257 0ustar00zuulzuulMetadata-Version: 2.4 Name: ceilometer Version: 27.0.0.0rc1 Summary: OpenStack Telemetry Home-page: https://docs.openstack.org/ceilometer/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Topic :: System :: Monitoring Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Requires-Python: >=3.11 License-File: LICENSE Requires-Dist: xmltodict>=0.13.0 Requires-Dist: cachetools>=2.1.0 Requires-Dist: cotyledon>=1.3.0 Requires-Dist: futurist>=1.8.0 Requires-Dist: jsonpath-rw-ext>=1.1.3 Requires-Dist: lxml>=4.5.1 Requires-Dist: msgpack>=0.5.2 Requires-Dist: oslo.concurrency>=3.29.0 Requires-Dist: oslo.config>=8.6.0 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: oslo.log>=3.36.0 Requires-Dist: oslo.reports>=1.18.0 Requires-Dist: oslo.rootwrap>=2.0.0 Requires-Dist: pbr>=2.0.0 Requires-Dist: oslo.messaging>=10.3.0 Requires-Dist: oslo.upgradecheck>=0.1.1 Requires-Dist: oslo.utils>=4.7.0 Requires-Dist: oslo.privsep>=1.32.0 Requires-Dist: keystoneauth1>=3.18.0 Requires-Dist: python-novaclient>=9.1.0 Requires-Dist: python-swiftclient>=3.2.0 Requires-Dist: PyYAML>=5.1 Requires-Dist: requests>=2.25.1 Requires-Dist: stevedore>=1.20.0 Requires-Dist: tenacity>=6.3.1 Requires-Dist: tooz>=1.47.0 Requires-Dist: oslo.cache>=1.26.0 Requires-Dist: gnocchiclient>=7.0.0 Requires-Dist: python-zaqarclient>=1.3.0 Requires-Dist: prometheus-client>=0.20.0 Requires-Dist: aodhclient>=3.8.0 Requires-Dist: awscurl>=0.40 Requires-Dist: botocore>=1.20.0 Requires-Dist: openstacksdk>=4.6.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: home-page Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary ========== Ceilometer ========== .. image:: https://governance.openstack.org/tc/badges/ceilometer.svg .. Change things from this point on -------- Overview -------- Ceilometer is a data collection service that collects event and metering data by monitoring notifications sent from OpenStack services. It publishes collected data to various targets including data stores and message queues. Ceilometer is distributed under the terms of the Apache License, Version 2.0. The full terms and conditions of this license are detailed in the LICENSE file. ------------- Documentation ------------- Release notes are available at https://releases.openstack.org/teams/telemetry.html Developer documentation is available at https://docs.openstack.org/ceilometer/latest/ Launchpad Projects ------------------ - Server: https://launchpad.net/ceilometer Code Repository --------------- - Server: https://opendev.org/openstack/ceilometer Bug Tracking ------------ - Bugs: https://bugs.launchpad.net/ceilometer/ Release Notes ------------- - Server: https://docs.openstack.org/releasenotes/ceilometer/ IRC --- IRC Channel: #openstack-telemetry on `OFTC`_. Mailinglist ----------- Project use http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss as the mailinglist. Please use tag ``[Ceilometer]`` in the subject for new threads. .. _OFTC: https://oftc.net/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.1981277 ceilometer-27.0.0.0rc1/ceilometer.egg-info/SOURCES.txt0000664000175000017500000006563715250267567021064 0ustar00zuulzuul.coveragerc .mailmap .pre-commit-config.yaml .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bindep.txt pyproject.toml reno.yaml requirements.txt setup.cfg setup.py test-requirements.txt tox.ini ceilometer/__init__.py ceilometer/agent.py ceilometer/cache_utils.py ceilometer/cinder_client.py ceilometer/declarative.py ceilometer/designate_client.py ceilometer/exceptions.py ceilometer/gnocchi_client.py ceilometer/i18n.py ceilometer/keystone_client.py ceilometer/manila_client.py ceilometer/messaging.py ceilometer/middleware.py ceilometer/neutron_client.py ceilometer/notification.py ceilometer/nova_client.py ceilometer/octavia_client.py ceilometer/opts.py ceilometer/sample.py ceilometer/service.py ceilometer/utils.py ceilometer/version.py ceilometer.egg-info/PKG-INFO ceilometer.egg-info/SOURCES.txt ceilometer.egg-info/dependency_links.txt ceilometer.egg-info/entry_points.txt ceilometer.egg-info/not-zip-safe ceilometer.egg-info/pbr.json ceilometer.egg-info/requires.txt ceilometer.egg-info/top_level.txt ceilometer/alarm/__init__.py ceilometer/alarm/aodh.py ceilometer/alarm/discovery.py ceilometer/cmd/__init__.py ceilometer/cmd/agent_notification.py ceilometer/cmd/polling.py ceilometer/cmd/sample.py ceilometer/cmd/status.py ceilometer/cmd/storage.py ceilometer/compute/__init__.py ceilometer/compute/discovery.py ceilometer/compute/pollsters/__init__.py ceilometer/compute/pollsters/disk.py ceilometer/compute/pollsters/instance_stats.py ceilometer/compute/pollsters/net.py ceilometer/compute/pollsters/util.py ceilometer/compute/virt/__init__.py ceilometer/compute/virt/inspector.py ceilometer/compute/virt/libvirt/__init__.py ceilometer/compute/virt/libvirt/inspector.py ceilometer/compute/virt/libvirt/utils.py ceilometer/data/meters.d/meters.yaml ceilometer/dns/__init__.py ceilometer/dns/designate.py ceilometer/dns/discovery.py ceilometer/event/__init__.py ceilometer/event/converter.py ceilometer/event/models.py ceilometer/event/trait_plugins.py ceilometer/hacking/__init__.py ceilometer/image/__init__.py ceilometer/image/discovery.py ceilometer/image/glance.py ceilometer/ipmi/__init__.py ceilometer/ipmi/notifications/__init__.py ceilometer/ipmi/notifications/ironic.py ceilometer/ipmi/platform/__init__.py ceilometer/ipmi/platform/exception.py ceilometer/ipmi/platform/ipmi_sensor.py ceilometer/ipmi/platform/ipmitool.py ceilometer/ipmi/pollsters/__init__.py ceilometer/ipmi/pollsters/sensor.py ceilometer/load_balancer/__init__.py ceilometer/load_balancer/discovery.py ceilometer/load_balancer/octavia.py ceilometer/locale/de/LC_MESSAGES/ceilometer.po ceilometer/locale/en_GB/LC_MESSAGES/ceilometer.po ceilometer/locale/es/LC_MESSAGES/ceilometer.po ceilometer/locale/fr/LC_MESSAGES/ceilometer.po ceilometer/locale/it/LC_MESSAGES/ceilometer.po ceilometer/locale/ja/LC_MESSAGES/ceilometer.po ceilometer/locale/ko_KR/LC_MESSAGES/ceilometer.po ceilometer/locale/pt_BR/LC_MESSAGES/ceilometer.po ceilometer/locale/ru/LC_MESSAGES/ceilometer.po ceilometer/locale/zh_CN/LC_MESSAGES/ceilometer.po ceilometer/locale/zh_TW/LC_MESSAGES/ceilometer.po ceilometer/meter/__init__.py ceilometer/meter/notifications.py ceilometer/network/__init__.py ceilometer/network/floatingip.py ceilometer/network/services/__init__.py ceilometer/network/services/base.py ceilometer/network/services/discovery.py ceilometer/network/services/fwaas.py ceilometer/network/services/vpnaas.py ceilometer/objectstore/__init__.py ceilometer/objectstore/rgw.py ceilometer/objectstore/rgw_client.py ceilometer/objectstore/swift.py ceilometer/pipeline/__init__.py ceilometer/pipeline/base.py ceilometer/pipeline/event.py ceilometer/pipeline/sample.py ceilometer/pipeline/data/event_definitions.yaml ceilometer/pipeline/data/event_pipeline.yaml ceilometer/pipeline/data/pipeline.yaml ceilometer/polling/__init__.py ceilometer/polling/dynamic_pollster.py ceilometer/polling/manager.py ceilometer/polling/plugin_base.py ceilometer/polling/prom_exporter.py ceilometer/polling/discovery/__init__.py ceilometer/polling/discovery/endpoint.py ceilometer/polling/discovery/localnode.py ceilometer/polling/discovery/non_openstack_credentials_discovery.py ceilometer/polling/discovery/tenant.py ceilometer/privsep/__init__.py ceilometer/privsep/ipmitool.py ceilometer/publisher/__init__.py ceilometer/publisher/file.py ceilometer/publisher/gnocchi.py ceilometer/publisher/http.py ceilometer/publisher/messaging.py ceilometer/publisher/opentelemetry_http.py ceilometer/publisher/prometheus.py ceilometer/publisher/tcp.py ceilometer/publisher/test.py ceilometer/publisher/udp.py ceilometer/publisher/utils.py ceilometer/publisher/zaqar.py ceilometer/publisher/data/gnocchi_resources.yaml ceilometer/share/__init__.py ceilometer/share/discovery.py ceilometer/share/manila.py ceilometer/telemetry/__init__.py ceilometer/telemetry/notifications.py ceilometer/tests/__init__.py ceilometer/tests/base.py ceilometer/tests/fixtures.py ceilometer/tests/unit/__init__.py ceilometer/tests/unit/fakes.py ceilometer/tests/unit/pipeline_base.py ceilometer/tests/unit/test_bin.py ceilometer/tests/unit/test_cache_utils.py ceilometer/tests/unit/test_cinder_client.py ceilometer/tests/unit/test_declarative.py ceilometer/tests/unit/test_decoupled_pipeline.py ceilometer/tests/unit/test_event_pipeline.py ceilometer/tests/unit/test_exceptions.py ceilometer/tests/unit/test_keystone_client.py ceilometer/tests/unit/test_messaging.py ceilometer/tests/unit/test_middleware.py ceilometer/tests/unit/test_neutronclient.py ceilometer/tests/unit/test_notification.py ceilometer/tests/unit/test_novaclient.py ceilometer/tests/unit/test_polling.py ceilometer/tests/unit/test_prom_exporter.py ceilometer/tests/unit/test_sample.py ceilometer/tests/unit/alarm/__init__.py ceilometer/tests/unit/alarm/test_aodh.py ceilometer/tests/unit/alarm/test_discovery.py ceilometer/tests/unit/cmd/__init__.py ceilometer/tests/unit/cmd/test_status.py ceilometer/tests/unit/compute/__init__.py ceilometer/tests/unit/compute/test_discovery.py ceilometer/tests/unit/compute/pollsters/__init__.py ceilometer/tests/unit/compute/pollsters/base.py ceilometer/tests/unit/compute/pollsters/test_cpu.py ceilometer/tests/unit/compute/pollsters/test_disk.py ceilometer/tests/unit/compute/pollsters/test_diskio.py ceilometer/tests/unit/compute/pollsters/test_location_metadata.py ceilometer/tests/unit/compute/pollsters/test_memory.py ceilometer/tests/unit/compute/pollsters/test_net.py ceilometer/tests/unit/compute/pollsters/test_perf.py ceilometer/tests/unit/compute/virt/__init__.py ceilometer/tests/unit/compute/virt/libvirt/__init__.py ceilometer/tests/unit/compute/virt/libvirt/test_inspector.py ceilometer/tests/unit/dns/__init__.py ceilometer/tests/unit/dns/test_designate.py ceilometer/tests/unit/event/__init__.py ceilometer/tests/unit/event/test_converter.py ceilometer/tests/unit/event/test_endpoint.py ceilometer/tests/unit/event/test_trait_plugins.py ceilometer/tests/unit/image/__init__.py ceilometer/tests/unit/image/test_glance.py ceilometer/tests/unit/ipmi/__init__.py ceilometer/tests/unit/ipmi/notifications/__init__.py ceilometer/tests/unit/ipmi/notifications/ipmi_test_data.py ceilometer/tests/unit/ipmi/notifications/test_ironic.py ceilometer/tests/unit/ipmi/platform/__init__.py ceilometer/tests/unit/ipmi/platform/fake_utils.py ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py ceilometer/tests/unit/ipmi/pollsters/__init__.py ceilometer/tests/unit/ipmi/pollsters/base.py ceilometer/tests/unit/ipmi/pollsters/test_sensor.py ceilometer/tests/unit/load_balancer/__init__.py ceilometer/tests/unit/load_balancer/test_octavia.py ceilometer/tests/unit/meter/__init__.py ceilometer/tests/unit/meter/test_meter_plugins.py ceilometer/tests/unit/meter/test_notifications.py ceilometer/tests/unit/network/__init__.py ceilometer/tests/unit/network/test_floating_ip.py ceilometer/tests/unit/network/services/__init__.py ceilometer/tests/unit/network/services/test_fwaas.py ceilometer/tests/unit/network/services/test_vpnaas.py ceilometer/tests/unit/objectstore/__init__.py ceilometer/tests/unit/objectstore/test_rgw.py ceilometer/tests/unit/objectstore/test_rgw_client.py ceilometer/tests/unit/objectstore/test_swift.py ceilometer/tests/unit/polling/__init__.py ceilometer/tests/unit/polling/test_discovery.py ceilometer/tests/unit/polling/test_dynamic_pollster.py ceilometer/tests/unit/polling/test_heartbeat.py ceilometer/tests/unit/polling/test_manager.py ceilometer/tests/unit/polling/test_non_openstack_credentials_discovery.py ceilometer/tests/unit/polling/test_non_openstack_dynamic_pollster.py ceilometer/tests/unit/publisher/__init__.py ceilometer/tests/unit/publisher/test_file.py ceilometer/tests/unit/publisher/test_gnocchi.py ceilometer/tests/unit/publisher/test_http.py ceilometer/tests/unit/publisher/test_messaging_publisher.py ceilometer/tests/unit/publisher/test_opentelemetry_http.py ceilometer/tests/unit/publisher/test_prometheus.py ceilometer/tests/unit/publisher/test_tcp.py ceilometer/tests/unit/publisher/test_udp.py ceilometer/tests/unit/publisher/test_utils.py ceilometer/tests/unit/publisher/test_zaqar.py ceilometer/tests/unit/share/__init__.py ceilometer/tests/unit/share/test_manila.py ceilometer/tests/unit/volume/__init__.py ceilometer/tests/unit/volume/test_cinder.py ceilometer/tests/unit/volume/test_discovery.py ceilometer/volume/__init__.py ceilometer/volume/cinder.py ceilometer/volume/discovery.py devstack/README.rst devstack/local.conf.sample devstack/plugin.sh devstack/settings devstack/files/rpms/ceilometer devstack/upgrade/settings devstack/upgrade/shutdown.sh devstack/upgrade/upgrade.sh doc/dictionary.txt doc/requirements.txt doc/source/conf.py doc/source/glossary.rst doc/source/index.rst doc/source/admin/index.rst doc/source/admin/telemetry-best-practices.rst doc/source/admin/telemetry-data-collection.rst doc/source/admin/telemetry-data-pipelines.rst doc/source/admin/telemetry-dynamic-pollster.rst doc/source/admin/telemetry-events.rst doc/source/admin/telemetry-measurements.rst doc/source/admin/telemetry-system-architecture.rst doc/source/admin/telemetry-troubleshooting-guide.rst doc/source/cli/ceilometer-status.rst doc/source/cli/index.rst doc/source/configuration/index.rst doc/source/contributor/1-agents.png doc/source/contributor/2-1-collection-notification.png doc/source/contributor/2-2-collection-poll.png doc/source/contributor/2-accessmodel.png doc/source/contributor/3-Pipeline.png doc/source/contributor/5-multi-publish.png doc/source/contributor/6-storagemodel.png doc/source/contributor/architecture.rst doc/source/contributor/ceilo-arch.png doc/source/contributor/ceilo-gnocchi-arch.png doc/source/contributor/devstack.rst doc/source/contributor/events.rst doc/source/contributor/gmr.rst doc/source/contributor/index.rst doc/source/contributor/measurements.rst doc/source/contributor/new_resource_types.rst doc/source/contributor/overview.rst doc/source/contributor/plugins.rst doc/source/contributor/testing.rst doc/source/install/get_started.rst doc/source/install/index.rst doc/source/install/install-base-config-common.inc doc/source/install/install-base-prereq-common.inc doc/source/install/install-base-rdo.rst doc/source/install/install-base-ubuntu.rst doc/source/install/install-compute-common.inc doc/source/install/install-compute-rdo.rst doc/source/install/install-compute-ubuntu.rst doc/source/install/install-compute.rst doc/source/install/install-controller.rst doc/source/install/install-gnocchi.inc doc/source/install/next-steps.rst doc/source/install/verify.rst doc/source/install/cinder/install-cinder-config-common.inc doc/source/install/cinder/install-cinder-rdo.rst doc/source/install/cinder/install-cinder-ubuntu.rst doc/source/install/glance/install-glance-rdo.rst doc/source/install/glance/install-glance-ubuntu.rst doc/source/install/heat/install-heat-rdo.rst doc/source/install/heat/install-heat-ubuntu.rst doc/source/install/neutron/install-neutron-rdo.rst doc/source/install/neutron/install-neutron-ubuntu.rst doc/source/install/swift/install-swift-config-common.inc doc/source/install/swift/install-swift-prereq-common.inc doc/source/install/swift/install-swift-rdo.rst doc/source/install/swift/install-swift-ubuntu.rst doc/source/releasenotes/folsom.rst doc/source/releasenotes/index.rst etc/ceilometer/ceilometer-config-generator.conf etc/ceilometer/polling.yaml etc/ceilometer/polling_all.yaml etc/ceilometer/rootwrap.conf etc/ceilometer/examples/osprofiler_event_definitions.yaml etc/ceilometer/rootwrap.d/ipmi.filters releasenotes/notes/.placeholder releasenotes/notes/add-aodh-metrics-afbe9b780fd137d6.yaml releasenotes/notes/add-availability_zone-gnocchi-instance-15170e4966a89d63.yaml releasenotes/notes/add-db-legacy-clean-tool-7b3e3714f414c448.yaml releasenotes/notes/add-designate-dns-metrics-8f4a2c7e9b1d3e5a.yaml releasenotes/notes/add-disk-latency-metrics-9e5c05108a78c3d9.yaml releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml releasenotes/notes/add-full-snmpv3-usm-support-ab540c902fa89b9d.yaml releasenotes/notes/add-image-meta-to-samples-72d62016f5ab0043.yaml releasenotes/notes/add-ipmi-sensor-data-gnocchi-70573728499abe86.yaml releasenotes/notes/add-json-output-to-file-publisher-786380cb7e21b56b.yaml releasenotes/notes/add-loadbalancer-resource-type-a73c29594b72f012.yaml releasenotes/notes/add-magnum-event-4c75ed0bb268d19c.yaml releasenotes/notes/add-manila-share-polling-metrics-ede1c8507d684f.yaml releasenotes/notes/add-map-trait-plugin-0d969f5cc7b18175.yaml releasenotes/notes/add-memory-available-metric-d59dc787c485efd3.yaml releasenotes/notes/add-memory-pollster-7689d354c45cc740.yaml releasenotes/notes/add-memory-swap-metric-f1633962ab2cf0f6.yaml releasenotes/notes/add-octavia-loadbalancer-metrics-8f4e7d2c1a3b5e69.yaml releasenotes/notes/add-parameter-for-disabled-projects-381da4543fff071d.yaml releasenotes/notes/add-pool-size-metrics-cdecb979135bba85.yaml releasenotes/notes/add-power-state-metric-cdfbb3098b50a704.yaml releasenotes/notes/add-rgw-client-tls-version-1e24dc968255abcb.yaml releasenotes/notes/add-swift-storage_policy-attribute-322fbb5716c5bb10.yaml releasenotes/notes/add-tenant-name-discovery-668260bb4b2b0e8c.yaml releasenotes/notes/add-tool-for-migrating-data-to-gnocchi-cea8d4db68ce03d0.yaml releasenotes/notes/add-upgrade-check-framework-d78858c54cb85f91.yaml releasenotes/notes/add-vcpus-pollster-23cfa683d07092b3.yaml releasenotes/notes/add-volume-pollster-metadata-d7b435fed9aac0aa.yaml releasenotes/notes/add-volume-service-health-metric-f6ecac5486da168f.yaml releasenotes/notes/add-volume_type_id-attr-f29af86534907941.yaml releasenotes/notes/aggregator-transformer-timeout-e0f42b6c96aa7ada.yaml releasenotes/notes/allow-disable-gnocchi-project-filter-c7e08c74d57b0224.yaml releasenotes/notes/always-requeue-7a2df9243987ab67.yaml releasenotes/notes/batch-messaging-d126cc525879d58e.yaml releasenotes/notes/bug-1929178-a8243526ce2311f7.yaml releasenotes/notes/bug-2007108-dba7163b245ad8fd.yaml releasenotes/notes/bug-2113768-a2db3a59c8e13558.yaml releasenotes/notes/cache-json-parsers-888307f3b6b498a2.yaml releasenotes/notes/ceilometer-api-deprecate-862bfaa54e80fa01.yaml releasenotes/notes/ceilometer-api-removal-6bd44d3eab05e593.yaml releasenotes/notes/ceilometer-event-api-removed-49c57835e307b997.yaml releasenotes/notes/cinder-capacity-samples-de94dcfed5540b6c.yaml releasenotes/notes/cinder-volume-size-poller-availability_zone-2d20a7527e2341b9.yaml releasenotes/notes/compute-discovery-interval-d19f7c9036a8c186.yaml releasenotes/notes/configurable-data-collector-e247aadbffb85243.yaml releasenotes/notes/cors-support-70c33ba1f6825a7b.yaml releasenotes/notes/deprecate-aggregated-disk-metrics-54a395c05e74d685.yaml releasenotes/notes/deprecate-ceilometer-collector-b793b91cd28b9e7f.yaml releasenotes/notes/deprecate-contrail-256177299deb6926.yaml releasenotes/notes/deprecate-events-6561f4059fa25c02.yaml releasenotes/notes/deprecate-file-dispatcher-2aff376db7609136.yaml releasenotes/notes/deprecate-generic-hardware-declarative-pollstar-dfa418bf6a5e0459.yaml releasenotes/notes/deprecate-http-control-exchanges-026a8de6819841f8.yaml releasenotes/notes/deprecate-http-dispatcher-dbbaacee8182b550.yaml releasenotes/notes/deprecate-http_timeout-ce98003e4949f9d9.yaml releasenotes/notes/deprecate-kafka-publisher-17b4f221758e15da.yaml releasenotes/notes/deprecate-neutron-fwaas-e985afe956240c08.yaml releasenotes/notes/deprecate-neutron-lbaas-5a36406cbe44bbe3.yaml releasenotes/notes/deprecate-odl-07e3f59165612566.yaml releasenotes/notes/deprecate-pollster-builder-d481966e497959a3.yaml releasenotes/notes/deprecate-pollster-list-ccf22b0dea44f043.yaml releasenotes/notes/deprecate-vmware-ae49e07e40e74577.yaml releasenotes/notes/deprecate-windows-support-d784b975ce878864.yaml releasenotes/notes/deprecate-xen-support-27600e2bf7be548c.yaml releasenotes/notes/deprecated_database_event_dispatcher_panko-607d558c86a90f17.yaml releasenotes/notes/disabled-projects-api-filter-4dee304fcd523356.yaml releasenotes/notes/drop-collector-4c207b35d67b2977.yaml releasenotes/notes/drop-image-meter-9c9b6cebd546dae7.yaml releasenotes/notes/drop-instance-meter-1b657717b21a0f55.yaml releasenotes/notes/drop-kwapi-b687bc476186d01b.yaml releasenotes/notes/drop-py-2-7-87352d5763131c13.yaml releasenotes/notes/drop-python-3-6-and-3-7-f67097fa6894da52.yaml releasenotes/notes/dynamic-pollster-system-6b45c8c973201b2b.yaml releasenotes/notes/dynamic-pollster-system-for-non-openstack-apis-4e06694f223f34f3.yaml releasenotes/notes/dynamic-pollster-url-joins-6cdb01c4015976f7.yaml releasenotes/notes/enable-promethus-exporter-tls-76e78d4f4a52c6c4.yaml releasenotes/notes/event-type-race-c295baf7f1661eab.yaml releasenotes/notes/fetch_metadata-default-false-b03d0a1db61439f7.yaml releasenotes/notes/fix-1940660-5226988f2e7ae1bd.yaml releasenotes/notes/fix-agent-coordination-a7103a78fecaec24.yaml releasenotes/notes/fix-aggregation-transformer-9472aea189fa8f65.yaml releasenotes/notes/fix-floatingip-pollster-f5172060c626b19e.yaml releasenotes/notes/fix-network-lb-bytes-sample-5dec2c6f3a8ae174.yaml releasenotes/notes/fix-notification-batch-9bb42cbdf817e7f9.yaml releasenotes/notes/fix-radosgw-name-6de6899ddcd7e06d.yaml releasenotes/notes/fix-size-metric-units-e6028b4b4fc3e6aa.yaml releasenotes/notes/fix-volume-provider-pool-capacity-metrics-7b8b0de29a513cea.yaml releasenotes/notes/get-more-flavor-info-from-libvirt-c8db26fe410abe6e.yaml releasenotes/notes/gnocchi-cache-1d8025dfc954f281.yaml releasenotes/notes/gnocchi-cache-b9ad4d85a1da8d3f.yaml releasenotes/notes/gnocchi-client-42cd992075ee53ab.yaml releasenotes/notes/gnocchi-host-metrics-829bcb965d8f2533.yaml releasenotes/notes/gnocchi-no-metric-by-default-b643e09f5ffef2c4.yaml releasenotes/notes/gnocchi-orchestration-3497c689268df0d1.yaml releasenotes/notes/gnocchi-publisher-hash-blake2-514c0ca1882b4423.yaml releasenotes/notes/gnocchi-udp-collector-00415e6674b5cc0f.yaml releasenotes/notes/gnocchi-update-project-id-c8355fc5901e7aa3.yaml releasenotes/notes/handle-malformed-resource-definitions-ad4f69f898ced34d.yaml releasenotes/notes/http-dispatcher-batching-4e17fce46a196b07.yaml releasenotes/notes/http-dispatcher-verify-ssl-551d639f37849c6f.yaml releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml releasenotes/notes/http_proxy_to_wsgi_enabled-616fa123809e1600.yaml releasenotes/notes/improve-events-rbac-support-f216bd7f34b02032.yaml releasenotes/notes/include-monasca-publisher-1f47dde52af50feb.yaml releasenotes/notes/index-events-mongodb-63cb04200b03a093.yaml releasenotes/notes/instance-discovery-new-default-7f9b451a515dddf4.yaml releasenotes/notes/instance-record-launched-created-deleted-d7f44df3bbcf0790.yaml releasenotes/notes/keystone-v3-fab1e257c5672965.yaml releasenotes/notes/kwapi_deprecated-c92b9e72c78365f0.yaml releasenotes/notes/less-nova-polling-ac56687da3f8b1a3.yaml releasenotes/notes/log-error-alarm-rgw-pollsters-c53e01b411864840.yaml releasenotes/notes/lookup-meter-def-vol-correctly-0122ae429275f2a6.yaml releasenotes/notes/make-image-format-attributes-optional-cc7acd492f791553.yaml releasenotes/notes/make-instance-host-optional-972fa14405c1e2f6.yaml releasenotes/notes/manager-based-ipc-queues-85e3bf59ffdfb0ac.yaml releasenotes/notes/memory-bandwidth-meter-f86cf01178573671.yaml releasenotes/notes/mongodb-handle-large-numbers-7c235598ca700f2d.yaml releasenotes/notes/network-statistics-from-opendaylight-787df77484d8d751.yaml releasenotes/notes/openstack-dynamic-pollsters-metadata-enrichment-703cf5914cf0c578.yaml releasenotes/notes/parallel_requests_option-a3f901b6001e26e4.yaml releasenotes/notes/parallels-virt_type-ee29c4802fdf5c8e.yaml releasenotes/notes/pecan-debug-removed-dc737efbf911bde7.yaml releasenotes/notes/perf-events-meter-b06c2a915c33bfaf.yaml releasenotes/notes/pipeline-fallback-polling-3d962a0fff49ccdd.yaml releasenotes/notes/polling-batch-size-7fe11925df8d1221.yaml releasenotes/notes/polling-definition-efffb92e3810e571.yaml releasenotes/notes/polling-deprecation-4d5b83180893c053.yaml releasenotes/notes/prometheus-bcb201cfe46d5778.yaml releasenotes/notes/prometheus-pushgateway-grouping-a1b2c3d4e5f6a7b8.yaml releasenotes/notes/publish-fw-with-invalid-state-e961372c95791bcd.yaml releasenotes/notes/publish-network-resources-with-invalid-state-6693c6fa1fefa097.yaml releasenotes/notes/refresh-legacy-cache-e4dbbd3e2eeca70b.yaml releasenotes/notes/remove-alarms-4df3cdb4f1fb5faa.yaml releasenotes/notes/remove-aliases-for-telemetry_secret-5112a88118973427.yaml releasenotes/notes/remove-batch_polled_samples-b40241c8aad3667d.yaml releasenotes/notes/remove-cadf-http-f8449ced3d2a29d4.yaml releasenotes/notes/remove-ceilometer-dbsync-53aa1b529f194f15.yaml releasenotes/notes/remove-check_watchers-a7c955703b6d9f57.yaml releasenotes/notes/remove-compute-disk-meters-264e686622886ff0.yaml releasenotes/notes/remove-compute-rate-deprecated-meters-201893c6b686b04a.yaml releasenotes/notes/remove-compute-workload-partitioning-option-26538bc1e80500e3.yaml releasenotes/notes/remove-direct-publisher-5785ee7edd16c4d9.yaml releasenotes/notes/remove-eventlet-6738321434b60c78.yaml releasenotes/notes/remove-exchange-control-options-75ecd49423639068.yaml releasenotes/notes/remove-file-dispatcher-56ba1066c20d314a.yaml releasenotes/notes/remove-generic-hardware-declarative-pollster-e05c614f273ab149.yaml releasenotes/notes/remove-gnocchi-dispatcher-dd588252976c2abb.yaml releasenotes/notes/remove-gnocchi-dispatcher-options-4f4ba2a155c1a766.yaml releasenotes/notes/remove-http-dispatcher-1afdce1d1dc3158d.yaml releasenotes/notes/remove-http_timeout-5bced56beb771f2d.yaml releasenotes/notes/remove-intel-cmt-perf-meters-15d0fe72b2804f48.yaml releasenotes/notes/remove-intel-node-manager-0889de66dede9ab0.yaml releasenotes/notes/remove-kafka-broker-publisher-7026b370cfc831db.yaml releasenotes/notes/remove-meter-definitions-cfg-file-config-476596fc86c36a81.yaml releasenotes/notes/remove-meter-definitions-cfg-file-d57c726d563d805f.yaml releasenotes/notes/remove-monasca-d5ceda231839d43d.yaml releasenotes/notes/remove-neutron-lbaas-d3d4a5327f6a167a.yaml releasenotes/notes/remove-notification-workload-partitioning-2cef114fb2478e39.yaml releasenotes/notes/remove-nova-http-log-option-64e97a511e58da5d.yaml releasenotes/notes/remove-opencontrail-88656a9354179299.yaml releasenotes/notes/remove-opendaylight-c3839bbe9aa2a227.yaml releasenotes/notes/remove-pollster-list-bda30d747fb87c9e.yaml releasenotes/notes/remove-publisher-topic-options-7a40787a3998921d.yaml releasenotes/notes/remove-py38-80670bdcfd4dd135.yaml releasenotes/notes/remove-py39-8c39f81f856bee9f.yaml releasenotes/notes/remove-refresh-pipeline-618af089c5435db7.yaml releasenotes/notes/remove-rpc-collector-d0d0a354140fd107.yaml releasenotes/notes/remove-sahara-9254593d4fb137b9.yaml releasenotes/notes/remove-service-type-volume-v2-08c81098dc7c0922.yaml releasenotes/notes/remove-shuffle_time_before_polling_task-option-05a4d225236c64b1.yaml releasenotes/notes/remove-transformers-14e00a789dedd76b.yaml releasenotes/notes/remove-uml-e86feeabdd16c628.yaml releasenotes/notes/remove-vsphere-support-411c97b66bdcd264.yaml releasenotes/notes/remove-windows-support-0d280cc7c7fffc61.yaml releasenotes/notes/remove-xen-support-7cb932b7bc621269.yaml releasenotes/notes/removed-rgw-ae3d80c2eafc9319.yaml releasenotes/notes/rename-ceilometer-dbsync-eb7a1fa503085528.yaml releasenotes/notes/rename-tenant_name_discovery-1675a236bb51176b.yaml releasenotes/notes/replace-cinderclient-with-openstacksdk-c129982a98ecbdf2.yaml releasenotes/notes/report-metrics-shutoff-vms-b041bea41b39dca4.yaml releasenotes/notes/rgw-tls-config-options-d76125440231dbd2.yaml releasenotes/notes/save-rate-in-gnocchi-66244262bc4b7842.yaml releasenotes/notes/scan-domains-for-tenants-8f8c9edcb74cc173.yaml releasenotes/notes/selective-pipeline-notification-47e8a390b1c7dcc4.yaml releasenotes/notes/ship-yaml-files-33aa5852bedba7f0.yaml releasenotes/notes/single-thread-pipelines-f9e6ac4b062747fe.yaml releasenotes/notes/skip-duplicate-meter-def-0420164f6a95c50c.yaml releasenotes/notes/snmp-cpu-util-055cd7704056c1ce.yaml releasenotes/notes/snmp-diskio-samples-fc4b5ed5f19c096c.yaml releasenotes/notes/sql-query-optimisation-ebb2233f7a9b5d06.yaml releasenotes/notes/support-None-query-45abaae45f08eda4.yaml releasenotes/notes/support-cinder-volume-snapshot-backup-metering-d0a93b86bd53e803.yaml releasenotes/notes/support-lbaasv2-polling-c830dd49bcf25f64.yaml releasenotes/notes/support-meter-batch-recording-mongo-6c2bdf4fbb9764eb.yaml releasenotes/notes/support-multiple-meter-definition-files-e3ce1fa73ef2e1de.yaml releasenotes/notes/support-snmp-cpu-util-5c1c7afb713c1acd.yaml releasenotes/notes/support-unique-meter-query-221c6e0c1dc1b726.yaml releasenotes/notes/switch-to-oslo-privsep-b58f20a279f31bc0.yaml releasenotes/notes/thread-safe-matching-4a635fc4965c5d4c.yaml releasenotes/notes/threeads-process-pollsters-cbd22cca6f2effc4.yaml releasenotes/notes/tooz-coordination-system-d1054b9d1a5ddf32.yaml releasenotes/notes/transformer-ed4b1ea7d1752576.yaml releasenotes/notes/undeprecate-neutron-fwaas-9ad1b7fb10056e38.yaml releasenotes/notes/unify-timestamp-of-polled-data-fbfcff43cd2d04bc.yaml releasenotes/notes/use-glance-v2-in-image-pollsters-137a315577d5dc4c.yaml releasenotes/notes/use-notification-transport-url-489f3d31dc66c4d2.yaml releasenotes/notes/use-usable-metric-if-available-970ee58e8fdeece6.yaml releasenotes/notes/volume-metrics-01ddde0180bc21cb.yaml releasenotes/notes/volume-provider-pool-provider-metadata-3f1c2a9d7e5b4c60.yaml releasenotes/notes/zaqar-publisher-f7efa030b71731f4.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/2025.1.rst releasenotes/source/2025.2.rst releasenotes/source/2026.1.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/liberty.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po tools/__init__.py tools/send_test_data.py././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788964727.063128 ceilometer-27.0.0.0rc1/ceilometer.egg-info/dependency_links.txt0000664000175000017500000000000115250267567023225 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.0651278 ceilometer-27.0.0.0rc1/ceilometer.egg-info/entry_points.txt0000664000175000017500000002411315250267567022456 0ustar00zuulzuul[ceilometer.compute.virt] libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector [ceilometer.discover.central] alarm = ceilometer.alarm.discovery:AlarmDiscovery barbican = ceilometer.polling.discovery.non_openstack_credentials_discovery:NonOpenStackCredentialsDiscovery dns_zones = ceilometer.dns.discovery:ZoneDiscovery endpoint = ceilometer.polling.discovery.endpoint:EndpointDiscovery fip_services = ceilometer.network.services.discovery:FloatingIPDiscovery fw_policy = ceilometer.network.services.discovery:FirewallPolicyDiscovery fw_services = ceilometer.network.services.discovery:FirewallDiscovery images = ceilometer.image.discovery:ImagesDiscovery ipsec_connections = ceilometer.network.services.discovery:IPSecConnectionsDiscovery lb_services = ceilometer.load_balancer.discovery:LoadBalancerDiscovery manila_shares = ceilometer.share.discovery:ShareDiscovery tenant = ceilometer.polling.discovery.tenant:TenantDiscovery volume_backups = ceilometer.volume.discovery:VolumeBackupsDiscovery volume_pools = ceilometer.volume.discovery:VolumePoolsDiscovery volume_services = ceilometer.volume.discovery:VolumeServicesDiscovery volume_snapshots = ceilometer.volume.discovery:VolumeSnapshotsDiscovery volumes = ceilometer.volume.discovery:VolumeDiscovery vpn_services = ceilometer.network.services.discovery:VPNServicesDiscovery [ceilometer.discover.compute] local_instances = ceilometer.compute.discovery:InstanceDiscovery local_node = ceilometer.polling.discovery.localnode:LocalNodeDiscovery [ceilometer.discover.ipmi] local_node = ceilometer.polling.discovery.localnode:LocalNodeDiscovery [ceilometer.event.publisher] file = ceilometer.publisher.file:FilePublisher gnocchi = ceilometer.publisher.gnocchi:GnocchiPublisher http = ceilometer.publisher.http:HttpPublisher https = ceilometer.publisher.http:HttpPublisher notifier = ceilometer.publisher.messaging:EventNotifierPublisher test = ceilometer.publisher.test:TestPublisher zaqar = ceilometer.publisher.zaqar:ZaqarPublisher [ceilometer.event.trait_plugin] bitfield = ceilometer.event.trait_plugins:BitfieldTraitPlugin map = ceilometer.event.trait_plugins:MapTraitPlugin split = ceilometer.event.trait_plugins:SplitterTraitPlugin timedelta = ceilometer.event.trait_plugins:TimedeltaPlugin [ceilometer.notification.pipeline] event = ceilometer.pipeline.event:EventPipelineManager meter = ceilometer.pipeline.sample:SamplePipelineManager [ceilometer.poll.central] alarm.evaluation_result = ceilometer.alarm.aodh:EvaluationResultPollster dns.zone.recordsets = ceilometer.dns.designate:ZoneRecordsetCountPollster dns.zone.serial = ceilometer.dns.designate:ZoneSerialPollster dns.zone.status = ceilometer.dns.designate:ZoneStatusPollster dns.zone.ttl = ceilometer.dns.designate:ZoneTTLPollster image.size = ceilometer.image.glance:ImageSizePollster ip.floating = ceilometer.network.floatingip:FloatingIPPollster loadbalancer.operating = ceilometer.load_balancer.octavia:LoadBalancerOperatingStatusPollster loadbalancer.provisioning = ceilometer.load_balancer.octavia:LoadBalancerProvisioningStatusPollster manila.share.size = ceilometer.share.manila:ShareSizePollster manila.share.status = ceilometer.share.manila:ShareStatusPollster network.services.firewall = ceilometer.network.services.fwaas:FirewallPollster network.services.firewall.policy = ceilometer.network.services.fwaas:FirewallPolicyPollster network.services.vpn = ceilometer.network.services.vpnaas:VPNServicesPollster network.services.vpn.connections = ceilometer.network.services.vpnaas:IPSecConnectionsPollster radosgw.containers.objects = ceilometer.objectstore.rgw:ContainersObjectsPollster radosgw.containers.objects.size = ceilometer.objectstore.rgw:ContainersSizePollster radosgw.objects = ceilometer.objectstore.rgw:ObjectsPollster radosgw.objects.containers = ceilometer.objectstore.rgw:ObjectsContainersPollster radosgw.objects.size = ceilometer.objectstore.rgw:ObjectsSizePollster radosgw.usage = ceilometer.objectstore.rgw:UsagePollster storage.containers.objects = ceilometer.objectstore.swift:ContainersObjectsPollster storage.containers.objects.size = ceilometer.objectstore.swift:ContainersSizePollster storage.objects = ceilometer.objectstore.swift:ObjectsPollster storage.objects.containers = ceilometer.objectstore.swift:ObjectsContainersPollster storage.objects.size = ceilometer.objectstore.swift:ObjectsSizePollster volume.backup.size = ceilometer.volume.cinder:VolumeBackupSize volume.provider.pool.capacity.allocated = ceilometer.volume.cinder:VolumeProviderPoolCapacityAllocated volume.provider.pool.capacity.free = ceilometer.volume.cinder:VolumeProviderPoolCapacityFree volume.provider.pool.capacity.provisioned = ceilometer.volume.cinder:VolumeProviderPoolCapacityProvisioned volume.provider.pool.capacity.total = ceilometer.volume.cinder:VolumeProviderPoolCapacityTotal volume.provider.pool.capacity.virtual_free = ceilometer.volume.cinder:VolumeProviderPoolCapacityVirtualFree volume.service.health = ceilometer.volume.cinder:VolumeServiceHealthPollster volume.size = ceilometer.volume.cinder:VolumeSizePollster volume.snapshot.size = ceilometer.volume.cinder:VolumeSnapshotSize [ceilometer.poll.compute] cpu = ceilometer.compute.pollsters.instance_stats:CPUPollster disk.device.allocation = ceilometer.compute.pollsters.disk:PerDeviceAllocationPollster disk.device.capacity = ceilometer.compute.pollsters.disk:PerDeviceCapacityPollster disk.device.read.bytes = ceilometer.compute.pollsters.disk:PerDeviceReadBytesPollster disk.device.read.latency = ceilometer.compute.pollsters.disk:PerDeviceDiskReadLatencyPollster disk.device.read.requests = ceilometer.compute.pollsters.disk:PerDeviceReadRequestsPollster disk.device.usage = ceilometer.compute.pollsters.disk:PerDevicePhysicalPollster disk.device.write.bytes = ceilometer.compute.pollsters.disk:PerDeviceWriteBytesPollster disk.device.write.latency = ceilometer.compute.pollsters.disk:PerDeviceDiskWriteLatencyPollster disk.device.write.requests = ceilometer.compute.pollsters.disk:PerDeviceWriteRequestsPollster disk.ephemeral.size = ceilometer.compute.pollsters.disk:EphemeralSizePollster disk.root.size = ceilometer.compute.pollsters.disk:RootSizePollster memory = ceilometer.compute.pollsters.instance_stats:MemoryPollster memory.available = ceilometer.compute.pollsters.instance_stats:MemoryAvailablePollster memory.resident = ceilometer.compute.pollsters.instance_stats:MemoryResidentPollster memory.swap.in = ceilometer.compute.pollsters.instance_stats:MemorySwapInPollster memory.swap.out = ceilometer.compute.pollsters.instance_stats:MemorySwapOutPollster memory.usage = ceilometer.compute.pollsters.instance_stats:MemoryUsagePollster network.incoming.bytes = ceilometer.compute.pollsters.net:IncomingBytesPollster network.incoming.bytes.delta = ceilometer.compute.pollsters.net:IncomingBytesDeltaPollster network.incoming.bytes.rate = ceilometer.compute.pollsters.net:IncomingBytesRatePollster network.incoming.packets = ceilometer.compute.pollsters.net:IncomingPacketsPollster network.incoming.packets.drop = ceilometer.compute.pollsters.net:IncomingDropPollster network.incoming.packets.error = ceilometer.compute.pollsters.net:IncomingErrorsPollster network.outgoing.bytes = ceilometer.compute.pollsters.net:OutgoingBytesPollster network.outgoing.bytes.delta = ceilometer.compute.pollsters.net:OutgoingBytesDeltaPollster network.outgoing.bytes.rate = ceilometer.compute.pollsters.net:OutgoingBytesRatePollster network.outgoing.packets = ceilometer.compute.pollsters.net:OutgoingPacketsPollster network.outgoing.packets.drop = ceilometer.compute.pollsters.net:OutgoingDropPollster network.outgoing.packets.error = ceilometer.compute.pollsters.net:OutgoingErrorsPollster perf.cache.misses = ceilometer.compute.pollsters.instance_stats:PerfCacheMissesPollster perf.cache.references = ceilometer.compute.pollsters.instance_stats:PerfCacheReferencesPollster perf.cpu.cycles = ceilometer.compute.pollsters.instance_stats:PerfCPUCyclesPollster perf.instructions = ceilometer.compute.pollsters.instance_stats:PerfInstructionsPollster power.state = ceilometer.compute.pollsters.instance_stats:PowerStatePollster vcpus = ceilometer.compute.pollsters.instance_stats:VCPUsPollster [ceilometer.poll.ipmi] hardware.ipmi.current = ceilometer.ipmi.pollsters.sensor:CurrentSensorPollster hardware.ipmi.fan = ceilometer.ipmi.pollsters.sensor:FanSensorPollster hardware.ipmi.power = ceilometer.ipmi.pollsters.sensor:PowerSensorPollster hardware.ipmi.temperature = ceilometer.ipmi.pollsters.sensor:TemperatureSensorPollster hardware.ipmi.voltage = ceilometer.ipmi.pollsters.sensor:VoltageSensorPollster [ceilometer.sample.endpoint] _sample = ceilometer.telemetry.notifications:TelemetryIpc hardware.ipmi.current = ceilometer.ipmi.notifications.ironic:CurrentSensorNotification hardware.ipmi.fan = ceilometer.ipmi.notifications.ironic:FanSensorNotification hardware.ipmi.temperature = ceilometer.ipmi.notifications.ironic:TemperatureSensorNotification hardware.ipmi.voltage = ceilometer.ipmi.notifications.ironic:VoltageSensorNotification http.request = ceilometer.middleware:HTTPRequest http.response = ceilometer.middleware:HTTPResponse meter = ceilometer.meter.notifications:ProcessMeterNotifications [ceilometer.sample.publisher] file = ceilometer.publisher.file:FilePublisher gnocchi = ceilometer.publisher.gnocchi:GnocchiPublisher http = ceilometer.publisher.http:HttpPublisher https = ceilometer.publisher.http:HttpPublisher notifier = ceilometer.publisher.messaging:SampleNotifierPublisher opentelemetryhttp = ceilometer.publisher.opentelemetry_http:OpentelemetryHttpPublisher prometheus = ceilometer.publisher.prometheus:PrometheusPublisher tcp = ceilometer.publisher.tcp:TCPPublisher test = ceilometer.publisher.test:TestPublisher udp = ceilometer.publisher.udp:UDPPublisher zaqar = ceilometer.publisher.zaqar:ZaqarPublisher [console_scripts] ceilometer-agent-notification = ceilometer.cmd.agent_notification:main ceilometer-polling = ceilometer.cmd.polling:main ceilometer-rootwrap = oslo_rootwrap.cmd:main ceilometer-send-sample = ceilometer.cmd.sample:send_sample ceilometer-status = ceilometer.cmd.status:main ceilometer-upgrade = ceilometer.cmd.storage:upgrade [oslo.config.opts] ceilometer = ceilometer.opts:list_opts ceilometer-auth = ceilometer.opts:list_keystoneauth_opts ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.0621278 ceilometer-27.0.0.0rc1/ceilometer.egg-info/not-zip-safe0000664000175000017500000000000115250267567021405 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788964727.061128 ceilometer-27.0.0.0rc1/ceilometer.egg-info/pbr.json0000664000175000017500000000006015250267567020631 0ustar00zuulzuul{"git_version": "af3cbf7c7", "is_release": true}././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.0651278 ceilometer-27.0.0.0rc1/ceilometer.egg-info/requires.txt0000664000175000017500000000121015250267567021551 0ustar00zuulzuulxmltodict>=0.13.0 cachetools>=2.1.0 cotyledon>=1.3.0 futurist>=1.8.0 jsonpath-rw-ext>=1.1.3 lxml>=4.5.1 msgpack>=0.5.2 oslo.concurrency>=3.29.0 oslo.config>=8.6.0 oslo.i18n>=3.15.3 oslo.log>=3.36.0 oslo.reports>=1.18.0 oslo.rootwrap>=2.0.0 pbr>=2.0.0 oslo.messaging>=10.3.0 oslo.upgradecheck>=0.1.1 oslo.utils>=4.7.0 oslo.privsep>=1.32.0 keystoneauth1>=3.18.0 python-novaclient>=9.1.0 python-swiftclient>=3.2.0 PyYAML>=5.1 requests>=2.25.1 stevedore>=1.20.0 tenacity>=6.3.1 tooz>=1.47.0 oslo.cache>=1.26.0 gnocchiclient>=7.0.0 python-zaqarclient>=1.3.0 prometheus-client>=0.20.0 aodhclient>=3.8.0 awscurl>=0.40 botocore>=1.20.0 openstacksdk>=4.6.0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1788964727.066128 ceilometer-27.0.0.0rc1/ceilometer.egg-info/top_level.txt0000664000175000017500000000001315250267567021703 0ustar00zuulzuulceilometer ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2761276 ceilometer-27.0.0.0rc1/devstack/0000775000175000017500000000000015250267567015141 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2343566 ceilometer-27.0.0.0rc1/devstack/README.rst0000664000175000017500000000172115250267452016622 0ustar00zuulzuul=============================== Enabling Ceilometer in DevStack =============================== 1. Download Devstack:: git clone https://opendev.org/openstack/devstack cd devstack 2. Add this repo as an external repository in ``local.conf`` file:: [[local|localrc]] enable_plugin ceilometer https://opendev.org/openstack/ceilometer To use stable branches, make sure devstack is on that branch, and specify the branch name to enable_plugin, for example:: enable_plugin ceilometer https://opendev.org/openstack/ceilometer stable/mitaka There are some options, such as CEILOMETER_BACKEND, defined in ``ceilometer/devstack/settings``, they can be used to configure the installation of Ceilometer. If you don't want to use their default value, you can set a new one in ``local.conf``. Alternitvely you can modify copy and modify the sample ``local.conf`` located at ``ceilometer/devstack/local.conf.sample`` 3. Run ``stack.sh``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2181277 ceilometer-27.0.0.0rc1/devstack/files/0000775000175000017500000000000015250267567016243 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2771275 ceilometer-27.0.0.0rc1/devstack/files/rpms/0000775000175000017500000000000015250267567017224 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2343566 ceilometer-27.0.0.0rc1/devstack/files/rpms/ceilometer0000664000175000017500000000003015250267452021261 0ustar00zuulzuulselinux-policy-targeted ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2353568 ceilometer-27.0.0.0rc1/devstack/local.conf.sample0000664000175000017500000000266715250267452020366 0ustar00zuulzuul[[local|localrc]] # Common options # -------------- #RECLONE=True #FORCE=True #OFFLINE=True # HOST_IP should be set to an ip that is present on the host # e.g. the ip of eth0. This will be used to bind api endpoints and horizon. HOST_IP= # Minimal Contents # ---------------- # While ``stack.sh`` is happy to run without ``localrc``, devlife is better when # there are a few minimal variables set: # If the ``*_PASSWORD`` variables are not set here you will be prompted to enter # values for them by ``stack.sh``and they will be added to ``local.conf``. ADMIN_PASSWORD=password DATABASE_PASSWORD=$ADMIN_PASSWORD RABBIT_PASSWORD=$ADMIN_PASSWORD SERVICE_PASSWORD=$ADMIN_PASSWORD LOGFILE=$DEST/logs/stack.sh.log LOGDAYS=2 # the plugin line order matters but the placement in the file does not enable_plugin aodh https://opendev.org/openstack/aodh enable_plugin ceilometer https://opendev.org/openstack/ceilometer.git # Gnocchi settings # Gnocchi is optional but can be enabled by uncommenting CEILOMETER_BACKEND CEILOMETER_BACKEND=gnocchi # if gnocchi is not in LIBS_FROM_GIT it will install from pypi. # Currently this is broken with the latest gnocchi release 4.4.2 # so we need to install from git until # https://github.com/gnocchixyz/gnocchi/issues/1290 is resolved LIBS_FROM_GIT+=gnocchi # to control the version of gnocchi installed from git uncomment these options #GNOCCHI_BRANCH="master" #GNOCCHI_REPO=https://github.com/gnocchixyz/gnocchi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2353568 ceilometer-27.0.0.0rc1/devstack/plugin.sh0000664000175000017500000003305115250267452016766 0ustar00zuulzuul# Install and start **Ceilometer** service in devstack # # To enable Ceilometer in devstack add an entry to local.conf that # looks like # # [[local|localrc]] # enable_plugin ceilometer https://opendev.org/openstack/ceilometer # # By default all ceilometer services are started (see devstack/settings) # except for the ceilometer-aipmi service. To disable a specific service # use the disable_service function. # # NOTE: Currently, there are two ways to get the IPMI based meters in # OpenStack. One way is to configure Ironic conductor to report those meters # for the nodes managed by Ironic and to have Ceilometer notification # agent to collect them. Ironic by default does NOT enable that reporting # functionality. So in order to do so, users need to set the option of # conductor.send_sensor_data to true in the ironic.conf configuration file # for the Ironic conductor service, and also enable the # ceilometer-anotification service. # # The other way is to use Ceilometer ipmi agent only to get the IPMI based # meters. To make use of the Ceilometer ipmi agent, it must be explicitly # enabled with the following setting: # # enable_service ceilometer-aipmi # # To avoid duplicated meters, users need to make sure to set the # option of conductor.send_sensor_data to false in the ironic.conf # configuration file if the node on which Ceilometer ipmi agent is running # is also managed by Ironic. # # Several variables set in the localrc section adjust common behaviors # of Ceilometer (see within for additional settings): # # CEILOMETER_PIPELINE_INTERVAL: Seconds between pipeline processing runs. Default 300. # CEILOMETER_BACKENDS: List of database backends (e.g. 'gnocchi', 'sg-core', 'gnocchi,sg-core', 'none') # CEILOMETER_COORDINATION_URL: URL for group membership service provided by tooz. # CEILOMETER_EVENT_ALARM: Set to True to enable publisher for event alarming # Save trace setting XTRACE=$(set +o | grep xtrace) set -o xtrace # Support potential entry-points console scripts in VENV or not if [[ ${USE_VENV} = True ]]; then PROJECT_VENV["ceilometer"]=${CEILOMETER_DIR}.venv CEILOMETER_BIN_DIR=${PROJECT_VENV["ceilometer"]}/bin else CEILOMETER_BIN_DIR=$(get_python_exec_prefix) fi # Test if any Ceilometer services are enabled # is_ceilometer_enabled function is_ceilometer_enabled { [[ ,${ENABLED_SERVICES} =~ ,"ceilometer-" ]] && return 0 return 1 } function gnocchi_service_url { echo "$GNOCCHI_SERVICE_PROTOCOL://$GNOCCHI_SERVICE_HOST/metric" } # _ceilometer_install_redis() - Install the redis server and python lib. function _ceilometer_install_redis { if is_ubuntu; then install_package redis-server restart_service redis-server else # This will fail (correctly) where a redis package is unavailable install_package redis restart_service redis fi pip_install_gr redis } # Install required services for coordination function _ceilometer_prepare_coordination { if echo $CEILOMETER_COORDINATION_URL | grep -q '^memcached:'; then install_package memcached elif [[ "${CEILOMETER_COORDINATION_URL%%:*}" == "redis" || "${CACHE_BACKEND##*.}" == "redis" || "${CEILOMETER_BACKENDS}" =~ "gnocchi" ]]; then _ceilometer_install_redis fi } # Create ceilometer related accounts in Keystone function ceilometer_create_accounts { local gnocchi_service create_service_user "ceilometer" "admin" if is_service_enabled swift; then # Ceilometer needs ResellerAdmin role to access Swift account stats. get_or_add_user_project_role "ResellerAdmin" "ceilometer" $SERVICE_PROJECT_NAME fi if [[ "$CEILOMETER_BACKENDS" =~ "gnocchi" ]]; then create_service_user "gnocchi" gnocchi_service=$(get_or_create_service "gnocchi" "metric" "OpenStack Metric Service") get_or_create_endpoint $gnocchi_service \ "$REGION_NAME" \ "$(gnocchi_service_url)" \ "$(gnocchi_service_url)" \ "$(gnocchi_service_url)" fi } function install_gnocchi { echo_summary "Installing Gnocchi" if use_library_from_git "gnocchi"; then git_clone_by_name gnocchi pip_install -e ${GITDIR["gnocchi"]}[redis,${DATABASE_TYPE},keystone] else pip_install gnocchi[redis,${DATABASE_TYPE},keystone] fi } function configure_gnocchi { echo_summary "Configure Gnocchi" recreate_database gnocchi sudo install -d -o $STACK_USER -m 755 $GNOCCHI_CONF_DIR iniset $GNOCCHI_CONF DEFAULT debug "$ENABLE_DEBUG_LOG_LEVEL" iniset $GNOCCHI_CONF indexer url `database_connection_url gnocchi` iniset $GNOCCHI_CONF storage driver redis iniset $GNOCCHI_CONF storage redis_url redis://localhost:6379 iniset $GNOCCHI_CONF metricd metric_processing_delay "$GNOCCHI_METRICD_PROCESSING_DELAY" iniset $GNOCCHI_CONF api auth_mode keystone configure_keystone_authtoken_middleware $GNOCCHI_CONF gnocchi gnocchi-upgrade rm -f "$GNOCCHI_UWSGI_FILE" write_uwsgi_config "$GNOCCHI_UWSGI_FILE" "$GNOCCHI_WSGI" "/metric" "" "gnocchi" if [ -n "$GNOCCHI_COORDINATOR_URL" ]; then iniset $GNOCCHI_CONF coordination_url "$GNOCCHI_COORDINATOR_URL" fi } # Activities to do before ceilometer has been installed. function preinstall_ceilometer { echo_summary "Preinstall not in virtualenv context. Skipping." } # cleanup_ceilometer() - Remove residual data files, anything left over # from previous runs that a clean run would need to clean up function cleanup_ceilometer { sudo rm -f "$CEILOMETER_CONF_DIR"/* sudo rmdir "$CEILOMETER_CONF_DIR" } # Set configuration for storage backend. function _ceilometer_configure_storage_backend { # delete any "," characters used for delimiting individual backends before checking for "none" if [ $(echo "$CEILOMETER_BACKENDS" | tr -d ",") = 'none' ] ; then echo_summary "All Ceilometer backends seems disabled, set \$CEILOMETER_BACKENDS to select one." else head -n -1 $CEILOMETER_CONF_DIR/pipeline.yaml > $CEILOMETER_CONF_DIR/tmp ; mv $CEILOMETER_CONF_DIR/tmp $CEILOMETER_CONF_DIR/pipeline.yaml head -n -1 $CEILOMETER_CONF_DIR/event_pipeline.yaml > $CEILOMETER_CONF_DIR/tmp ; mv $CEILOMETER_CONF_DIR/tmp $CEILOMETER_CONF_DIR/event_pipeline.yaml BACKENDS=$(echo $CEILOMETER_BACKENDS | tr "," "\n") for CEILOMETER_BACKEND in ${BACKENDS[@]}; do if [ "$CEILOMETER_BACKEND" = 'gnocchi' ] ; then echo " - gnocchi://?archive_policy=${GNOCCHI_ARCHIVE_POLICY}&filter_project=service" >> $CEILOMETER_CONF_DIR/event_pipeline.yaml echo " - gnocchi://?archive_policy=${GNOCCHI_ARCHIVE_POLICY}&filter_project=service" >> $CEILOMETER_CONF_DIR/pipeline.yaml configure_gnocchi elif [ "$CEILOMETER_BACKEND" = 'sg-core' ] ; then echo " - tcp://127.0.0.1:4242" >> $CEILOMETER_CONF_DIR/event_pipeline.yaml echo " - tcp://127.0.0.1:4242" >> $CEILOMETER_CONF_DIR/pipeline.yaml else die $LINENO "Unable to configure unknown CEILOMETER_BACKEND $CEILOMETER_BACKEND" fi done fi } # Configure Ceilometer function configure_ceilometer { iniset_rpc_backend ceilometer $CEILOMETER_CONF iniset $CEILOMETER_CONF oslo_messaging_notifications topics "$CEILOMETER_NOTIFICATION_TOPICS" iniset $CEILOMETER_CONF DEFAULT debug "$ENABLE_DEBUG_LOG_LEVEL" iniset $CEILOMETER_CONF compute fetch_extra_metadata $CEILOMETER_COMPUTE_FETCH_EXTRA_METADATA if [[ -n "$CEILOMETER_COORDINATION_URL" ]]; then iniset $CEILOMETER_CONF coordination backend_url $CEILOMETER_COORDINATION_URL iniset $CEILOMETER_CONF notification workers $API_WORKERS fi iniset $CEILOMETER_CONF cache enabled $CEILOMETER_ENABLE_CACHE iniset $CEILOMETER_CONF cache backend $CACHE_BACKEND iniset $CEILOMETER_CONF cache memcache_servers $MEMCACHE_SERVERS # Install the policy file and declarative configuration files to # the conf dir. # NOTE(cdent): Do not make this a glob as it will conflict # with rootwrap installation done elsewhere and also clobber # ceilometer.conf settings that have already been made. # Anyway, explicit is better than implicit. cp $CEILOMETER_DIR/etc/ceilometer/polling_all.yaml $CEILOMETER_CONF_DIR/polling.yaml cp $CEILOMETER_DIR/ceilometer/pipeline/data/*.yaml $CEILOMETER_CONF_DIR if [ "$CEILOMETER_PIPELINE_INTERVAL" ]; then sed -i "s/interval:.*/interval: ${CEILOMETER_PIPELINE_INTERVAL}/" $CEILOMETER_CONF_DIR/polling.yaml fi if [ "$CEILOMETER_EVENT_ALARM" == "True" ]; then if ! grep -q '^ *- notifier://?topic=alarm.all$' $CEILOMETER_CONF_DIR/event_pipeline.yaml; then sed -i '/^ *publishers:$/,+1s|^\( *\)-.*$|\1- notifier://?topic=alarm.all\n&|' $CEILOMETER_CONF_DIR/event_pipeline.yaml fi fi # The compute and central agents need these credentials in order to # call out to other services' public APIs. configure_keystoneauth $CEILOMETER_CONF ceilometer service_credentials iniset $CEILOMETER_CONF service_credentials region_name $REGION_NAME _ceilometer_configure_storage_backend if is_service_enabled ceilometer-aipmi; then # Configure rootwrap for the ipmi agent configure_rootwrap ceilometer fi if [[ "$VIRT_DRIVER" = 'libvirt' ]]; then if ! getent group $LIBVIRT_GROUP >/dev/null; then sudo groupadd $LIBVIRT_GROUP fi add_user_to_group $STACK_USER $LIBVIRT_GROUP fi } # init_ceilometer() - Initialize etc. function init_ceilometer { # Nothing to do : } # Install Ceilometer. # The storage and coordination backends are installed here because the # virtualenv context is active at this point and python drivers need to be # installed. The context is not active during preinstall (when it would # otherwise makes sense to do the backend services). function install_ceilometer { if is_service_enabled ceilometer-acentral ceilometer-acompute ceilometer-anotification gnocchi-api gnocchi-metricd; then _ceilometer_prepare_coordination fi if [[ "$CEILOMETER_BACKENDS" =~ 'gnocchi' ]]; then install_gnocchi fi setup_develop $CEILOMETER_DIR sudo install -d -o $STACK_USER -m 755 $CEILOMETER_CONF_DIR } # start_ceilometer() - Start running processes, including screen function start_ceilometer { if [[ "$CEILOMETER_BACKENDS" =~ "gnocchi" ]] ; then run_process gnocchi-api "$(which uwsgi) --ini $GNOCCHI_UWSGI_FILE" "" run_process gnocchi-metricd "$CEILOMETER_BIN_DIR/gnocchi-metricd --config-file $GNOCCHI_CONF" wait_for_service 30 "$(gnocchi_service_url)" if is_service_enabled gnocchi-api; then $CEILOMETER_BIN_DIR/ceilometer-upgrade fi fi run_process ceilometer-acentral "$CEILOMETER_BIN_DIR/ceilometer-polling --polling-namespaces central --config-file $CEILOMETER_CONF" run_process ceilometer-aipmi "$CEILOMETER_BIN_DIR/ceilometer-polling --polling-namespaces ipmi --config-file $CEILOMETER_CONF" # run the notification agent after restarting apache as it needs # operational keystone if using gnocchi run_process ceilometer-anotification "$CEILOMETER_BIN_DIR/ceilometer-agent-notification --config-file $CEILOMETER_CONF" run_process ceilometer-acompute "$CEILOMETER_BIN_DIR/ceilometer-polling --polling-namespaces compute --config-file $CEILOMETER_CONF" $LIBVIRT_GROUP } # configure_tempest_for_ceilometer() - Configure all the tempest settings function configure_tempest_for_ceilometer { if is_service_enabled tempest; then iniset $TEMPEST_CONFIG telemetry alarm_granularity $CEILOMETER_ALARM_GRANULARITY iniset $TEMPEST_CONFIG telemetry alarm_threshold $CEILOMETER_ALARM_THRESHOLD iniset $TEMPEST_CONFIG telemetry alarm_metric_name $CEILOMETER_ALARM_METRIC_NAME iniset $TEMPEST_CONFIG telemetry alarm_aggregation_method $CEILOMETER_ALARM_AGGREGATION_METHOD local metric_backends=${CEILOMETER_BACKENDS//sg-core/prometheus} iniset $TEMPEST_CONFIG telemetry_services metric_backends $metric_backends fi } # stop_ceilometer() - Stop running processes function stop_ceilometer { # Kill the ceilometer and gnocchi services for serv in ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification gnocchi-api gnocchi-metricd; do stop_process $serv done } # This is the main for plugin.sh if is_service_enabled ceilometer; then if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then # Set up other services echo_summary "Configuring system services for Ceilometer" preinstall_ceilometer elif [[ "$1" == "stack" && "$2" == "install" ]]; then echo_summary "Installing Ceilometer" # Use stack_install_service here to account for virtualenv stack_install_service ceilometer elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then echo_summary "Configuring Ceilometer" configure_ceilometer # Get ceilometer keystone settings in place ceilometer_create_accounts elif [[ "$1" == "stack" && "$2" == "extra" ]]; then echo_summary "Initializing Ceilometer" # Tidy base for ceilometer init_ceilometer # Start the services start_ceilometer elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then configure_tempest_for_ceilometer fi if [[ "$1" == "unstack" ]]; then echo_summary "Shutting Down Ceilometer" stop_ceilometer fi if [[ "$1" == "clean" ]]; then echo_summary "Cleaning Ceilometer" cleanup_ceilometer fi fi # Restore xtrace $XTRACE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2363567 ceilometer-27.0.0.0rc1/devstack/settings0000664000175000017500000000576215250267452016727 0ustar00zuulzuul# turn on all the ceilometer services by default (except for ipmi pollster) # Pollsters enable_service ceilometer-acompute ceilometer-acentral # Notification Agent enable_service ceilometer-anotification # Default directories CEILOMETER_DIR=$DEST/ceilometer CEILOMETER_CONF_DIR=/etc/ceilometer CEILOMETER_CONF=$CEILOMETER_CONF_DIR/ceilometer.conf # Gnocchi is the default backind if both "CEILOMETER_BACKEND" # and "CEILOMETER_BACKENDS" are empty CEILOMETER_BACKEND=${CEILOMETER_BACKEND:""} if ! [[ "$CEILOMETER_BACKENDS" =~ "$CEILOMETER_BACKEND" ]]; then CEILOMETER_BACKENDS+=","$CEILOMETER_BACKEND fi CEILOMETER_BACKENDS=${CEILOMETER_BACKENDS:-"gnocchi"} if [[ "$CEILOMETER_BACKENDS" =~ "gnocchi" ]]; then enable_service gnocchi-api gnocchi-metricd fi if [[ "$CEILOMETER_BACKENDS" =~ "sg-core" ]]; then enable_service sg-core fi GITDIR["gnocchi"]=${GNOCCHI_DIR:-${DEST}/gnocchi} GITBRANCH["gnocchi"]=${GNOCCHI_BRANCH:-"master"} GITREPO["gnocchi"]=${GNOCCHI_REPO:-https://github.com/gnocchixyz/gnocchi} # Gnocchi default archive_policy for Ceilometer if [ -n "$GNOCCHI_ARCHIVE_POLICY_TEMPEST" ]; then GNOCCHI_ARCHIVE_POLICY=$GNOCCHI_ARCHIVE_POLICY_TEMPEST else GNOCCHI_ARCHIVE_POLICY=${GNOCCHI_ARCHIVE_POLICY:-ceilometer-low} fi GNOCCHI_CONF_DIR=${GNOCCHI_CONF_DIR:-/etc/gnocchi} GNOCCHI_CONF=${GNOCCHI_CONF:-${GNOCCHI_CONF_DIR}/gnocchi.conf} GNOCCHI_WSGI=gnocchi.wsgi.api:application GNOCCHI_COORDINATOR_URL=${CEILOMETER_COORDINATOR_URL:-redis://localhost:6379} GNOCCHI_METRICD_PROCESSING_DELAY=${GNOCCHI_METRICD_PROCESSING_DELAY:-5} GNOCCHI_UWSGI_FILE=${GNOCCHI_UWSGI_FILE:-${GNOCCHI_CONF_DIR}/uwsgi.ini} GNOCCHI_SERVICE_PROTOCOL=http GNOCCHI_SERVICE_HOST=${GNOCCHI_SERVICE_HOST:-${SERVICE_HOST}} # FIXME(sileht): put 300 by default to match the archive policy # when the gate job have overridden this. CEILOMETER_ALARM_GRANULARITY=${CEILOMETER_ALARM_GRANULARITY:-60} CEILOMETER_ALARM_AGGREGATION_METHOD=${CEILOMETER_ALARM_AGGREGATION_METHOD:-rate:mean} CEILOMETER_ALARM_METRIC_NAME=${CEILOMETER_ALARM_METRIC_NAME:-cpu} CEILOMETER_ALARM_THRESHOLD=${CEILOMETER_ALARM_THRESHOLD:-10000000} # NOTE(callumdickinson): Enable/disable fetching extra metadata from Nova API. # This is disabled by default in Ceilometer itself to reduce Nova API load when # this metadata is not required, but for the Telemetry Tempest Plugin's Prometheus # auto-scaling tests to pass we enable it by default in DevStack. CEILOMETER_COMPUTE_FETCH_EXTRA_METADATA=${CEILOMETER_COMPUTE_FETCH_EXTRA_METADATA:-true} # To enable OSprofiler change value of this variable to "notifications,profiler" CEILOMETER_NOTIFICATION_TOPICS=${CEILOMETER_NOTIFICATION_TOPICS:-notifications} CEILOMETER_COORDINATION_URL=${CEILOMETER_COORDINATION_URL:-redis://localhost:6379} CEILOMETER_PIPELINE_INTERVAL=${CEILOMETER_PIPELINE_INTERVAL:-} CEILOMETER_ENABLE_CACHE=${CEILOMETER_ENABLE_CACHE:-True} CEILOMETER_EVENT_ALARM=${CEILOMETER_EVENT_ALARM:-False} # Get rid of this before done. # Tell emacs to use shell-script-mode ## Local variables: ## mode: shell-script ## End: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2771275 ceilometer-27.0.0.0rc1/devstack/upgrade/0000775000175000017500000000000015250267567016570 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2363567 ceilometer-27.0.0.0rc1/devstack/upgrade/settings0000664000175000017500000000073415250267452020350 0ustar00zuulzuulregister_project_for_upgrade ceilometer devstack_localrc base enable_plugin ceilometer https://opendev.org/openstack/ceilometer devstack_localrc base enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification tempest devstack_localrc target enable_plugin ceilometer https://opendev.org/openstack/ceilometer devstack_localrc target enable_service ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification tempest ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2363567 ceilometer-27.0.0.0rc1/devstack/upgrade/shutdown.sh0000775000175000017500000000114515250267452020774 0ustar00zuulzuul#!/bin/bash # # set -o errexit source $GRENADE_DIR/grenaderc source $GRENADE_DIR/functions source $BASE_DEVSTACK_DIR/functions source $BASE_DEVSTACK_DIR/stackrc # needed for status directory source $BASE_DEVSTACK_DIR/lib/tls source $BASE_DEVSTACK_DIR/lib/apache # Locate the ceilometer plugin and get its functions CEILOMETER_DEVSTACK_DIR=$(dirname $(dirname $0)) source $CEILOMETER_DEVSTACK_DIR/plugin.sh set -o xtrace stop_ceilometer # ensure everything is stopped SERVICES_DOWN="ceilometer-acompute ceilometer-acentral ceilometer-aipmi ceilometer-anotification" ensure_services_stopped $SERVICES_DOWN ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2383566 ceilometer-27.0.0.0rc1/devstack/upgrade/upgrade.sh0000775000175000017500000000447115250267452020555 0ustar00zuulzuul#!/usr/bin/env bash # ``upgrade-ceilometer`` echo "*********************************************************************" echo "Begin $0" echo "*********************************************************************" # Clean up any resources that may be in use cleanup() { set +o errexit echo "*********************************************************************" echo "ERROR: Abort $0" echo "*********************************************************************" # Kill ourselves to signal any calling process trap 2; kill -2 $$ } trap cleanup SIGHUP SIGINT SIGTERM # Keep track of the grenade directory RUN_DIR=$(cd $(dirname "$0") && pwd) # Source params source $GRENADE_DIR/grenaderc # Import common functions source $GRENADE_DIR/functions # This script exits on an error so that errors don't compound and you see # only the first error that occurred. set -o errexit # Upgrade Ceilometer # ================== # Locate ceilometer devstack plugin, the directory above the # grenade plugin. CEILOMETER_DEVSTACK_DIR=$(dirname $(dirname $0)) # Get functions from current DevStack source $TARGET_DEVSTACK_DIR/functions source $TARGET_DEVSTACK_DIR/stackrc source $TARGET_DEVSTACK_DIR/lib/apache # Get ceilometer functions from devstack plugin source $CEILOMETER_DEVSTACK_DIR/settings # Print the commands being run so that we can see the command that triggers # an error. set -o xtrace # Install the target ceilometer source $CEILOMETER_DEVSTACK_DIR/plugin.sh stack install # calls upgrade-ceilometer for specific release upgrade_project ceilometer $RUN_DIR $BASE_DEVSTACK_BRANCH $TARGET_DEVSTACK_BRANCH # Migrate the database # NOTE(chdent): As we evolve BIN_DIR is likely to be defined, but # currently it is not. CEILOMETER_BIN_DIR=$(get_python_exec_prefix) $CEILOMETER_BIN_DIR/ceilometer-upgrade --skip-gnocchi-resource-types || die $LINENO "ceilometer-upgrade error" # Start Ceilometer start_ceilometer # Note(liamji): Disable the test for ceilometer-aipmi. # In the test environment, the impi is not ready and the service should fail. ensure_services_started ceilometer-acentral ceilometer-acompute ceilometer-anotification set +o xtrace echo "*********************************************************************" echo "SUCCESS: End $0" echo "*********************************************************************" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2781274 ceilometer-27.0.0.0rc1/doc/0000775000175000017500000000000015250267567014102 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2383566 ceilometer-27.0.0.0rc1/doc/dictionary.txt0000664000175000017500000000000015250267452016767 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2393568 ceilometer-27.0.0.0rc1/doc/requirements.txt0000664000175000017500000000016315250267452017357 0ustar00zuulzuulsphinx>=2.1.1 # BSD reno>=3.1.0 # Apache-2.0 os-api-ref>=1.4.0 # Apache-2.0 openstackdocstheme>=2.2.1 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2781274 ceilometer-27.0.0.0rc1/doc/source/0000775000175000017500000000000015250267567015402 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2801275 ceilometer-27.0.0.0rc1/doc/source/admin/0000775000175000017500000000000015250267567016472 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2393568 ceilometer-27.0.0.0rc1/doc/source/admin/index.rst0000664000175000017500000000100415250267452020317 0ustar00zuulzuul.. _admin: =================== Administrator Guide =================== Overview ======== .. toctree:: :maxdepth: 2 telemetry-system-architecture Configuration ============= .. toctree:: :maxdepth: 2 telemetry-data-collection telemetry-data-pipelines telemetry-best-practices telemetry-dynamic-pollster Data Types ========== .. toctree:: :maxdepth: 2 telemetry-measurements telemetry-events Management ========== .. toctree:: :maxdepth: 2 telemetry-troubleshooting-guide ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2393568 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-best-practices.rst0000664000175000017500000000232215250267452024134 0ustar00zuulzuulTelemetry best practices ~~~~~~~~~~~~~~~~~~~~~~~~ The following are some suggested best practices to follow when deploying and configuring the Telemetry service. Data collection --------------- #. The Telemetry service collects a continuously growing set of data. Not all the data will be relevant for an administrator to monitor. - Based on your needs, you can edit the ``polling.yaml`` and ``pipeline.yaml`` configuration files to include select meters to generate or process - By default, Telemetry service polls the service APIs every 10 minutes. You can change the polling interval on a per meter basis by editing the ``polling.yaml`` configuration file. .. warning:: If the polling interval is too short, it will likely increase the stress on the service APIs. #. If polling many resources or at a high frequency, you can add additional central and compute agents as necessary. The agents are designed to scale horizontally. For more information refer to the `high availability guide `_. .. note:: The High Availability Guide is a work in progress and is changing rapidly while testing continues. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2403567 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-data-collection.rst0000664000175000017500000002770015250267452024275 0ustar00zuulzuul.. _telemetry-data-collection: =============== Data collection =============== The main responsibility of Telemetry in OpenStack is to collect information about the system that can be used by billing systems or interpreted by analytic tooling. Collected data can be stored in the form of samples or events in the supported databases, which are listed in :ref:`telemetry-supported-databases`. The available data collection mechanisms are: Notifications Processing notifications from other OpenStack services, by consuming messages from the configured message queue system. Polling Retrieve information directly from the hypervisor or by using the APIs of other OpenStack services. Notifications ============= All OpenStack services send notifications about the executed operations or system state. Several notifications carry information that can be metered. For example, CPU time of a VM instance created by OpenStack Compute service. The notification agent is responsible for consuming notifications. This component is responsible for consuming from the message bus and transforming notifications into events and measurement samples. By default, the notification agent is configured to build both events and samples. To enable selective data models, set the required pipelines using `pipelines` option under the `[notification]` section. Additionally, the notification agent is responsible to send to any supported publisher target such as gnocchi or panko. These services persist the data in configured databases. The different OpenStack services emit several notifications about the various types of events that happen in the system during normal operation. Not all these notifications are consumed by the Telemetry service, as the intention is only to capture the billable events and notifications that can be used for monitoring or profiling purposes. The notifications handled are contained under the `ceilometer.sample.endpoint` namespace. .. note:: Some services require additional configuration to emit the notifications. Please see the :ref:`install_controller` for more details. .. _meter_definitions: Meter definitions ----------------- The Telemetry service collects a subset of the meters by filtering notifications emitted by other OpenStack services. You can find the meter definitions in a separate configuration file, called ``ceilometer/data/meters.d/meters.yaml``. This enables operators/administrators to add new meters to Telemetry project by updating the ``meters.yaml`` file without any need for additional code changes. .. note:: The ``meters.yaml`` file should be modified with care. Unless intended, do not remove any existing meter definitions from the file. Also, the collected meters can differ in some cases from what is referenced in the documentation. It also support loading multiple meter definition files and allow users to add their own meter definitions into several files according to different types of metrics under the directory of ``/etc/ceilometer/meters.d``. A standard meter definition looks like: .. code-block:: yaml --- metric: - name: 'meter name' event_type: 'event name' type: 'type of meter eg: gauge, cumulative or delta' unit: 'name of unit eg: MiB' volume: 'path to a measurable value eg: $.payload.size' resource_id: 'path to resource id eg: $.payload.id' project_id: 'path to project id eg: $.payload.owner' metadata: 'addiitonal key-value data describing resource' The definition above shows a simple meter definition with some fields, from which ``name``, ``event_type``, ``type``, ``unit``, and ``volume`` are required. If there is a match on the event type, samples are generated for the meter. The ``meters.yaml`` file contains the sample definitions for all the meters that Telemetry is collecting from notifications. The value of each field is specified by using JSON path in order to find the right value from the notification message. In order to be able to specify the right field you need to be aware of the format of the consumed notification. The values that need to be searched in the notification message are set with a JSON path starting with ``$.`` For instance, if you need the ``size`` information from the payload you can define it like ``$.payload.size``. A notification message may contain multiple meters. You can use ``*`` in the meter definition to capture all the meters and generate samples respectively. You can use wild cards as shown in the following example: .. code-block:: yaml --- metric: - name: $.payload.measurements.[*].metric.[*].name event_type: 'event_name.*' type: 'delta' unit: $.payload.measurements.[*].metric.[*].unit volume: payload.measurements.[*].result resource_id: $.payload.target user_id: $.payload.initiator.id project_id: $.payload.initiator.project_id In the above example, the ``name`` field is a JSON path with matching a list of meter names defined in the notification message. You can use complex operations on JSON paths. In the following example, ``volume`` and ``resource_id`` fields perform an arithmetic and string concatenation: .. code-block:: yaml --- metric: - name: 'compute.node.cpu.idle.percent' event_type: 'compute.metrics.update' type: 'gauge' unit: 'percent' volume: payload.metrics[?(@.name='cpu.idle.percent')].value * 100 resource_id: $.payload.host + "_" + $.payload.nodename You can use the ``timedelta`` plug-in to evaluate the difference in seconds between two ``datetime`` fields from one notification. .. code-block:: yaml --- metric: - name: 'compute.instance.booting.time' event_type: 'compute.instance.create.end' type: 'gauge' unit: 'sec' volume: fields: [$.payload.created_at, $.payload.launched_at] plugin: 'timedelta' project_id: $.payload.tenant_id resource_id: $.payload.instance_id .. _Polling-Configuration: Polling ======= The Telemetry service is intended to store a complex picture of the infrastructure. This goal requires additional information than what is provided by the events and notifications published by each service. Some information is not emitted directly, like resource usage of the VM instances. Therefore Telemetry uses another method to gather this data by polling the infrastructure including the APIs of the different OpenStack services and other assets, like hypervisors. The latter case requires closer interaction with the compute hosts. To solve this issue, Telemetry uses an agent based architecture to fulfill the requirements against the data collection. Configuration ------------- Polling rules are defined by the `polling.yaml` file. It defines the pollsters to enable and the interval they should be polled. Each source configuration encapsulates meter name matching which matches against the entry point of pollster. It also includes: polling interval determination, optional resource enumeration or discovery. All samples generated by polling are placed on the queue to be handled by the pipeline configuration loaded in the notification agent. The polling definition may look like the following:: --- sources: - name: 'source name' interval: 'how often the samples should be generated' meters: - 'meter filter' resources: - 'list of resource URLs' discovery: - 'list of discoverers' The *interval* parameter in the sources section defines the cadence of sample generation in seconds. Polling plugins are invoked according to each source's section whose *meters* parameter matches the plugin's meter name. Its matching logic functions the same as pipeline filtering. The optional *resources* section of a polling source allows a list of static resource URLs to be configured. An amalgamated list of all statically defined resources are passed to individual pollsters for polling. The optional *discovery* section of a polling source contains the list of discoverers. These discoverers can be used to dynamically discover the resources to be polled by the pollsters. If both *resources* and *discovery* are set, the final resources passed to the pollsters will be the combination of the dynamic resources returned by the discoverers and the static resources defined in the *resources* section. Agents ------ There are three types of agents supporting the polling mechanism, the ``compute agent``, the ``central agent``, and the ``IPMI agent``. Under the hood, all the types of polling agents are the same ``ceilometer-polling`` agent, except that they load different polling plug-ins (pollsters) from different namespaces to gather data. The following subsections give further information regarding the architectural and configuration details of these components. Running :command:`ceilometer-agent-compute` is exactly the same as: .. code-block:: console $ ceilometer-polling --polling-namespaces compute Running :command:`ceilometer-agent-central` is exactly the same as: .. code-block:: console $ ceilometer-polling --polling-namespaces central Running :command:`ceilometer-agent-ipmi` is exactly the same as: .. code-block:: console $ ceilometer-polling --polling-namespaces ipmi Compute agent ~~~~~~~~~~~~~ This agent is responsible for collecting resource usage data of VM instances on individual compute nodes within an OpenStack deployment. This mechanism requires a closer interaction with the hypervisor, therefore a separate agent type fulfills the collection of the related meters, which is placed on the host machines to retrieve this information locally. A Compute agent instance has to be installed on each and every compute node, installation instructions can be found in the :ref:`install_compute` section in the Installation Tutorials and Guides. The list of supported hypervisors can be found in :ref:`telemetry-supported-hypervisors`. The Compute agent uses the API of the hypervisor installed on the compute hosts. Therefore, the supported meters may be different in case of each virtualization back end, as each inspection tool provides a different set of meters. The list of collected meters can be found in :ref:`telemetry-compute-meters`. The support column provides the information about which meter is available for each hypervisor supported by the Telemetry service. Central agent ~~~~~~~~~~~~~ This agent is responsible for polling public REST APIs to retrieve additional information on OpenStack resources not already surfaced via notifications. Some of the services polled with this agent are: - OpenStack Networking - OpenStack Object Storage - OpenStack Block Storage To install and configure this service use the :ref:`install_rdo` section in the Installation Tutorials and Guides. Although Ceilometer has a set of default polling agents, operators can add new pollsters dynamically via the dynamic pollsters subsystem :ref:`telemetry_dynamic_pollster`. .. _telemetry-ipmi-agent: IPMI agent ~~~~~~~~~~ This agent is responsible for collecting IPMI sensor data and Intel Node Manager data on individual compute nodes within an OpenStack deployment. This agent requires an IPMI capable node with the ipmitool utility installed, which is commonly used for IPMI control on various Linux distributions. An IPMI agent instance could be installed on each and every compute node with IPMI support, except when the node is managed by the Bare metal service and the ``conductor.send_sensor_data`` option is set to ``true`` in the Bare metal service. It is no harm to install this agent on a compute node without IPMI support, as the agent checks for the hardware and if IPMI support is not available, returns empty data. It is suggested that you install the IPMI agent only on an IPMI capable node for performance reasons. The list of collected meters can be found in :ref:`telemetry-bare-metal-service`. .. note:: Do not deploy both the IPMI agent and the Bare metal service on one compute node. If ``conductor.send_sensor_data`` is set, this misconfiguration causes duplicated IPMI sensor samples. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2413566 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-data-pipelines.rst0000664000175000017500000002554515250267452024137 0ustar00zuulzuul.. _telemetry-data-pipelines: ============================= Data processing and pipelines ============================= The mechanism by which data is processed is called a pipeline. Pipelines, at the configuration level, describe a coupling between sources of data and the corresponding sinks for publication of data. This functionality is handled by the notification agents. A source is a producer of data: ``samples`` or ``events``. In effect, it is a set of notification handlers emitting datapoints for a set of matching meters and event types. Each source configuration encapsulates name matching and mapping to one or more sinks for publication. A sink, on the other hand, is a consumer of data, providing logic for the publication of data emitted from related sources. In effect, a sink describes a list of one or more publishers. .. _telemetry-pipeline-configuration: Pipeline configuration ~~~~~~~~~~~~~~~~~~~~~~ The notification agent supports two pipelines: one that handles samples and another that handles events. The pipelines can be enabled and disabled by setting `pipelines` option in the `[notifications]` section. The actual configuration of each pipelines is, by default, stored in separate configuration files: ``pipeline.yaml`` and ``event_pipeline.yaml``. The location of the configuration files can be set by the ``pipeline_cfg_file`` and ``event_pipeline_cfg_file`` options listed in :ref:`configuring` The meter pipeline definition looks like: .. code-block:: yaml --- sources: - name: 'source name' meters: - 'meter filter' sinks: - 'sink name' sinks: - name: 'sink name' publishers: - 'list of publishers' There are several ways to define the list of meters for a pipeline source. The list of valid meters can be found in :ref:`telemetry-measurements`. There is a possibility to define all the meters, or just included or excluded meters, with which a source should operate: - To include all meters, use the ``*`` wildcard symbol. It is highly advisable to select only the meters that you intend on using to avoid flooding the metering database with unused data. - To define the list of meters, use either of the following: - To define the list of included meters, use the ``meter_name`` syntax. - To define the list of excluded meters, use the ``!meter_name`` syntax. .. note:: The OpenStack Telemetry service does not have any duplication check between pipelines, and if you add a meter to multiple pipelines then it is assumed the duplication is intentional and may be stored multiple times according to the specified sinks. The above definition methods can be used in the following combinations: - Use only the wildcard symbol. - Use the list of included meters. - Use the list of excluded meters. - Use wildcard symbol with the list of excluded meters. .. note:: At least one of the above variations should be included in the meters section. Included and excluded meters cannot co-exist in the same pipeline. Wildcard and included meters cannot co-exist in the same pipeline definition section. The publishers section contains the list of publishers, where the samples data should be sent. Similarly, the event pipeline definition looks like: .. code-block:: yaml --- sources: - name: 'source name' events: - 'event filter' sinks: - 'sink name' sinks: - name: 'sink name' publishers: - 'list of publishers' The event filter uses the same filtering logic as the meter pipeline. .. _publishing: Publishers ---------- The Telemetry service provides several transport methods to transfer the data collected to an external system. The consumers of this data are widely different, like monitoring systems, for which data loss is acceptable and billing systems, which require reliable data transportation. Telemetry provides methods to fulfill the requirements of both kind of systems. The publisher component makes it possible to save the data into persistent storage through the message bus or to send it to one or more external consumers. One chain can contain multiple publishers. To solve this problem, the multi-publisher can be configured for each data point within the Telemetry service, allowing the same technical meter or event to be published multiple times to multiple destinations, each potentially using a different transport. The following publisher types are supported: gnocchi (default) ````````````````` When the gnocchi publisher is enabled, measurement and resource information is pushed to gnocchi for time-series optimized storage. Gnocchi must be registered in the Identity service as Ceilometer discovers the exact path via the Identity service. More details on how to enable and configure gnocchi can be found on its `official documentation page `__. prometheus `````````` Metering data can be send to the `pushgateway `__ of Prometheus by using: ``prometheus://pushgateway-host:9091/metrics/job/openstack-telemetry`` With this publisher, timestamp are not sent to Prometheus due to Prometheus Pushgateway design. All timestamps are set at the time it scrapes the metrics from the Pushgateway and not when the metric was polled on the OpenStack services. In order to get timeseries in Prometheus that looks like the reality (but with the lag added by the Prometheus scrapping mechanism). The `scrape_interval` for the pushgateway must be lower and a multiple of the Ceilometer polling interval. You can read more `here `__ Push semantics and grouping --------------------------- The Prometheus Pushgateway stores metrics per "group" defined by the job name and optional grouping labels that are embedded in the URL path after the job segment. When a metric family (i.e. all time series sharing the same metric name) is pushed again to the same group, the Pushgateway replaces that entire family with the one contained in the current payload. Ceilometer often pushes metrics in subsets per poll. To prevent a later push of the same metric name from replacing time series belonging to other resources, the Ceilometer Prometheus publisher applies the following behavior: - If your pipeline URL already contains grouping labels (e.g. ``/metrics/job/openstack-telemetry/region/r1``), the publisher respects them and sends a single push to that group. - If your pipeline URL only specifies the job (i.e. ``/metrics/job/openstack-telemetry`` with no grouping labels), the publisher automatically pushes per resource and appends a grouping label using the resource identifier, effectively pushing to ``/metrics/job//resource_id/``. This isolates the pushes for each resource so that subsequent pushes of the same metric name do not override series for other resources. If you prefer an alternative grouping strategy (e.g., by region or cell), you can explicitly include the desired grouping labels in the pipeline URL path, such as: ``prometheus://pushgateway:9091/metrics/job/openstack-telemetry/region/r1``. Due to this, this is not recommended to use this publisher for billing purpose as timestamps in Prometheus will not be exact. notifier ```````` The notifier publisher can be specified in the form of ``notifier://?option1=value1&option2=value2``. It emits data over AMQP using oslo.messaging. Any consumer can then subscribe to the published topic for additional processing. The following customization options are available: ``per_meter_topic`` The value of this parameter is 1. It is used for publishing the samples on additional ``metering_topic.sample_name`` topic queue besides the default ``metering_topic`` queue. ``policy`` Used for configuring the behavior for the case, when the publisher fails to send the samples, where the possible predefined values are: default Used for waiting and blocking until the samples have been sent. drop Used for dropping the samples which are failed to be sent. queue Used for creating an in-memory queue and retrying to send the samples on the queue in the next samples publishing period (the queue length can be configured with ``max_queue_length``, where 1024 is the default value). ``topic`` The topic name of the queue to publish to. Setting this will override the default topic defined by ``metering_topic`` and ``event_topic`` options. This option can be used to support multiple consumers. udp ``` This publisher can be specified in the form of ``udp://:/``. It emits metering data over UDP. file ```` The file publisher can be specified in the form of ``file://path?option1=value1&option2=value2``. This publisher records metering data into a file. .. note:: If a file name and location is not specified, the ``file`` publisher does not log any meters, instead it logs a warning message in the configured log file for Telemetry. The following options are available for the ``file`` publisher: ``max_bytes`` When this option is greater than zero, it will cause a rollover. When the specified size is about to be exceeded, the file is closed and a new file is silently opened for output. If its value is zero, rollover never occurs. ``backup_count`` If this value is non-zero, an extension will be appended to the filename of the old log, as '.1', '.2', and so forth until the specified value is reached. The file that is written and contains the newest data is always the one that is specified without any extensions. ``json`` If this option is present, will force ceilometer to write json format into the file. http ```` The Telemetry service supports sending samples to an external HTTP target. The samples are sent without any modification. To set this option as the notification agents' target, set ``http://`` as a publisher endpoint in the pipeline definition files. The HTTP target should be set along with the publisher declaration. For example, additional configuration options can be passed in: ``http://localhost:80/?option1=value1&option2=value2`` The following options are available: ``timeout`` The number of seconds before HTTP request times out. ``max_retries`` The number of times to retry a request before failing. ``batch`` If false, the publisher will send each sample and event individually, whether or not the notification agent is configured to process in batches. ``verify_ssl`` If false, the ssl certificate verification is disabled. The default publisher is ``gnocchi``, without any additional options specified. A sample ``publishers`` section in the ``/etc/ceilometer/pipeline.yaml`` looks like the following: .. code-block:: yaml publishers: - gnocchi:// - udp://10.0.0.2:1234 - notifier://?policy=drop&max_queue_length=512&topic=custom_target ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2423568 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-dynamic-pollster.rst0000664000175000017500000011473415250267452024525 0ustar00zuulzuul.. _telemetry_dynamic_pollster: Introduction to dynamic pollster subsystem ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The dynamic pollster feature allows system administrators to create/update REST API pollsters on the fly (without changing code). The system reads YAML configures that are found in ``pollsters_definitions_dirs`` parameter, which has the default at ``/etc/ceilometer/pollsters.d``. Operators can use a single file per dynamic pollster or multiple dynamic pollsters per file. Current limitations of the dynamic pollster system -------------------------------------------------- Currently, the following types of APIs are not supported by the dynamic pollster system: * Tenant APIs: Tenant APIs are the ones that need to be polled in a tenant fashion. This feature is "a nice" to have, but is currently not implemented. The dynamic pollsters system configuration (for OpenStack APIs) --------------------------------------------------------------- Each YAML file in the dynamic pollster feature can use the following attributes to define a dynamic pollster: .. warning:: Caution: Ceilometer does not accept complex value data structure for ``value`` and ``metadata`` configurations. Therefore, if you are extracting a complex data structure (Object, list, map, or others), you can take advantage of the ``Operations on extracted attributes`` feature to transform the object into a simple value (string or number) * ``name``: mandatory field. It specifies the name/key of the dynamic pollster. For instance, a pollster for magnum can use the name ``dynamic.magnum.cluster``; * ``sample_type``: mandatory field; it defines the sample type. It must be one of the values: ``gauge``, ``delta``, ``cumulative``; * ``unit``: mandatory field; defines the unit of the metric that is being collected. For magnum, for instance, one can use ``cluster`` as the unit or some other meaningful String value; * ``value_attribute``: mandatory attribute; defines the attribute in the response from the URL of the component being polled. We also accept nested values dictionaries. To use a nested value one can simply use ``attribute1.attribute2..lastattribute``. It is also possible to reference the sample itself using ``"." (dot)``; the self reference of the sample is interesting in cases when the attribute might not exist. Therefore, together with the operations options, one can first check if it exist before retrieving it (example: ``". | value['some_field'] if 'some_field' in value else ''"``). In our magnum example, we can use ``status`` as the value attribute; * ``endpoint_type``: mandatory field; defines the endpoint type that is used to discover the base URL of the component to be monitored; for magnum, one can use ``container-infra``. Other values are accepted such as ``volume`` for cinder endpoints, ``object-store`` for swift, and so on; * ``url_path``: mandatory attribute. It defines the path of the request that we execute on the endpoint to gather data. For example, to gather data from magnum, one can use ``v1/clusters/detail``; * ``metadata_fields``: optional field. It is a list of all fields that the response of the request executed with ``url_path`` that we want to retrieve. To use a nested value one can simply use ``attribute1.attribute2..lastattribute``. As an example, for magnum, one can use the following values: .. code-block:: yaml metadata_fields: - "labels" - "updated_at" - "keypair" - "master_flavor_id" - "api_address" - "master_addresses" - "node_count" - "docker_volume_size" - "master_count" - "node_addresses" - "status_reason" - "coe_version" - "cluster_template_id" - "name" - "stack_id" - "created_at" - "discovery_url" - "container_version" * ``skip_sample_values``: optional field. It defines the values that might come in the ``value_attribute`` that we want to ignore. For magnun, one could for instance, ignore some of the status it has for clusters. Therefore, data is not gathered for clusters in the defined status. .. code-block:: yaml skip_sample_values: - "CREATE_FAILED" - "DELETE_FAILED" * ``value_mapping``: optional attribute. It defines a mapping for the values that the dynamic pollster is handling. This is the actual value that is sent to Gnocchi or other backends. If there is no mapping specified, we will use the raw value that is obtained with the use of ``value_attribute``. An example for magnum, one can use: .. code-block:: yaml value_mapping: CREATE_IN_PROGRESS: "0" CREATE_FAILED: "1" CREATE_COMPLETE: "2" UPDATE_IN_PROGRESS: "3" UPDATE_FAILED: "4" UPDATE_COMPLETE: "5" DELETE_IN_PROGRESS: "6" DELETE_FAILED: "7" DELETE_COMPLETE: "8" RESUME_COMPLETE: "9" RESUME_FAILED: "10" RESTORE_COMPLETE: "11" ROLLBACK_IN_PROGRESS: "12" ROLLBACK_FAILED: "13" ROLLBACK_COMPLETE: "14" SNAPSHOT_COMPLETE: "15" CHECK_COMPLETE: "16" ADOPT_COMPLETE: "17" * ``default_value``: optional parameter. The default value for the value mapping in case the variable value receives data that is not mapped to something in the ``value_mapping`` configuration. This attribute is only used when ``value_mapping`` is defined. Moreover, it has a default of ``-1``. * ``metadata_mapping``: optional parameter. The map used to create new metadata fields. The key is a metadata name that exists in the response of the request we make, and the value of this map is the new desired metadata field that will be created with the content of the metadata that we are mapping. The ``metadata_mapping`` can be created as follows: .. code-block:: yaml metadata_mapping: name: "display_name" some_attribute: "new_attribute_name" * ``preserve_mapped_metadata``: optional parameter. It indicates if we preserve the old metadata name when it gets mapped to a new one. The default value is ``True``. * ``response_entries_key``: optional parameter. This value is used to define the "key" of the response that will be used to look-up the entries used in the dynamic pollster processing. If no ``response_entries_key`` is informed by the operator, we will use the first we find. Moreover, if the response contains a list, instead of an object where one of its attributes is a list of entries, we use the list directly. Therefore, this option will be ignored when the API is returning the list/array of entries to be processed directly. We also accept nested values dictionaries. To use a nested value one can simply use ``attribute1.attribute2..lastattribute`` * ``user_id_attribute``: optional parameter. The default value is ``user_id``. The name of the attribute in the entries that are processed from ``response_entries_key`` elements that will be mapped to ``user_id`` attribute that is sent to Gnocchi. * ``project_id_attribute``: optional parameter. The default value is ``project_id``. The name of the attribute in the entries that are processed from ``response_entries_key`` elements that will be mapped to ``project_id`` attribute that is sent to Gnocchi. * ``resource_id_attribute``: optional parameter. The default value is ``id``. The name of the attribute in the entries that are processed from ``response_entries_key`` elements that will be mapped to ``id`` attribute that is sent to Gnocchi. * ``headers``: optional parameter. It is a map (similar to the metadata_mapping) of key and value that can be used to customize the header of the request that is executed against the URL. This configuration works for both OpenStack and non-OpenStack dynamic pollster configuration. .. code-block:: yaml headers: "x-openstack-nova-api-version": "2.46" * ``timeout``: optional parameter. Defines the request timeout for the requests executed by the dynamic pollsters to gather data. The default timeout value is 30 seconds. If it is set to `None`, this means that the request never times out on the client side. Therefore, one might have problems if the server never closes the connection. The pollsters are executed serially, one after the other. Therefore, if the request hangs, all pollsters (including the non-dynamic ones) will stop executing. * ``namespaces``: optional parameter. Defines the namespaces (running ceilometer instances) where the pollster will be instantiated. This parameter accepts a single string value or a list of strings. The default value is `central`. The complete YAML configuration to gather data from Magnum (that has been used as an example) is the following: .. code-block:: yaml --- - name: "dynamic.magnum.cluster" sample_type: "gauge" unit: "cluster" value_attribute: "status" endpoint_type: "container-infra" url_path: "v1/clusters/detail" metadata_fields: - "labels" - "updated_at" - "keypair" - "master_flavor_id" - "api_address" - "master_addresses" - "node_count" - "docker_volume_size" - "master_count" - "node_addresses" - "status_reason" - "coe_version" - "cluster_template_id" - "name" - "stack_id" - "created_at" - "discovery_url" - "container_version" value_mapping: CREATE_IN_PROGRESS: "0" CREATE_FAILED: "1" CREATE_COMPLETE: "2" UPDATE_IN_PROGRESS: "3" UPDATE_FAILED: "4" UPDATE_COMPLETE: "5" DELETE_IN_PROGRESS: "6" DELETE_FAILED: "7" DELETE_COMPLETE: "8" RESUME_COMPLETE: "9" RESUME_FAILED: "10" RESTORE_COMPLETE: "11" ROLLBACK_IN_PROGRESS: "12" ROLLBACK_FAILED: "13" ROLLBACK_COMPLETE: "14" SNAPSHOT_COMPLETE: "15" CHECK_COMPLETE: "16" ADOPT_COMPLETE: "17" We can also replicate and enhance some hardcoded pollsters. For instance, the pollster to gather VPN connections. Currently, it is always persisting `1` for all of the VPN connections it finds. However, the VPN connection can have multiple statuses, and we should normally only bill for active resources, and not resources on `ERROR` states. An example to gather VPN connections data is the following (this is just an example, and one can adapt and configure as he/she desires): .. code-block:: yaml --- - name: "dynamic.network.services.vpn.connection" sample_type: "gauge" unit: "ipsec_site_connection" value_attribute: "status" endpoint_type: "network" url_path: "v2.0/vpn/ipsec-site-connections" metadata_fields: - "name" - "vpnservice_id" - "description" - "status" - "peer_address" value_mapping: ACTIVE: "1" metadata_mapping: name: "display_name" default_value: 0 * ``response_handlers``: optional parameter. Defines the response handlers used to handle the response. For now, the supported values are: ``json``: This handler will interpret the response as a `JSON` and will convert it to a `dictionary` which can be manipulated using the operations options when mapping the attributes: .. code-block:: yaml --- - name: "dynamic.json.response" sample_type: "gauge" [...] response_handlers: - json Response to handle: .. code-block:: json { "test": { "list": [1, 2, 3] } } Response handled: .. code-block:: python { 'test': { 'list': [1, 2, 3] } } ``xml``: This handler will interpret the response as an `XML` and will convert it to a `dictionary` which can be manipulated using the operations options when mapping the attributes: .. code-block:: yaml --- - name: "dynamic.json.response" sample_type: "gauge" [...] response_handlers: - xml Response to handle: .. code-block:: xml 1 2 3 Response handled: .. code-block:: python { 'test': { 'list': [1, 2, 3] } } ``text``: This handler will interpret the response as a `PlainText` and will convert it to a `dictionary` which can be manipulated using the operations options when mapping the attributes: .. code-block:: yaml --- - name: "dynamic.json.response" sample_type: "gauge" [...] response_handlers: - text Response to handle: .. code-block:: text Plain text response Response handled: .. code-block:: python { 'out': "Plain text response" } They can be used together or individually. If not defined, the `default` value will be `json`. If you set 2 or more response handlers, the first configured handler will be used to try to handle the response, if it is not possible, a `DEBUG` log message will be displayed, then the next will be used and so on. If no configured handler was able to handle the response, an empty dict will be returned and a `WARNING` log will be displayed to warn operators that the response was not able to be handled by any configured handler. The dynamic pollsters system configuration (for non-OpenStack APIs) ------------------------------------------------------------------- The dynamic pollster system can also be used for non-OpenStack APIs. to configure non-OpenStack APIs, one can use all but one attribute of the Dynamic pollster system. The attribute that is not supported is the ``endpoint_type``. The dynamic pollster system for non-OpenStack APIs is activated automatically when one uses the configurations ``module``. The extra parameters (in addition to the original ones) that are available when using the Non-OpenStack dynamic pollster sub-subsystem are the following: * ``module``: required parameter. It is the python module name that Ceilometer has to load to use the authentication object when executing requests against the API. For instance, if one wants to create a pollster to gather data from RadosGW, he/she can use the ``awsauth`` python module. * ``authentication_object``: mandatory parameter. The name of the class that we can find in the ``module`` that Ceilometer will use as the authentication object in the request. For instance, when using the ``awsauth`` python module to gather data from RadosGW, one can use the authentication object as ``S3Auth``. * ``authentication_parameters``: optional parameter. It is a comma separated value that will be used to instantiate the ``authentication_object``. For instance, if we gather data from RadosGW, and we use the ``S3Auth`` class, the ``authentication_parameters`` can be configured as ``, rados_gw_secret_key, rados_gw_host_name``. * ``barbican_secret_id``: optional parameter. The Barbican secret ID, from which, Ceilometer can retrieve the comma separated values of the ``authentication_parameters``. As follows we present an example on how to convert the hard-coded pollster for `radosgw.api.request` metric to the dynamic pollster model: .. code-block:: yaml --- - name: "dynamic.radosgw.api.request" sample_type: "gauge" unit: "request" value_attribute: "total.ops" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ",," user_id_attribute: "user" project_id_attribute: "user" resource_id_attribute: "user" response_entries_key: "summary" We can take that example a bit further, and instead of gathering the `total .ops` variable, which counts for all the requests (even the unsuccessful ones), we can use the `successful_ops`. .. code-block:: yaml --- - name: "dynamic.radosgw.api.request.successful_ops" sample_type: "gauge" unit: "request" value_attribute: "total.successful_ops" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ", ," user_id_attribute: "user" project_id_attribute: "user" resource_id_attribute: "user" response_entries_key: "summary" The dynamic pollsters system configuration (for local host commands) -------------------------------------------------------------------- The dynamic pollster system can also be used for local host commands, these commands must be installed in the system that is running the Ceilometer compute agent. To configure local hosts commands, one can use all but two attributes of the Dynamic pollster system. The attributes that are not supported are the ``endpoint_type`` and ``url_path``. The dynamic pollster system for local host commands is activated automatically when one uses the configuration ``host_command``. The extra parameter (in addition to the original ones) that is available when using the local host commands dynamic pollster sub-subsystem is the following: * ``host_command``: required parameter. It is the host command that will be executed in the same host the Ceilometer dynamic pollster agent is running. The output of the command will be processed by the pollster and stored in the configured backend. As follows we present an example on how to use the local host command: .. code-block:: yaml --- - name: "dynamic.host.command" sample_type: "gauge" unit: "request" value_attribute: "value" response_entries_key: "test" host_command: "echo 'id1_uid1_pid1meta-data-to-store1'" metadata_fields: - "meta" response_handlers: - xml To execute multi page host commands, the `next_sample_url_attribute` must generate the next sample command, like the following example: .. code-block:: yaml --- - name: "dynamic.s3.objects.size" sample_type: "gauge" unit: "request" value_attribute: "Size" project_id_attribute: "Owner.ID" user_id_attribute: "Owner.ID" resource_id_attribute: "Key" response_entries_key: "Contents" host_command: "aws s3api list-objects" next_sample_url_attribute: NextToken | 'aws s3api list-objects --starting-token "' + value + '"' Operations on extracted attributes ---------------------------------- The dynamic pollster system can execute Python operations to transform the attributes that are extracted from the JSON response that the system handles. One example of use case is the RadosGW that uses as the username (which is normally mapped to the Gnocchi resource_id). With this feature (operations on extracted attributes), one can create configurations in the dynamic pollster to clean/normalize that variable. It is as simple as defining `resource_id_attribute: "user | value.split('$')[0].strip()"` The operations are separated by `|` symbol. The first element of the expression is the key to be retrieved from the JSON object. The other elements are operations that can be applied to the `value` variable. The value variable is the variable we use to hold the data being extracted. The previous example can be rewritten as: `resource_id_attribute: "user | value.split ('$') | value[0] | value.strip()"` As follows we present a complete configuration for a RadosGW dynamic pollster that is removing the `$` symbol, and getting the first part of the String. .. code-block:: yaml --- - name: "dynamic.radosgw.api.request.successful_ops" sample_type: "gauge" unit: "request" value_attribute: "total.successful_ops" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ",," user_id_attribute: "user | value.split ('$') | value[0]" project_id_attribute: "user | value.split ('$') | value[0]" resource_id_attribute: "user | value.split ('$') | value[0]" response_entries_key: "summary" The Dynamic pollster configuration options that support this feature are the following: * value_attribute * response_entries_key * user_id_attribute * project_id_attribute * resource_id_attribute Multi metric dynamic pollsters (handling attribute values with list of objects) ------------------------------------------------------------------------------- The initial idea for this feature comes from the `categories` fields that we can find in the `summary` object of the RadosGW API. Each user has a `categories` attribute in the response; in the `categories` list, we can find the object that presents in a granular fashion the consumption of different RadosGW API operations such as GET, PUT, POST, and may others. As follows we present an example of such a JSON response. .. code-block:: json { "entries": [ { "buckets": [ { "bucket": "", "categories": [ { "bytes_received": 0, "bytes_sent": 40, "category": "list_buckets", "ops": 2, "successful_ops": 2 } ], "epoch": 1572969600, "owner": "user", "time": "2019-11-21 00:00:00.000000Z" }, { "bucket": "-", "categories": [ { "bytes_received": 0, "bytes_sent": 0, "category": "get_obj", "ops": 1, "successful_ops": 0 } ], "epoch": 1572969600, "owner": "someOtherUser", "time": "2019-11-21 00:00:00.000000Z" } ] } ] "summary": [ { "categories": [ { "bytes_received": 0, "bytes_sent": 0, "category": "create_bucket", "ops": 2, "successful_ops": 2 }, { "bytes_received": 0, "bytes_sent": 2120428, "category": "get_obj", "ops": 46, "successful_ops": 46 }, { "bytes_received": 0, "bytes_sent": 21484, "category": "list_bucket", "ops": 8, "successful_ops": 8 }, { "bytes_received": 6889056, "bytes_sent": 0, "category": "put_obj", "ops": 46, "successful_ops": 46 } ], "total": { "bytes_received": 6889056, "bytes_sent": 2141912, "ops": 102, "successful_ops": 102 }, "user": "user" }, { "categories": [ { "bytes_received": 0, "bytes_sent": 0, "category": "create_bucket", "ops": 1, "successful_ops": 1 }, { "bytes_received": 0, "bytes_sent": 0, "category": "delete_obj", "ops": 23, "successful_ops": 23 }, { "bytes_received": 0, "bytes_sent": 5371, "category": "list_bucket", "ops": 2, "successful_ops": 2 }, { "bytes_received": 3444350, "bytes_sent": 0, "category": "put_obj", "ops": 23, "successful_ops": 23 } ], "total": { "bytes_received": 3444350, "bytes_sent": 5371, "ops": 49, "successful_ops": 49 }, "user": "someOtherUser" } ] } In that context, and having in mind that we have APIs with similar data structures, we developed an extension for the dynamic pollster that enables multi-metric processing for a single pollster. It works as follows. The pollster name will contain a placeholder for the variable that identifies the "submetric". E.g. `dynamic.radosgw.api.request.{category}`. The placeholder `{category}` indicates the object's attribute that is in the list of objects that we use to load the sub metric name. Then, we must use a special notation in the `value_attribute` configuration to indicate that we are dealing with a list of objects. This is achieved via `[]` (brackets); for instance, in the `dynamic.radosgw.api.request.{category}`, we can use `[categories].ops` as the `value_attribute`. This indicates that the value we retrieve is a list of objects, and when the dynamic pollster processes it, we want it (the pollster) to load the `ops` value for the sub metrics being generated. Examples on how to create multi-metric pollster to handle data from RadosGW API are presented as follows: .. code-block:: yaml --- - name: "dynamic.radosgw.api.request.{category}" sample_type: "gauge" unit: "request" value_attribute: "[categories].ops" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ", ," user_id_attribute: "user | value.split('$')[0]" project_id_attribute: "user | value.split('$') | value[0]" resource_id_attribute: "user | value.split('$') | value[0]" response_entries_key: "summary" - name: "dynamic.radosgw.api.request.successful_ops.{category}" sample_type: "gauge" unit: "request" value_attribute: "[categories].successful_ops" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ", ," user_id_attribute: "user | value.split('$')[0]" project_id_attribute: "user | value.split('$') | value[0]" resource_id_attribute: "user | value.split('$') | value[0]" response_entries_key: "summary" - name: "dynamic.radosgw.api.bytes_sent.{category}" sample_type: "gauge" unit: "request" value_attribute: "[categories].bytes_sent" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ", ," user_id_attribute: "user | value.split('$')[0]" project_id_attribute: "user | value.split('$') | value[0]" resource_id_attribute: "user | value.split('$') | value[0]" response_entries_key: "summary" - name: "dynamic.radosgw.api.bytes_received.{category}" sample_type: "gauge" unit: "request" value_attribute: "[categories].bytes_received" url_path: "http://rgw.service.stage.i.ewcs.ch/admin/usage" module: "awsauth" authentication_object: "S3Auth" authentication_parameters: ", ," user_id_attribute: "user | value.split('$')[0]" project_id_attribute: "user | value.split('$') | value[0]" resource_id_attribute: "user | value.split('$') | value[0]" response_entries_key: "summary" Handling linked API responses ----------------------------- If the consumed API returns a linked response which contains a link to the next response set (page), the Dynamic pollsters can be configured to follow these links and join all linked responses into a single one. To enable this behavior the operator will need to configure the parameter `next_sample_url_attribute` that must contain a mapper to the response attribute that contains the link to the next response page. This parameter also supports operations like the others `*_attribute` dynamic pollster's parameters. Examples on how to create a pollster to handle linked API responses are presented as follows: - Example of a simple linked response: - API response: .. code-block:: json { "server_link": "http://test.com/v1/test-volumes/marker=c3", "servers": [ { "volume": [ { "name": "a", "tmp": "ra" } ], "id": 1, "name": "a1" }, { "volume": [ { "name": "b", "tmp": "rb" } ], "id": 2, "name": "b2" }, { "volume": [ { "name": "c", "tmp": "rc" } ], "id": 3, "name": "c3" } ] } - Pollster configuration: .. code-block:: yaml --- - name: "dynamic.linked.response" sample_type: "gauge" unit: "request" value_attribute: "[volume].tmp" url_path: "v1/test-volumes" response_entries_key: "servers" next_sample_url_attribute: "server_link" - Example of a complex linked response: - API response: .. code-block:: json { "server_link": [ { "href": "http://test.com/v1/test-volumes/marker=c3", "rel": "next" }, { "href": "http://test.com/v1/test-volumes/marker=b1", "rel": "prev" } ], "servers": [ { "volume": [ { "name": "a", "tmp": "ra" } ], "id": 1, "name": "a1" }, { "volume": [ { "name": "b", "tmp": "rb" } ], "id": 2, "name": "b2" }, { "volume": [ { "name": "c", "tmp": "rc" } ], "id": 3, "name": "c3" } ] } - Pollster configuration: .. code-block:: yaml --- - name: "dynamic.linked.response" sample_type: "gauge" unit: "request" value_attribute: "[volume].tmp" url_path: "v1/test-volumes" response_entries_key: "servers" next_sample_url_attribute: "server_link | filter(lambda v: v.get('rel') == 'next', value) | list(value) | value[0] | value.get('href')" OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's data ------------------------------------------------------------------------------- Sometimes we want/need to add/gather extra metadata for the samples being handled by Ceilometer Dynamic pollsters, such as the project name, domain id, domain name, and other metadata that are not always accessible via the OpenStack component where the sample is gathered. For instance, when gathering the status of virtual machines (VMs) from Nova, we only have the `tenant_id`, which must be used as the `project_id`. However, for billing and later invoicing one might need/want the project name, domain id, and other metadata that are available in Keystone (and maybe some others that are scattered over other components). To achieve that, one can use the OpenStack metadata enrichment option. As follows we present an example that shows a dynamic pollster configuration to gather virtual machine (VM) status, and to enrich the data pushed to the storage backend (e.g. Gnocchi) with project name, domain ID, and domain name. .. code-block:: yaml --- - name: "dynamic_pollster.instance.status" next_sample_url_attribute: "server_links | filter(lambda v: v.get('rel') == 'next', value) | list(value) | value[0] | value.get('href') | value.replace('http:', 'https:')" sample_type: "gauge" unit: "server" value_attribute: "status" endpoint_type: "compute" url_path: "/v2.1/servers/detail?all_tenants=true" headers: "Openstack-API-Version": "compute 2.65" project_id_attribute: "tenant_id" metadata_fields: - "status" - "name" - "flavor.vcpus" - "flavor.ram" - "flavor.disk" - "flavor.ephemeral" - "flavor.swap" - "flavor.original_name" - "image | value or { 'id': '' } | value['id']" - "OS-EXT-AZ:availability_zone" - "OS-EXT-SRV-ATTR:host" - "user_id" - "tags | ','.join(value)" - "locked" value_mapping: ACTIVE: "1" default_value: 0 metadata_mapping: "OS-EXT-AZ:availability_zone": "dynamic_availability_zone" "OS-EXT-SRV-ATTR:host": "dynamic_host" "flavor.original_name": "dynamic_flavor_name" "flavor.vcpus": "dynamic_flavor_vcpus" "flavor.ram": "dynamic_flavor_ram" "flavor.disk": "dynamic_flavor_disk" "flavor.ephemeral": "dynamic_flavor_ephemeral" "flavor.swap": "dynamic_flavor_swap" "image | value or { 'id': '' } | value['id']": "dynamic_image_ref" "name": "dynamic_display_name" "locked": "dynamic_locked" "tags | ','.join(value)": "dynamic_tags" extra_metadata_fields_cache_seconds: 3600 extra_metadata_fields_skip: - value: '1' metadata: dynamic_flavor_vcpus: 4 - value: '1' metadata: dynamic_flavor_vcpus: 2 extra_metadata_fields: - name: "project_name" endpoint_type: "identity" url_path: "'/v3/projects/' + str(sample['project_id'])" headers: "Openstack-API-Version": "identity latest" value: "name" extra_metadata_fields_cache_seconds: 1800 # overriding the default cache policy metadata_fields: - id - name: "domain_id" endpoint_type: "identity" url_path: "'/v3/projects/' + str(sample['project_id'])" headers: "Openstack-API-Version": "identity latest" value: "domain_id" metadata_fields: - id - name: "domain_name" endpoint_type: "identity" url_path: "'/v3/domains/' + str(extra_metadata_captured['domain_id'])" headers: "Openstack-API-Version": "identity latest" value: "name" metadata_fields: - id - name: "operating-system" host_command: "'get-vm --vm-name ' + str(extra_metadata_by_name['project_name']['metadata']['id'])" value: "os" The above example can be used to gather and persist in the backend the status of VMs. It will persist `1` in the backend as a measure for every collecting period if the VM's status is `ACTIVE`, and `0` otherwise. This is quite useful to create hashmap rating rules for running VMs in CloudKitty. Then, to enrich the resource in the storage backend, we are adding extra metadata that are collected in Keystone and in the local host via the `extra_metadata_fields` options. If you have multiples `extra_metadata_fields` defining the same `metadata_field`, the last not `None` metadata value will be used. To operate values in the `extra_metadata_fields`, you can access 3 local variables: * ``sample``: it is a dictionary which holds the current data of the root sample. The root sample is the final sample that will be persisted in the configured storage backend. * ``extra_metadata_captured``: it is a dictionary which holds the current data of all `extra_metadata_fields` processed before this one. If you have multiples `extra_metadata_fields` defining the same `metadata_field`, the last not `None` metadata value will be used. * ``extra_metadata_by_name``: it is a dictionary which holds the data of all `extra_metadata_fields` processed before this one. No data is overwritten in this variable. To access an specific `extra_metadata_field` using this variable, you can do `extra_metadata_by_name['']['value']` to get its value, or `extra_metadata_by_name['']['metadata']['']` to get its metadata. The metadata enrichment feature has the following options: * ``extra_metadata_fields_cache_seconds``: optional parameter. Defines the extra metadata request's response cache. Some requests, such as the ones executed against Keystone to retrieve extra metadata are rather static. Therefore, one does not need to constantly re-execute the request. That is the reason why we cache the response of such requests. By default the cache time to live (TTL) for responses is `3600` seconds. However, this value can be increased of decreased. * ``extra_metadata_fields``: optional parameter. This option is a list of objects or a single one, where each one of its elements is an dynamic pollster configuration set. Each one of the extra metadata definition can have the same options defined in the dynamic pollsters, including the `extra_metadata_fields` option, so this option is a multi-level option. When defined, the result of the collected data will be merged in the final sample resource metadata. If some of the required dynamic pollster configuration is not set in the `extra_metadata_fields`, will be used the parent pollster configuration, except the `name`. * ``extra_metadata_fields_skip``: optional parameter. This option is a list of objects or a single one, where each one of its elements is a set of key/value pairs. When defined, if any set of key/value pairs is a subset of the collected sample, then the extra_metadata_fields gathering of this sample will be skipped. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2423568 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-events.rst0000664000175000017500000001456415250267452022543 0ustar00zuulzuul====== Events ====== In addition to meters, the Telemetry service collects events triggered within an OpenStack environment. This section provides a brief summary of the events format in the Telemetry service. While a sample represents a single, numeric datapoint within a time-series, an event is a broader concept that represents the state of a resource at a point in time. The state may be described using various data types including non-numeric data such as an instance's flavor. In general, events represent any action made in the OpenStack system. Event configuration ~~~~~~~~~~~~~~~~~~~ By default, ceilometer builds event data from the messages it receives from other OpenStack services. .. note:: In releases older than Ocata, it is advisable to set ``disable_non_metric_meters`` to ``True`` when enabling events in the Telemetry service. The Telemetry service historically represented events as metering data, which may create duplication of data if both events and non-metric meters are enabled. Event structure ~~~~~~~~~~~~~~~ Events captured by the Telemetry service are represented by five key attributes: event\_type A dotted string defining what event occurred such as ``"compute.instance.resize.start"``. message\_id A UUID for the event. generated A timestamp of when the event occurred in the system. traits A flat mapping of key-value pairs which describe the event. The event's traits contain most of the details of the event. Traits are typed, and can be strings, integers, floats, or datetimes. raw Mainly for auditing purpose, the full event message can be stored (unindexed) for future evaluation. Event indexing ~~~~~~~~~~~~~~ The general philosophy of notifications in OpenStack is to emit any and all data someone might need, and let the consumer filter out what they are not interested in. In order to make processing simpler and more efficient, the notifications are stored and processed within Ceilometer as events. The notification payload, which can be an arbitrarily complex JSON data structure, is converted to a flat set of key-value pairs. This conversion is specified by a config file. .. note:: The event format is meant for efficient processing and querying. Storage of complete notifications for auditing purposes can be enabled by configuring ``store_raw`` option. Event conversion ---------------- The conversion from notifications to events is driven by a configuration file defined by the ``definitions_cfg_file`` in the ``ceilometer.conf`` configuration file. This includes descriptions of how to map fields in the notification body to Traits, and optional plug-ins for doing any programmatic translations (splitting a string, forcing case). The mapping of notifications to events is defined per event\_type, which can be wildcarded. Traits are added to events if the corresponding fields in the notification exist and are non-null. .. note:: The default definition file included with the Telemetry service contains a list of known notifications and useful traits. The mappings provided can be modified to include more or less data according to user requirements. If the definitions file is not present, a warning will be logged, but an empty set of definitions will be assumed. By default, any notifications that do not have a corresponding event definition in the definitions file will be converted to events with a set of minimal traits. This can be changed by setting the option ``drop_unmatched_notifications`` in the ``ceilometer.conf`` file. If this is set to ``True``, any unmapped notifications will be dropped. The basic set of traits (all are TEXT type) that will be added to all events if the notification has the relevant data are: service (notification's publisher), tenant\_id, and request\_id. These do not have to be specified in the event definition, they are automatically added, but their definitions can be overridden for a given event\_type. Event definitions format ------------------------ The event definitions file is in YAML format. It consists of a list of event definitions, which are mappings. Order is significant, the list of definitions is scanned in reverse order to find a definition which matches the notification's event\_type. That definition will be used to generate the event. The reverse ordering is done because it is common to want to have a more general wildcarded definition (such as ``compute.instance.*``) with a set of traits common to all of those events, with a few more specific event definitions afterwards that have all of the above traits, plus a few more. Each event definition is a mapping with two keys: event\_type This is a list (or a string, which will be taken as a 1 element list) of event\_types this definition will handle. These can be wildcarded with unix shell glob syntax. An exclusion listing (starting with a ``!``) will exclude any types listed from matching. If only exclusions are listed, the definition will match anything not matching the exclusions. traits This is a mapping, the keys are the trait names, and the values are trait definitions. Each trait definition is a mapping with the following keys: fields A path specification for the field(s) in the notification you wish to extract for this trait. Specifications can be written to match multiple possible fields. By default the value will be the first such field. The paths can be specified with a dot syntax (``payload.host``). Square bracket syntax (``payload[host]``) is also supported. In either case, if the key for the field you are looking for contains special characters, like ``.``, it will need to be quoted (with double or single quotes): ``payload.image_meta.`org.openstack__1__architecture```. The syntax used for the field specification is a variant of `JSONPath `__ type (Optional) The data type for this trait. Valid options are: ``text``, ``int``, ``float``, and ``datetime``. Defaults to ``text`` if not specified. plugin (Optional) Used to execute simple programmatic conversions on the value in a notification field. Event delivery to external sinks -------------------------------- You can configure the Telemetry service to deliver the events into external sinks. These sinks are configurable in the ``/etc/ceilometer/event_pipeline.yaml`` file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2433567 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-measurements.rst0000664000175000017500000012402515250267452023741 0ustar00zuulzuul.. _telemetry-measurements: ============ Measurements ============ The Telemetry service collects meters within an OpenStack deployment. This section provides a brief summary about meters format and origin and also contains the list of available meters. Telemetry collects meters by polling the infrastructure elements and also by consuming the notifications emitted by other OpenStack services. For more information about the polling mechanism and notifications see :ref:`telemetry-data-collection`. There are several meters which are collected by polling and by consuming. The origin for each meter is listed in the tables below. .. note:: You may need to configure Telemetry or other OpenStack services in order to be able to collect all the samples you need. For further information about configuration requirements see the `Telemetry chapter `__ in the Installation Tutorials and Guides. Telemetry uses the following meter types: +--------------+--------------------------------------------------------------+ | Type | Description | +==============+==============================================================+ | Cumulative | Increasing over time (instance hours) | +--------------+--------------------------------------------------------------+ | Delta | Changing over time (bandwidth) | +--------------+--------------------------------------------------------------+ | Gauge | Discrete items (floating IPs, image uploads) and fluctuating | | | values (disk I/O) | +--------------+--------------------------------------------------------------+ | Telemetry provides the possibility to store metadata for samples. This metadata can be extended for OpenStack Compute and OpenStack Object Storage. In order to add additional metadata information to OpenStack Compute you have two options to choose from. The first one is to specify them when you boot up a new instance. The additional information will be stored with the sample in the form of ``resource_metadata.user_metadata.*``. The new field should be defined by using the prefix ``metering.``. The modified boot command look like the following: .. code-block:: console $ openstack server create --property metering.custom_metadata=a_value my_vm The other option is to set the ``reserved_metadata_keys`` to the list of metadata keys that you would like to be included in ``resource_metadata`` of the instance related samples that are collected for OpenStack Compute. This option is included in the ``DEFAULT`` section of the ``ceilometer.conf`` configuration file. You might also specify headers whose values will be stored along with the sample data of OpenStack Object Storage. The additional information is also stored under ``resource_metadata``. The format of the new field is ``resource_metadata.http_header_$name``, where ``$name`` is the name of the header with ``-`` replaced by ``_``. For specifying the new header, you need to set ``metadata_headers`` option under the ``[filter:ceilometer]`` section in ``proxy-server.conf`` under the ``swift`` folder. You can use this additional data for instance to distinguish external and internal users. Measurements are grouped by services which are polled by Telemetry or emit notifications that this service consumes. .. _telemetry-compute-meters: OpenStack Compute ~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Compute. +-----------+-------+------+----------+----------+---------+------------------+ | Name | Type | Unit | Resource | Origin | Support | Note | +===========+=======+======+==========+==========+=========+==================+ | **Meters added in the Mitaka release or earlier** | +-----------+-------+------+----------+----------+---------+------------------+ | memory | Gauge | MiB | instance | Notific\ | Libvirt | Volume of RAM | | | | | ID | ation, \ | | allocated to the | | | | | | Pollster | | instance | +-----------+-------+------+----------+----------+---------+------------------+ | memory.\ | Gauge | MiB | instance | Pollster | Libvirt | Volume of RAM | | usage | | | ID | | | used by the inst\| | | | | | | | ance from the | | | | | | | | amount of its | | | | | | | | allocated memory | +-----------+-------+------+----------+----------+---------+------------------+ | memory.r\ | Gauge | MiB | instance | Pollster | Libvirt | Volume of RAM u\ | | esident | | | ID | | | sed by the inst\ | | | | | | | | ance on the phy\ | | | | | | | | sical machine | +-----------+-------+------+----------+----------+---------+------------------+ | cpu | Cumu\ | ns | instance | Pollster | Libvirt | CPU time used | | | lative| | ID | | | | +-----------+-------+------+----------+----------+---------+------------------+ | vcpus | Gauge | vcpu | instance | Notific\ | Libvirt | Number of virtual| | | | | ID | ation, \ | | CPUs allocated to| | | | | | Pollster | | the instance | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Cumu\ | req\ | disk ID | Pollster | Libvirt | Number of read | | ice.read\ | lative| uest | | | | requests | | .requests | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Cumu\ | req\ | disk ID | Pollster | Libvirt | Number of write | | ice.write\| lative| uest | | | | requests | | .requests | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Cumu\ | B | disk ID | Pollster | Libvirt | Volume of reads | | ice.read\ | lative| | | | | | | .bytes | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Cumu\ | B | disk ID | Pollster | Libvirt | Volume of writes | | ice.write\| lative| | | | | | | .bytes | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.root\| Gauge | GiB | instance | Notific\ | Libvirt | Size of root disk| | .size | | | ID | ation, \ | | | | | | | | Pollster | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.ephe\| Gauge | GiB | instance | Notific\ | Libvirt | Size of ephemeral| | meral.size| | | ID | ation, \ | | disk | | | | | | Pollster | | | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Gauge | B | disk ID | Pollster | Libvirt | The amount of d\ | | ice.capa\ | | | | | | isk per device | | city | | | | | | that the instan\ | | | | | | | | ce can see | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Gauge | B | disk ID | Pollster | Libvirt | The amount of d\ | | ice.allo\ | | | | | | isk per device | | cation | | | | | | occupied by the | | | | | | | | instance on th\ | | | | | | | | e host machine | +-----------+-------+------+----------+----------+---------+------------------+ | disk.dev\ | Gauge | B | disk ID | Pollster | Libvirt | The physical si\ | | ice.usag\ | | | | | | ze in bytes of | | e | | | | | | the image conta\ | | | | | | | | iner on the hos\ | | | | | | | | t per device | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumu\ | B | interface| Pollster | Libvirt | Number of | | incoming.\| lative| | ID | | | incoming bytes | | bytes | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumu\ | B | interface| Pollster | Libvirt | Number of | | outgoing\ | lative| | ID | | | outgoing bytes | | .bytes | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumu\ | pac\ | interface| Pollster | Libvirt | Number of | | incoming\ | lative| ket | ID | | | incoming packets | | .packets | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumu\ | pac\ | interface| Pollster | Libvirt | Number of | | outgoing\ | lative| ket | ID | | | outgoing packets | | .packets | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Newton release** | +-----------+-------+------+----------+----------+---------+------------------+ | perf.cpu\ | Gauge | cyc\ | instance | Pollster | Libvirt | the number of c\ | | .cycles | | le | ID | | | pu cycles one i\ | | | | | | | | nstruction needs | +-----------+-------+------+----------+----------+---------+------------------+ | perf.ins\ | Gauge | inst\| instance | Pollster | Libvirt | the count of in\ | | tructions | | ruct\| ID | | | structions | | | | ion | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | perf.cac\ | Gauge | cou\ | instance | Pollster | Libvirt | the count of ca\ | | he.refer\ | | nt | ID | | | che hits | | ences | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | perf.cac\ | Gauge | cou\ | instance | Pollster | Libvirt | the count of ca\ | | he.misses | | nt | ID | | | che misses | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Ocata release** | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumul\| pack\| interface| Pollster | Libvirt | Number of | | incoming\ | ative | et | ID | | | incoming dropped | | .packets\ | | | | | | packets | | .drop | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumul\| pack\| interface| Pollster | Libvirt | Number of | | outgoing\ | ative | et | ID | | | outgoing dropped | | .packets\ | | | | | | packets | | .drop | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumul\| pack\| interface| Pollster | Libvirt | Number of | | incoming\ | ative | et | ID | | | incoming error | | .packets\ | | | | | | packets | | .error | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | network.\ | Cumul\| pack\| interface| Pollster | Libvirt | Number of | | outgoing\ | ative | et | ID | | | outgoing error | | .packets\ | | | | | | packets | | .error | | | | | | | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Pike release** | +-----------+-------+------+----------+----------+---------+------------------+ | memory.\ | Cumul\| | | | | | | swap.in | ative | MiB | instance | Pollster | Libvirt | Memory swap in | | | | | ID | | | | +-----------+-------+------+----------+----------+---------+------------------+ | memory.\ | Cumul\| | | | | | | swap.out | ative | MiB | instance | Pollster | Libvirt | Memory swap out | | | | | ID | | | | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Queens release** | +-----------+-------+------+----------+----------+---------+------------------+ | disk.devi\| Cumul\| | | | | Total time read | | ce.read.l\| ative | ns | Disk ID | Pollster | Libvirt | operations have | | atency | | | | | | taken | +-----------+-------+------+----------+----------+---------+------------------+ | disk.devi\| Cumul\| | | | | Total time write | | ce.write.\| ative | ns | Disk ID | Pollster | Libvirt | operations have | | latency | | | | | | taken | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Epoxy release** | +-----------+-------+------+----------+----------+---------+------------------+ | power.sta\| Gauge | state| instance | Pollster | Libvirt | virDomainState | | te | | | ID | | | of the VM | +-----------+-------+------+----------+----------+---------+------------------+ | **Meters added in the Flamingo release** | +-----------+-------+------+----------+----------+---------+------------------+ | memory.\ | Gauge | MiB | instance | Pollster | Libvirt | Volume of RAM | | available | | | ID | | | available to the | | | | | | | | instance as seen | | | | | | | | from within the | | | | | | | | instance | +-----------+-------+------+----------+----------+---------+------------------+ .. note:: To enable the libvirt ``memory.usage`` support, you need to install libvirt version 1.1.1+, QEMU version 1.5+, and you also need to prepare suitable balloon driver in the image. It is applicable particularly for Windows guests, most modern Linux distributions already have it built in. Telemetry is not able to fetch the ``memory.usage`` samples without the image balloon driver. .. note:: To enable libvirt ``disk.*`` support when running on RBD-backed shared storage, you need to install libvirt version 1.2.16+. OpenStack Compute is capable of collecting ``CPU`` related meters from the compute host machines. In order to use that you need to set the ``compute_monitors`` option to ``cpu.virt_driver`` in the ``nova.conf`` configuration file. For further information see the Compute configuration section in the `Compute chapter `__ of the OpenStack Configuration Reference. The following host machine related meters are collected for OpenStack Compute: +---------------------+-------+------+----------+-------------+---------------+ | Name | Type | Unit | Resource | Origin | Note | +=====================+=======+======+==========+=============+===============+ | **Meters added in the Mitaka release or earlier** | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | MHz | host ID | Notification| CPU frequency | | frequency | | | | | | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Cumu\ | ns | host ID | Notification| CPU kernel | | kernel.time | lative| | | | time | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Cumu\ | ns | host ID | Notification| CPU idle time | | idle.time | lative| | | | | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Cumu\ | ns | host ID | Notification| CPU user mode | | user.time | lative| | | | time | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Cumu\ | ns | host ID | Notification| CPU I/O wait | | iowait.time | lative| | | | time | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | % | host ID | Notification| CPU kernel | | kernel.percent | | | | | percentage | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | % | host ID | Notification| CPU idle | | idle.percent | | | | | percentage | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | % | host ID | Notification| CPU user mode | | user.percent | | | | | percentage | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | % | host ID | Notification| CPU I/O wait | | iowait.percent | | | | | percentage | +---------------------+-------+------+----------+-------------+---------------+ | compute.node.cpu.\ | Gauge | % | host ID | Notification| CPU | | percent | | | | | utilization | +---------------------+-------+------+----------+-------------+---------------+ .. _telemetry-bare-metal-service: IPMI meters ~~~~~~~~~~~ Telemetry captures notifications that are emitted by the Bare metal service. The source of the notifications are IPMI sensors that collect data from the host machine. Alternatively, IPMI meters can be generated by deploying the ceilometer-agent-ipmi on each IPMI-capable node. For further information about the IPMI agent see :ref:`telemetry-ipmi-agent`. .. warning:: To avoid duplication of metering data and unnecessary load on the IPMI interface, do not deploy the IPMI agent on nodes that are managed by the Bare metal service and keep the ``conductor.send_sensor_data`` option set to ``False`` in the ``ironic.conf`` configuration file. The following IPMI sensor meters are recorded: +------------------+-------+------+----------+-------------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +==================+=======+======+==========+=============+==================+ | **Meters added in the Mitaka release or earlier** | +------------------+-------+------+----------+-------------+------------------+ | hardware.ipmi.fan| Gauge | RPM | fan | Notificatio\| Fan rounds per | | | | | sensor | n, Pollster | minute (RPM) | +------------------+-------+------+----------+-------------+------------------+ | hardware.ipmi\ | Gauge | C | temper\ | Notificatio\| Temperature read\| | .temperature | | | ature | n, Pollster | ing from sensor | | | | | sensor | | | +------------------+-------+------+----------+-------------+------------------+ | hardware.ipmi\ | Gauge | A | current | Notificatio\| Current reading | | .current | | | sensor | n, Pollster | from sensor | +------------------+-------+------+----------+-------------+------------------+ | hardware.ipmi\ | Gauge | V | voltage | Notificatio\| Voltage reading | | .voltage | | | sensor | n, Pollster | from sensor | +------------------+-------+------+----------+-------------+------------------+ .. note:: The sensor data is not available in the Bare metal service by default. To enable the meters and configure this module to emit notifications about the measured values see the `Installation Guide `__ for the Bare metal service. OpenStack Image service ~~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Image service: +--------------------+--------+------+----------+----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +====================+========+======+==========+==========+==================+ | **Meters added in the Mitaka release or earlier** | +--------------------+--------+------+----------+----------+------------------+ | image.size | Gauge | B | image ID | Notifica\| Size of the upl\ | | | | | | tion, Po\| oaded image | | | | | | llster | | +--------------------+--------+------+----------+----------+------------------+ | image.download | Delta | B | image ID | Notifica\| Image is downlo\ | | | | | | tion | aded | +--------------------+--------+------+----------+----------+------------------+ | image.serve | Delta | B | image ID | Notifica\| Image is served | | | | | | tion | out | +--------------------+--------+------+----------+----------+------------------+ OpenStack Block Storage ~~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Block Storage: +--------------------+-------+--------+----------+----------+-----------------+ | Name | Type | Unit | Resource | Origin | Note | +====================+=======+========+==========+==========+=================+ | **Meters added in the Mitaka release or earlier** | +--------------------+-------+--------+----------+----------+-----------------+ | volume.size | Gauge | GiB | volume ID| Notifica\| Size of the vol\| | | | | | tion | ume | +--------------------+-------+--------+----------+----------+-----------------+ | snapshot.size | Gauge | GiB | snapshot | Notifica\| Size of the sna\| | | | | ID | tion | pshot | +--------------------+-------+--------+----------+----------+-----------------+ | **Meters added in the Queens release** | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.ca\| Gauge | GiB | hostname | Notifica\| Total volume | | pacity.total | | | | tion | capacity on host| +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.ca\| Gauge | GiB | hostname | Notifica\| Free volume | | pacity.free | | | | tion | capacity on host| +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.ca\| Gauge | GiB | hostname | Notifica\| Assigned volume | | pacity.allocated | | | | tion | capacity on host| | | | | | | by Cinder | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.ca\| Gauge | GiB | hostname | Notifica\| Assigned volume | | pacity.provisioned | | | | tion | capacity on host| +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.ca\| Gauge | GiB | hostname | Notifica\| Virtual free | | pacity.virtual_free| | | | tion | volume capacity | | | | | | | on host | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.po\| Gauge | GiB | hostname\| Notifica\| Total volume | | ol.capacity.total | | | #pool | tion, Po\| capacity in pool| | | | | | llster | | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.po\| Gauge | GiB | hostname\| Notifica\| Free volume | | ol.capacity.free | | | #pool | tion, Po\| capacity in pool| | | | | | llster | | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.po\| Gauge | GiB | hostname\| Notifica\| Assigned volume | | ol.capacity.alloca\| | | #pool | tion, Po\| capacity in pool| | ted | | | | llster | by Cinder | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.po\| Gauge | GiB | hostname\| Notifica\| Assigned volume | | ol.capacity.provis\| | | #pool | tion, Po\| capacity in pool| | ioned | | | | llster | | +--------------------+-------+--------+----------+----------+-----------------+ | volume.provider.po\| Gauge | GiB | hostname\| Notifica\| Virtual free | | ol.capacity.virtua\| | | #pool | tion, Po\| volume capacity | | l_free | | | | llster | in pool | +--------------------+-------+--------+----------+----------+-----------------+ | **Meters added in the Hibiscus release** | +--------------------+-------+--------+----------+----------+-----------------+ | volume.service.he\ | Gauge | health | binary@ | Pollster | Health of cinder| | alth | | | host | | service (1=up, | | | | | | | 0=down) | +--------------------+-------+--------+----------+----------+-----------------+ OpenStack File Share ~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack File Share: +--------------------+-------+--------+----------+----------+-----------------+ | Name | Type | Unit | Resource | Origin | Note | +====================+=======+========+==========+==========+=================+ | **Meters added in the Pike release** | +--------------------+-------+--------+----------+----------+-----------------+ | manila.share.size | Gauge | GiB | share ID | Notifica\| Size of the fil\| | | | | | tion, Po\| e share | | | | | | llster | | +--------------------+-------+--------+----------+----------+-----------------+ | **Meters added in the Gazpacho release** | +--------------------+-------+--------+----------+----------+-----------------+ | manila.share.\ | Gauge | status | share ID | Pollster | Numeric status | | status | | | | | of the share | +--------------------+-------+--------+----------+----------+-----------------+ .. _telemetry-object-storage-meter: OpenStack Object Storage ~~~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Object Storage: +--------------------+-------+-------+------------+---------+-----------------+ | Name | Type | Unit | Resource | Origin | Note | +====================+=======+=======+============+=========+=================+ | **Meters added in the Mitaka release or earlier** | +--------------------+-------+-------+------------+---------+-----------------+ | storage.objects | Gauge | object| storage ID | Pollster| Number of objec\| | | | | | | ts | +--------------------+-------+-------+------------+---------+-----------------+ | storage.objects.si\| Gauge | B | storage ID | Pollster| Total size of s\| | ze | | | | | tored objects | +--------------------+-------+-------+------------+---------+-----------------+ | storage.objects.co\| Gauge | conta\| storage ID | Pollster| Number of conta\| | ntainers | | iner | | | iners | +--------------------+-------+-------+------------+---------+-----------------+ | storage.objects.in\| Delta | B | storage ID | Notific\| Number of incom\| | coming.bytes | | | | ation | ing bytes | +--------------------+-------+-------+------------+---------+-----------------+ | storage.objects.ou\| Delta | B | storage ID | Notific\| Number of outgo\| | tgoing.bytes | | | | ation | ing bytes | +--------------------+-------+-------+------------+---------+-----------------+ | storage.containers\| Gauge | object| storage ID\| Pollster| Number of objec\| | .objects | | | /container | | ts in container | +--------------------+-------+-------+------------+---------+-----------------+ | storage.containers\| Gauge | B | storage ID\| Pollster| Total size of s\| | .objects.size | | | /container | | tored objects i\| | | | | | | n container | +--------------------+-------+-------+------------+---------+-----------------+ Ceph Object Storage ~~~~~~~~~~~~~~~~~~~ In order to gather meters from Ceph, you have to install and configure the Ceph Object Gateway (radosgw) as it is described in the `Installation Manual `__. You also have to enable `usage logging `__ in order to get the related meters from Ceph. You will need an ``admin`` user with ``users``, ``buckets``, ``metadata`` and ``usage`` ``caps`` configured. In order to access Ceph from Telemetry, you need to specify a ``service group`` for ``radosgw`` in the ``ceilometer.conf`` configuration file along with ``access_key`` and ``secret_key`` of the ``admin`` user mentioned above. The following meters are collected for Ceph Object Storage: +------------------+------+--------+------------+----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +==================+======+========+============+==========+==================+ | **Meters added in the Mitaka release or earlier** | +------------------+------+--------+------------+----------+------------------+ | radosgw.objects | Gauge| object | storage ID | Pollster | Number of objects| +------------------+------+--------+------------+----------+------------------+ | radosgw.objects.\| Gauge| B | storage ID | Pollster | Total size of s\ | | size | | | | | tored objects | +------------------+------+--------+------------+----------+------------------+ | radosgw.objects.\| Gauge| contai\| storage ID | Pollster | Number of conta\ | | containers | | ner | | | iners | +------------------+------+--------+------------+----------+------------------+ | radosgw.api.requ\| Gauge| request| storage ID | Pollster | Number of API r\ | | est | | | | | equests against | | | | | | | Ceph Object Ga\ | | | | | | | teway (radosgw) | +------------------+------+--------+------------+----------+------------------+ | radosgw.containe\| Gauge| object | storage ID\| Pollster | Number of objec\ | | rs.objects | | | /container | | ts in container | +------------------+------+--------+------------+----------+------------------+ | radosgw.containe\| Gauge| B | storage ID\| Pollster | Total size of s\ | | rs.objects.size | | | /container | | tored objects in | | | | | | | container | +------------------+------+--------+------------+----------+------------------+ .. note:: The ``usage`` related information may not be updated right after an upload or download, because the Ceph Object Gateway needs time to update the usage properties. For instance, the default configuration needs approximately 30 minutes to generate the usage logs. OpenStack Identity ~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Identity: +-------------------+------+--------+-----------+-----------+-----------------+ | Name | Type | Unit | Resource | Origin | Note | +===================+======+========+===========+===========+=================+ | **Meters added in the Mitaka release or earlier** | +-------------------+------+--------+-----------+-----------+-----------------+ | identity.authent\ | Delta| user | user ID | Notifica\ | User successful\| | icate.success | | | | tion | ly authenticated| +-------------------+------+--------+-----------+-----------+-----------------+ | identity.authent\ | Delta| user | user ID | Notifica\ | User pending au\| | icate.pending | | | | tion | thentication | +-------------------+------+--------+-----------+-----------+-----------------+ | identity.authent\ | Delta| user | user ID | Notifica\ | User failed to | | icate.failure | | | | tion | authenticate | +-------------------+------+--------+-----------+-----------+-----------------+ OpenStack Networking ~~~~~~~~~~~~~~~~~~~~ The following meters are collected for OpenStack Networking: +-----------------+-------+--------+-----------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +=================+=======+========+===========+===========+==================+ | **Meters added in the Mitaka release or earlier** | +-----------------+-------+--------+-----------+-----------+------------------+ | bandwidth | Delta | B | label ID | Notifica\ | Bytes through t\ | | | | | | tion | his l3 metering | | | | | | | label | +-----------------+-------+--------+-----------+-----------+------------------+ VPN-as-a-Service (VPNaaS) ~~~~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for VPNaaS: +---------------+-------+---------+------------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +===============+=======+=========+============+===========+==================+ | **Meters added in the Mitaka release or earlier** | +---------------+-------+---------+------------+-----------+------------------+ | network.serv\ | Gauge | vpnser\ | vpn ID | Pollster | Existence of a | | ices.vpn | | vice | | | VPN | +---------------+-------+---------+------------+-----------+------------------+ | network.serv\ | Gauge | ipsec\_\| connection | Pollster | Existence of an | | ices.vpn.con\ | | site\_c\| ID | | IPSec connection | | nections | | onnect\ | | | | | | | ion | | | | +---------------+-------+---------+------------+-----------+------------------+ Firewall-as-a-Service (FWaaS) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for FWaaS: +---------------+-------+---------+------------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +===============+=======+=========+============+===========+==================+ | **Meters added in the Mitaka release or earlier** | +---------------+-------+---------+------------+-----------+------------------+ | network.serv\ | Gauge | firewall| firewall ID| Pollster | Existence of a | | ices.firewall | | | | | firewall | +---------------+-------+---------+------------+-----------+------------------+ | network.serv\ | Gauge | firewa\ | firewall ID| Pollster | Existence of a | | ices.firewal\ | | ll_pol\ | | | firewall policy | | l.policy | | icy | | | | +---------------+-------+---------+------------+-----------+------------------+ Octavia Load Balancer ~~~~~~~~~~~~~~~~~~~~~ The following meters are collected for Octavia Load Balancer: +---------------+-------+---------+------------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +===============+=======+=========+============+===========+==================+ | **Meters added in the Gazpacho release** | +---------------+-------+---------+------------+-----------+------------------+ | loadbalancer\ | Gauge | status | lb ID | Pollster | Operating status | | .operating | | | | | of a load | | | | | | | balancer | +---------------+-------+---------+------------+-----------+------------------+ | loadbalancer\ | Gauge | status | lb ID | Pollster | Provisioning | | .provisioning | | | | | status of a load | | | | | | | balancer | +---------------+-------+---------+------------+-----------+------------------+ Designate DNS ~~~~~~~~~~~~~ The following meters are collected for Designate DNS: +---------------+-------+---------+------------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +===============+=======+=========+============+===========+==================+ | **Meters added in the Gazpacho release** | +---------------+-------+---------+------------+-----------+------------------+ | dns.zone.sta\ | Gauge | status | zone ID | Pollster | Status of a DNS | | tus | | | | | zone (1=ACTIVE, | | | | | | | 2=PENDING, | | | | | | | 3=ERROR) | +---------------+-------+---------+------------+-----------+------------------+ | dns.zone.rec\ | Gauge | record\ | zone ID | Pollster | Number of record\| | ordsets | | set | | | sets in a DNS | | | | | | | zone | +---------------+-------+---------+------------+-----------+------------------+ | dns.zone.ttl | Gauge | second | zone ID | Pollster | TTL value of a | | | | | | | DNS zone | +---------------+-------+---------+------------+-----------+------------------+ | dns.zone.ser\ | Gauge | serial | zone ID | Pollster | Serial number of | | ial | | | | | a DNS zone | +---------------+-------+---------+------------+-----------+------------------+ Openstack alarming ~~~~~~~~~~~~~~~~~~ The following meters are collected for Aodh: +---------------+-------+---------+------------+-----------+------------------+ | Name | Type | Unit | Resource | Origin | Note | +===============+=======+=========+============+===========+==================+ | **Meters added in the Flamingo release** | +---------------+-------+---------+------------+-----------+------------------+ | alarm.evalua\ | Gauge | evalua\ | alarm ID | Pollster | Total count of | | tion_result | | tion_r\ | | | evaluation | | | | esult\_\| | | results for each | | | | count | | | alarm | +---------------+-------+---------+------------+-----------+------------------+ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2433567 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-system-architecture.rst0000664000175000017500000000442515250267452025236 0ustar00zuulzuul.. _telemetry-system-architecture: =================== System architecture =================== The Telemetry service uses an agent-based architecture. Several modules combine their responsibilities to collect, normalize, and redirect data to be used for use cases such as metering, monitoring, and alerting. The Telemetry service is built from the following agents: ceilometer-polling Polls for different kinds of meter data by using the polling plug-ins (pollsters) registered in different namespaces. It provides a single polling interface across different namespaces. .. note:: The ``ceilometer-polling`` service provides polling support on any namespace but many distributions continue to provide namespace-scoped agents: ``ceilometer-agent-central``, ``ceilometer-agent-compute``, and ``ceilometer-agent-ipmi``. ceilometer-agent-notification Consumes AMQP messages from other OpenStack services, normalizes messages, and publishes them to configured targets. Except for the ``ceilometer-polling`` agents polling the ``compute`` or ``ipmi`` namespaces, all the other services are placed on one or more controller nodes. The Telemetry architecture depends on the AMQP service both for consuming notifications coming from OpenStack services and internal communication. .. _telemetry-supported-databases: Supported databases ~~~~~~~~~~~~~~~~~~~ The other key external component of Telemetry is the database, where samples, alarm definitions, and alarms are stored. Each of the data models have their own storage service and each support various back ends. The list of supported base back ends for measurements: - `gnocchi `__ The list of supported base back ends for alarms: - `aodh `__ .. _telemetry-supported-hypervisors: Supported hypervisors ~~~~~~~~~~~~~~~~~~~~~ The Telemetry service collects information about the virtual machines, which requires close connection to the hypervisor that runs on the compute hosts. The following is a list of supported hypervisors. - `Libvirt supported hypervisors `__ such as KVM and QEMU .. note:: For details about hypervisor support in libvirt please see the `Libvirt API support matrix `__. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2433567 ceilometer-27.0.0.0rc1/doc/source/admin/telemetry-troubleshooting-guide.rst0000664000175000017500000000155115250267452025551 0ustar00zuulzuulTroubleshoot Telemetry ~~~~~~~~~~~~~~~~~~~~~~ Logging in Telemetry -------------------- The Telemetry service has similar log settings as the other OpenStack services. Multiple options are available to change the target of logging, the format of the log entries and the log levels. The log settings can be changed in ``ceilometer.conf``. The list of configuration options are listed in the logging configuration options table in the `Telemetry section `__ in the OpenStack Configuration Reference. By default ``stderr`` is used as standard output for the log messages. It can be changed to either a log file or syslog. The ``debug`` and ``verbose`` options are also set to false in the default settings, the default log levels of the corresponding modules can be found in the table referred above. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2811275 ceilometer-27.0.0.0rc1/doc/source/cli/0000775000175000017500000000000015250267567016151 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2443566 ceilometer-27.0.0.0rc1/doc/source/cli/ceilometer-status.rst0000664000175000017500000000373715250267452022357 0ustar00zuulzuul================= ceilometer-status ================= -------------------------------------------- CLI interface for Ceilometer status commands -------------------------------------------- Synopsis ======== :: ceilometer-status [] Description =========== :program:`ceilometer-status` is a tool that provides routines for checking the status of a Ceilometer deployment. Options ======= The standard pattern for executing a :program:`ceilometer-status` command is:: ceilometer-status [] Run without arguments to see a list of available command categories:: ceilometer-status Categories are: * ``upgrade`` Detailed descriptions are below: You can also run with a category argument such as ``upgrade`` to see a list of all commands in that category:: ceilometer-status upgrade These sections describe the available categories and arguments for :program:`ceilometer-status`. Upgrade ~~~~~~~ .. _ceilometer-status-checks: ``ceilometer-status upgrade check`` Performs a release-specific readiness check before restarting services with new code. For example, missing or changed configuration options, incompatible object states, or other conditions that could lead to failures while upgrading. **Return Codes** .. list-table:: :widths: 20 80 :header-rows: 1 * - Return code - Description * - 0 - All upgrade readiness checks passed successfully and there is nothing to do. * - 1 - At least one check encountered an issue and requires further investigation. This is considered a warning but the upgrade may be OK. * - 2 - There was an upgrade status check failure that needs to be investigated. This should be considered something that stops an upgrade. * - 255 - An unexpected error occurred. **History of Checks** **12.0.0 (Stein)** * Sample check to be filled in with checks as they are added in Stein. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2443566 ceilometer-27.0.0.0rc1/doc/source/cli/index.rst0000664000175000017500000000034015250267452020000 0ustar00zuulzuul============================ Ceilometer CLI Documentation ============================ In this section you will find information on Ceilometer’s command line interface. .. toctree:: :maxdepth: 1 ceilometer-status ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2453568 ceilometer-27.0.0.0rc1/doc/source/conf.py0000664000175000017500000002353115250267452016676 0ustar00zuulzuul# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # Ceilometer documentation build configuration file, created by # sphinx-quickstart on Thu Oct 27 11:38:59 2011. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) sys.path.insert(0, ROOT) sys.path.insert(0, BASE_DIR) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. # They can be extensions coming with Sphinx (named 'sphinx.ext.*') # or your custom ones. extensions = [ 'openstackdocstheme', 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'oslo_config.sphinxconfiggen', ] config_generator_config_file = os.path.join( ROOT, 'etc/ceilometer/ceilometer-config-generator.conf') sample_config_basename = '_static/ceilometer' todo_include_todos = True # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Ceilometer' copyright = '2012-2015, OpenStack Foundation' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['**/#*', '**~', '**/#*#'] # The reST default role (used for this markup: `text`) # to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] primary_domain = 'py' nitpicky = False # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme_path = ['.'] # html_theme = '_theme' html_theme = 'openstackdocs' # openstackdocstheme options openstackdocs_repo_name = 'openstack/ceilometer' openstackdocs_pdf_link = True openstackdocs_auto_name = False openstackdocs_bug_project = 'ceilometer' openstackdocs_bug_tag = '' # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Ceilometerdoc' # -- Options for LaTeX output ------------------------------------------------- latex_domain_indices = False latex_elements = { 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', 'maxlistdepth': '10', } # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False # Disable smartquotes, they don't work in latex smartquotes_excludes = {'builders': ['latex']} # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'doc-ceilometer.tex', 'Ceilometer Documentation', 'OpenStack Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'ceilometer', 'Ceilometer Documentation', ['OpenStack'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Ceilometer', 'Ceilometer Documentation', 'OpenStack', 'Ceilometer', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Epub output -------------------------------------------------- # Bibliographic Dublin Core info. epub_title = 'Ceilometer' epub_author = 'OpenStack' epub_publisher = 'OpenStack' epub_copyright = '2012-2015, OpenStack' # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be an ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True # NOTE(dhellmann): pbr used to set this option but now that we are # using Sphinx>=1.6.2 it does not so we have to set it ourselves. suppress_warnings = [ 'app.add_directive', 'app.add_role', 'app.add_generic_role', 'app.add_node', 'image.nonlocal_uri', ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2811275 ceilometer-27.0.0.0rc1/doc/source/configuration/0000775000175000017500000000000015250267567020251 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2453568 ceilometer-27.0.0.0rc1/doc/source/configuration/index.rst0000664000175000017500000000160515250267452022105 0ustar00zuulzuul.. _configuring: ================================ Ceilometer Configuration Options ================================ Ceilometer Sample Configuration File ==================================== Configure Ceilometer by editing /etc/ceilometer/ceilometer.conf. No config file is provided with the source code, it will be created during the installation. In case where no configuration file was installed, one can be easily created by running:: oslo-config-generator \ --config-file=/etc/ceilometer/ceilometer-config-generator.conf \ --output-file=/etc/ceilometer/ceilometer.conf .. only:: html The following is a sample Ceilometer configuration for adaptation and use. It is auto-generated from Ceilometer when this documentation is built, and can also be viewed in `file form <_static/ceilometer.conf.sample>`_. .. literalinclude:: ../_static/ceilometer.conf.sample ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2861276 ceilometer-27.0.0.0rc1/doc/source/contributor/0000775000175000017500000000000015250267567017754 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2463567 ceilometer-27.0.0.0rc1/doc/source/contributor/1-agents.png0000664000175000017500000014157115250267452022103 0ustar00zuulzuul‰PNG  IHDR—û�a»3bKGDÿÿÿ ½§“ pHYs  šœtIMEß  >–„ IDATxÚìÝ}xUå�ïÿ�süé�<@¦ PBCÇlñªI€¢cÂUsô�ˆ½¬„ ‰ÅË  aÎH‹­@­txf-f(0zZHR™‘à@¡<$ôîxÄ„H,ḷלÃï�d­¬½÷Z;{…<’÷ë%Y{gí{¯ïºïû»î‡;nÞ¼yS¸ðgÀ-’Ë×H.\#¹ píNŠ ÿ¨¯¯Wyy9 G¤¦¦:þŽär?qîÜ9�7Ž‚Ðc¢¢¢ô§?ýÉöw,‹ÑOìß¿ŸBУêëëuäÈÛß1r¹;TÃG¥ t‹KV©¹±%è1$—û¡éO¦jnÎS€nñã¬5:êBÐcXàÉe€k$—®‘\¸FràÉe€kwRèΟº K«5fl¼Â"ÂtïØÑ Ћ¹<@å|o¹›©åÏ®éðØ59yz|l¦voz«KþöÕ+×´ÇÛ?|l¦›©«W®™?kjhVÎ÷–ëÇYkôÆk;ôã¬5Z›³ÁñøžÒÔЬݛËcãK¿êÒ²êJóÓëñ±™ª8y�À-#¹<554ëÒÅ*IRÅÉ &?ù°Z’4æþø[þÛûw¼­ùi‹ÕÜØâóó�ÛÎ'<2LÃG5~âðïuéb•Â#ÃôDÖcz"ë1M2ÕñøžðñÅ*-þÞr�(=ð»KÞÖ²zð¡úÜwn$á»â{XcºÔ–,� Ssc‹J[¦D‡dèÕ+×Ú“’]°Å��%&_‡�ªu;V*<2Ìçç'·&pÓžLÕ /=kþ¼©¡Ùöøžðþáßëê•kJ›•ð»˳Z˪�%p�ï|XìPE 'pËH.@çÛF*Oš6Q'/¨t™æær¨ÔáýG5wÑS Kc”î½÷�öùySC³^Y¼Áv½æˆÁází×+Ì„äÇ«ôã¬ö�¯^¹¦g­Ñ#Ó¾£•›–I’ùûí¥ùæï­Œçÿf�"‡ûo=ç…µ{ó[jjhöyý�ƒ>çdØúZ¡ö¾m[N‰= —ó—šïÿúO·˜#¹%é�×vH’þùƒ4ÿýþáßkÁò,Ízöq󸥧´u]¡ÏƃÆzÍþÃðÒÖª©¡YÿüÁ?jíâ >ë;—ªL‡ö—iݯW„œ`6¾ó«µ×ôÜô¿ñ)Ÿ…•½ïîÍo™åóöŽúçþQs=%I*ðK² ÿ"¹<ÀØ­¡<½-‰[ºß7YùñÅ*3kÝ ÎHÒ>2í;úEáJsTëð‘CµrÓ2sé#ùi¬�üym�¤Öõ“ÛFM[ÏiÌØxŸãÍõ™Û–�0–�ð?Þ8×…¦u;VjÖ³�køÈ¡ºwìh­Ü´LÃb‡šKRH­ íÒß–)<2L›~»NÓŸLÕ½cGkøÈ¡šþdª–ü|¡yœÁº„Ńùž“ÝzÒM Ífâø‡?_è³®õ¤´‰ZÑ6jûDé)}lm|¾«W®iŒ'^›~û MJ›Ø¶ŒÉD¥µ�ümnlqõ�K­ ò^zV÷Ž­{ÇŽÖ’×êÁ‰­ç러6–±ÛœÐiý슶×<‘õ¸ùóˆÁáš›ó”œø€Ïu€þ�äòc$W­£Q'¥M4“¯‡,‰?ãØa±CÍѹ'/¨âä…G†™ XÆæ{'ÿÞü™1W ÜTÎø;þKoØ�šv:ÞX†aÒ´‰¶K8ÌÍyÊ=+µ�¦¶Ž<¶jrHÜ:�æm/×öDì�ƒjjhÖƒ°] bøÈ¡fb÷¼%‰m¼Wxd˜¼”ð:#©æê;;Ôgù ƒñ}XéÖd·]yçk—x–¤ÃûË–&™›ó”Vä/5“ãèßXsy€1ˆþ ÞYY�iëºBÞ_f&B/›ÀYFŸ8ܺöï¤i×û}ð¡¤Í¾?3FÏÉTŸßÙ$‘­Éhÿä¦Ýñï·%²�e+üù'w�å7¤öº­çY­¦ÆöѵÖÏn=Ö¿üÚ“­íçjŒŸåpNÆyœ?å; ÙøŽfe=n[ÆvËQ„ò�ÏÍyÊö÷vIjs#ljöÚ=¤�¤´Y©:PxP'/è¹é£'²37 tÚ¼ýÉåÆi4pÚ“©Ú½ù-Uœ¼ «W®iøÈ¡æ(ÝDK‚ñü©‹’¤IÓ¾ãø7ìÖÔ5“‘~ÉÚ¦†æöe.,£`�„¨rÓîxãØðÈ0ÛQ¶vššµu]¡í Öd«5yú±Í’"þçkœ“u�ãIi;,«a–÷3Ø�Ø”±Ór¡|çNß™Ýòƃ§¿a~?~¿¿wìh­Û±R_ú•>¯½Öº¹ß¦·”ödª,Ï yBô}$—§5”¥Öuq'M›¨ÒýeÚ_ø¶^xéÙö5w- [sÍæ IÂó6££+lFöJ¾ËIX�v£¦�Ž7’­NÏù36$ljhÖ°ØÖ5–ÇܯðÁáæˆæÇÇf”“Óh])0io&�c‡=ãuF²º£å(ì–ßå;·.mpŒM"Ùxˆàôw‚­ÇœøÐúõá|�(=¥ý…uþTëúÖ—>¬Öº_¯ Á p› ¹<€Ø­¡l57ç)•î/Óû‡¯´YíËH¸]Îàýw[—¨°&�$ª2ò¼SÒÙamc»ã?·)mUqò‚>¯½¦{<ñºwìhíÞü–¹ò/ W¢´uéÿ‘Ðç– ¶„Çw?¯¦†f…G†µoVØÁrNåÕÑw1Ä>¡ÛÔÐl.)b)ݾibàß±®Ëm|Þ«W®éóÚk ‹h/³Ii5)m¢*N^ÐÚœ<]jÛtÑiyô/lè7€8�6Ì]½rÍL8ú'ƒ�‘¸Í~›µöïxÛ)k,a1íŸ|5FÍ$�Ö6¶;~ØÈࣃ_ÿɯ´ñ¥_™‰V#qí´LÄ��Ûþ†ï¹^rÚ`Ðf=ici kâÙßÿ´lBèÿu´E¨#—�÷»díóY šß«ñÝXGOû/ÿÑÔÐlž·õóVœ¼ g­ÑÖ× þFâChVÖã>ç€þ�äòb$Uï 2êÕØ|nw[Ñ?Á:)­5!»û—ÿ+àµ'/hëºÖäâ /e™?7FÛ�Ƶۜ.ØÚÆvljØó§.¬÷üÆk;tõÊ5sù ©=azµ6pij±î´õ}Û˯ý¼|~n³žô½cG›‰x#�kµñ'¿RÅÉ � ÓKYµ�ضO;­™ÝÑw¦ý;ÞöùÝÇ«ÌïyÉk ÍŸÛ­Á,µ¯Sm.Cbù¼Æùž?uÁ'9m°{ ú7–Å@ŒÑÀÁ““Ò&jXìP}^k¬cì› œ»è)�(ý½.]¬RÎ÷–kú¬T 9T'Ÿ27Ç›»è)ŸMìŒÄâ'Þjm}­Pá‘aš›ó”ϦwÖD®ÓÚÆNÇ9TOd=¦…µø{Ë5wÑS ¦Òß–™ Ü›–šÇ§=™ªÒýe:PxPwèMJûŽ.]¬Ö‰Ã§Tqò‚ÆÜožƒ�ÿùËÿ¥1÷Çë‘ï~G÷Ží¸žô’×jù³k´{ó[ºä­VÚ¬T576ûœ×º+}–(±[çÚúù�F€ûÎ�ÄúÖu…ºä­Öð‘CuõÊ5óûúáÏú”ç˜ûã¦æÆ­]¼Acî�WsC‹N>¥ðÈ0¥Íj-?ëF�÷Ž­'> ó§.襬UÚ¬TMJûŽ®^¹¦Š“Tº¿Lá‘aæfô$—ër­¡<+ë1s²":bp¸~Q¸R_ú•Οº 7,£TÇÜ�X–Z“¹û êóÚkÚ_ø¶œø€æÊ²®¯Ã:ÌN#‡íFõ¾ðÒ³’ZG ¿ñÚóçN|@ ^ÊòIÆ&>ô€,ÏÒîÍoiáÛÚ_ø¶ylþoÖéýÿץ«FAIÕ¥§t¢ô”ž˜×:ÊÛi �ćк­ee¼Æú^Od=æs^Æwd7bÛúù�Öcö÷±¹nr¼ææ<¥¦ÆfŸQÔÃb‡ê…—²¾¯ˆÁáZðÒ³ÚúÚŸó~"ë1½ðÒ³ÊùÞò¶÷í󺛖ê@áA³L�rµ~Ã;XÂýÇ7oÞ¼I1ô}?ûÙÏ´zõjI­#ƒû¦hÖ‘Äþ›ßÙ1’§ÃGí¶$£õœ:ú;M ÍæåPÏéã‹Ujnl éóÚ½®»?¿›òéè!ƒõØ1÷ÇÛnÙÑwÝÛŸ�óã¬5æ Êû·ÓÔ©SŽaä2:ÍmÒ01Äu‚{êœ"‡»>'7 å®x]o–Ï­$…{â»@ïbC?€k$—®‘\¸FràÉe€kwRýÏÕÚk:ê [47µtx Éå~¨ô·e*ým ×°,F?E!èqN¹IF.÷³fÍÒ¹sçTUUEaèÉÉÉJNN¶ýÝ7oÞ¼IÜ`Y €k,‹��;wNŸ~ú©«×DGG+))Iaaa¶Ü>,Iš6mtâÄ IÒ¤I“( €û3â �¸-�\FPÇ�׎;:õÚ… :®Çr»ûõ¯m&kjjôƒü€‹i³^^¯—ëàþ €xˆGÄ#n ÿåg?ûÙÏ(89wîœ*++;õÚo~ó›òx<º"�¤O?ýTׯ_°‰v\ñ€xˆGÄ#nOŒ\FÈ¢GQLÜŸ=¦®æOº~åK*?ÆÏxâÈõÀõ�ñH<Ä#â· ’ËYLÜŸkì¤1A�¹¨K6¹ì_Œzà›’¤ËþH…ÀõÀõ�ˆG€x@<â¶Cr覊`ü_=`þ› �ë�ë â‘xˆGÄ#n7¬¹Œ *++Í5—c¾¥¡-‹ñéŸtýÓzIRBB€Xs¹£Š`Ä_ UKà }y­Ik&q=p=Ä#@<�ñ€xÄí�ä2‚"¹|k ®€xˆGâ �¸]‘\FP$—o½" B aÀõ�ñH<Ä#â·#’ËŠär×TT4 ¸â ‰G€x@<âvCrA‘\€ �†×@<Ä#ñ�ñH<âvBrA‘\îÚŠ€ �†×@<Ä#ñ�ñH<âvq'Et®"¸ó®;>$LÞ÷«4hðÿ«÷Æèÿ¹»ã�úìãkj¸Ö¬ð!aºó®;õŸ_ÿ§$™ïýƒü€Âîç×Ãà¡áqïÐ�Þ‡ëàþ �Ä#@<Ä#ñˆþŠä2‚ÇûT’ôŸ_ÿ§.ž¸dþûáŒD�¸7&èû\þÿ>Ó™w.:þþĉJHHÐäÉ“)ô>ìøñã]r=|öq�>(:Ïõô�xäþ �ýâ ~Üû3ŠèõŸ7txLsÃ�� £0ÈõÊ1¸?Ä#ñ�ñH<¢¯bä2‚iÓ¦iРAº~ýºÏϽ^¯>úè£N½ç}÷ݰ&utt4ë$õ“'OÖ�7ÔÒÒÂõô�x”Äý Ш‰Gô’Ë€Ë Á_g+�Ç£ôôt ¶7عîψG€x@ªm9¹ü�ˆ»ˆC ›TÖ6�\öò×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\»“"Ð_—�Uye�ªk¯«ú³:IRÊ�’┞:ŽzÁ+E’¤¨ˆ0åÌIsõÚªÚ:íúÝqIÒ£ã=J�๥s±¾ß3ÿm²FÇÆðá¶ÕÙø),>¦Ë¼®QߌVVú 2„û[WÜŸ®+Üê\vNå•—UQY#IŠ£øØè�®é[m«•�öê½3^IÒËÙ·]½þÞ™JÛ²MOIV’g ú,’ËúE#|gÉqUÕÖÙ60$itlŒ¶®œO#½v–W}c³Ï™NaÀµGΚ à™©É®: ù{Jµio©$é䮕·|.ÕŸ]×Ú­í�v’˸�Y¯÷¨È0y¬STdXH÷ü£g¼Jïéòär}c‹r7îSÁªù·Eåûrví p]a@«olѦ½¥æõëËkþ_RBœ^ÎÎPÆÔqÝÒV;zÚk> é+ÉåúƽRP¤ôÔq�Šiãõù{JËvíÖ"¥NðhýgM2oÚSªGÇ'�ˆF�cY }º3ãÅõZ»µÈL,'&Ä)=5Y/gg(e¼Gñ#¢%µ>é�ñâz£à\˜ñâz=¿úŸTßxƒÂ@§,Îl­\\vÖÕkKŽž3ãšF0pkõeöší½ze§½ò<±\…%ÔÃp;)÷^6ûd†Ä„8Í›99 OV^Y£ïçnÖ²�{DÙTÕÖÉóÄr‡Äpèý1ëëSÆ{”2Þ£ôÔd¥Œ÷hHÄ ³ž�±0OåÞ˶çñÐÜÕZºq¯ê›èסç1r@Ÿ5ãÅõ*o™žš¬Ù¶ ¨ü=‡´lã>IRöšíŠ��a$ˆ‹dp+2¦ŽÓ²�{õeÓ mÚ{8äðEGΚ�¬ j�Stä¬ŠŽœu-ÖÝŽžöª¾±…/n#õ�-š±0ϼ¿§§&+oI¦íˆãÂâcf›0O©¾l¼Ñå3YR&xô²úÎrÕŸ]¿¥º/Ï!Ÿþî¶UÏÌBªolQþžR½RP¤úÆ=ý£_Ê{`]ÀyïôF.è“–nØkV�ófNÖ[y9Ž#Ï™®‚•í —½Çºn«›�^^�¡ü=‡”:Ác>1·26¢ðßptlŒæÍœ¬œÌ4ÛJ}Æ‹ë%Ië8[ñ±1ÊݸOEegͧø£cc´þ‡³ÍÑ—åÞËze[±O‚;)!Në—d,×QX|L»~w\Þ§ K3ÍÍŒ÷ŽŠ Óâ9ižÛ3ÿm²ãÆLK7ìÕù�ZG�¿³%7àg’´ëwÇÍ�—�c:*·¤„8-ž3]ófNæ¢à’<£?"ZÕŸ]WÉÑsÚ°43èñÖØ˜7sŠccûÕ‚b=ãí’ë.”X1âÑ?ʽ—•ûú>óçe§½zµ ÈgY™Œ©ãôòóéæ ‹�%ÇõJAûzñQ‘aÊH§õKf;vÊN{µio©ÏòQ‘aJ™àQNfËý H‡sº¾l¼¡£g¼]²<ÆÎ’ã>SuƒÕ•F|TvÝ6ÞÊ+ktþ£Å�ˆù¢ô IDAT±�]ߨ¢§s7K’YÚÝŒYIþ›•�öjWÉqŸzÙˆI§¸ñ�ée÷šë]†ºA°‘Èÿ²íov´ÉমZÿÃÙ¦W Š}Ö1÷ß,-”ú(”ºÆZß{­]›ÐZ¿9ëÓ® µ-N›²oÊßsÈüÿ¼%™®Ú…9™iÚ´·µO¬Nrº†�îßNmµPê0cßž`I\c�Së†çÎuŠQ�X�ÉݸWQ‘aŽ×0ÕŸÕuxÌèØå-™­úÆJ±”ÍŒ×Ûž‡]íkÿ:3µ­­i÷=uTgZûÁFùï,9°üâèØ¥Œ÷è§Ù鎣ß�{„SxíÖ"�ÿ¨&h{Á®ÿìó¡k�\Ðç9çS‘‡*u‚DZ3X§ôKŸJÆZ ­ÝZ¤�%ÇõÏ÷× j£b,¯¬±}�ªÚ:}?w³ò–ÌVÊx�Ϻdæß¯¬ÑŒ×ë�-¹>çXýÙu•�öêæM项«ÍFÐ�ˆAú²é†ê[´vk‘ŠŽœUÁÊùŽçöèxçNpEeMÀÓmÿŸUÕÖÙ–M¹÷²íç1>Óó«ÿIEe­çÆèÓ�žÜJÓ²�ûÌ™Áb×:3Á©ãàß`÷¿îò÷Ò;[rC¾îB‰#ºM7ÌŸ³…St䬎žöÊ{`�r7î ØÔ¬¾±E…%ÇT^yY'w¯ ,“=¥Zj³ù�Ñ)3îN‰q `Õ|=4wµ¾lº¡ì5Û•2Áãú¾l$zíâÀ©®´Æ‡]¼�Ž�n‹y¯íÕ£–‘qå•5¶�Åâ²³*;íÕ�ˆA>õ uÏ»˜,:rVY3§$µ­ç¼vk‘ÏFJUµuª®­“‚$—� ‡�:Û®~:ø.�žñê•‚bÛöä÷s7ë�-¹ª®­ Z}°ke@'{õvÇM7­õÍ›ëÔãþ×½ýVTvV�ŽO0Ûµ�iSº­ÛѽŠËÚ7^vJ:Éš9Ùlï•Ù· w•·½ƒµ{œÚj¡Ôaù{Z“Æïüj™í=»èÈYÇ奌sÊ[2[‹çL·­ûŒØ°»þ�¤Œ÷è诊ËÎiÓžRåÌIë ½=Ýñ¾ávík§‘äÆ(õ¬™SêjëgÍßs(°ÎlKŒ;Ý#üû›Eegm¿§{„µ?|³­kWÆÁ¾?ëçëêuÀÑŠe1ô9FÒ31!®K—Uµuš±0ÏLž¾œ�¡«ïþƒ¾:µMWßýsttUm�žþÑ/׫Ì}½5qörv†NîZ©«ïþƒ VÎ7wð}¥ X3æéæÍ›*X9_Þëä=°Îgôµu€ÿg.¯¬QbBœ¼ÖéóË×W§¶)oÉl³‘ðô�~Ùeeœ·d¶Ï“ìy3'ë�-¹>?3Ê­¾±EC")oÉl³Ü¬ŸË¨È1°YG [×S¶‹G£Ñ¹Ø¦]î½l6 ‡D RÁÊù>×�1ý¿¼²ÆíØ“²×lWbBœy^'w­4GZÕ7¶èágÖ¨°ä˜RÆ{ôΖ\]}÷ôΖ\s6EyeMÀ:y…ÅÇÌÄrbBœÞ\¿H_�Ú¦¯NmÓÉ]+Í%²×lgÉ8cNYîìòÖNyNfšNîZiÖ•F}ç_W&%Äé�-¹>#�údÞÌÉæõ+µ&’ƒuŠë×m7ޱŽÔjÝ8j_‡qSXrLK7ìuü̯iHÄ åd¦éåì %&Ä}8f—X桺ڲ�ûtóæMå-™m¶'�6¡Ô:21{ÍvÅ�ˆÖ›ë™ÇXë£W Š|ÞsíÖ"3‘—žš,ï�ufÌx¬3×s5Ú¼v÷ë~(ÆýÁˆ·úÆ3i7–¤ÑËÙæß·¶ÅË+khSöÖ{q’͌ЎX‡NÉζëÑÚÇxsý"Å�ˆîT»'{ÍvŸe<Œkô«SÛÌ:ÌÿZ´~Þï·­/mÄ•µN1ÚqË6îSÑ‘³fÝg�K£eýYG¬má¥÷ÊóÄr-Û¸×Õ²Nçaí×YûkvŸÏh_–S®ÃC[£¿k­3ãGD+=u\[Ù´ï™d�qÿ¿Qߨ¢µ~÷'c£Bã{1î}_�Úf¶£Ë+k¯¥ŽúÆý­£6:�ä2€>+*¢kF-;ë�À Ú§C$VÕÖiÙÆ½Ž ã¼%³µbA†’<£¦¬ô)æäúÆݼyS'w¯RVú�Ž�1;ûFcß©Ñ-µ>¹>´%×gd€u³Â®Ü¼!É3ÊgÄhüˆ˜€‘ßÖr{3/G‹çL7ËÍø\FC¦èÈYÇQ ñÒuní`·$†Ñ¡1H‡¶ä*+}ŠÏu÷V^ŽÙ -;ííñ MüÏ+É3JÛV=gv„ªj딞š¬Co´ÎRˆŠ Sê�Þ\¿È|ëòõ�-ægNLˆÓ¡-¹>‰­$Ï(z#×L”Ùuøk�a\+EGκŠ�Ââö)¬/gghÃÒL39`Ôw‡Ú:ªÆ0ãw©<ŠÑ^wõÉèØsÙÉþÁSÉQßû…‡Úš°ÊhëÀZã&~D´cÜÙM{Kmgç1]Yô mXš© 2tj÷*ÇÛ$–Ñ“Œ¶—Ñž´.Q^Y£øÑ:¹{•2¦Ž3�±ÖGþIciÊxOÀ¦l£cc´ai¦ùþUµu>É7k;/oÉlm[õœy0â-Ø’Ë6î5ßï�-¹Z± ÃüûF[ÜÚ¦äAjï«oºáÓOè £>rÉ*Io®_äÓÇȘ:N'w¯2¯ãPÛ=Öë&'3-`3xkVߨÐß³¶?�¸ò©S¶äšçTXr̬ûÚÿFbBkÿÊÍL–Œ©ã|+õëôÖëî‰Ï롹«;L6;�‡µ_×Ñç{+/Ç'ÁìÔ¯»yó¦mÉ5ëÌÊ¢_htlLÀ~*Ö·þ #IïßWÈßÓ^OׄñúÔ ­}ä” ³��Æçóï?lXšiö‚µ Ðy$—ô­†Œ¥!;$rP—¼§QÑ¥§&;v³Ò§˜ªSblHÄ Û©HÖ‘TFß_(�²<‡µX³Ò§˜�™�!l–ÖU߃ñ·‚­õºxÎtóÜœFecà°&~œ’ZF")=59àz/÷^6³‹çLwlœ¯X�a^wFg¹'“wvqj]ç}…ÍZñ£ccÌs¶ŽÞÜYr̼ï­² 1²… KÑ‘‚Uí�TcÆM(¬ËÕ8mÚ”äe&�ÜÄž1ªé½3•>?·.ÉdŒ*².�å/Fg¸èÈYËè¦LǸ±®j}°e5oæ”�fIYËÆ¨(Ëè.)ãí—z³Ž ]<Ç~?£]jMèÕ7¶('3Mé©É¶³†ìÞßúzc„iüˆhÛ¶°oƽǩ->oæä�Ú”NKw çT÷@ò-=5Ùv¦HTd˜ÏÌR»-þ¬×h(u˜µ¿WU[çÓþ´‹«¨È0¥§Ž šà쬬ô):¹{•æÍœCå•5f²ÙóÄòNõ‹Žœ5ëÚ`uæ¶UÏYêLû~]ÆÔq¶íóúƽœ�¡”ñŸ�¾�ú þŒu´ƒ]N#ÂýûNK¸Xï}NmtÉe}е²û²ñÆ-¿ŸÏæ©Á×o¶&ÆìžÖ&%tüº³OöãGD}ÊmŒðì©��­�ùŽ652’jç?ú” x€Ë˜:.èŲÓí›kdÙŒZ¶^ßm{œ;$ã:¿=ÒáwˆëýÁ)–íîÖó6ßú;f ÿå1„8ÅܸS:¸çÉ'§%,ìõˆÿúþF]cM¤ù×sƽÄú@Êç^$nŒÍƒ$™›Ö:�[0þ#–ßÌË!±Œ^©küG'†Z×£ƒßÊËqŒ™úÆÝ¡;lg<JÒ–ŽŠ ³m+[묎sÆçö…Þí“u[»1Èõd��Pú?Öõ¡ƒ�»q Zû{Ö~O°:pÃÒLz#WoååtKݽmÕsúüßòõΖ\3QkUU[§çWÿ“²W»[:ƃÁꮨÈ0K�Y´üœÚ‡ÞÈulWÕÖé²eöžõçFÛ X�l� åÔöýY?_E÷6ô0`Äw°…õ÷_Ú¬»ÝáßèÌšdN'Uµu®7ÕpËšðz¥ (è5cŠ?Ó‹ µ>y¥ HÅeçTߨâÓÀßiUb×¹­·ÇemâGDkHdX—’°ÞÏhK÷’ËúœôÔqÚ´·ÔœrëfDâÝŸWRBœæÍœì³v›[)}¬ÃhM’wö3u–u§a #£cc”žš¬â²sÚYr¼}„¢e�T§MÜ$�¬£z:&ºCbBœÏú°ÁDE âBCH±¸bA†–mÜg.�ÊýüåìŒ�ë@7³uRÆ{´³ä¸™P6ÇC"™õ|Êx�™\–¤"Ë4çÎ&™ºb¤±‘g|lŒ2~¬/›nhÁšíú`×Ê™:t…ìÕÛͤ�‘HN™ài]š-!NIžQ*;í õÙÙYyv VÎiä£qCïÖ!Æ¿Î,SbÝâVûU%§­í¢y3';bð×ÛíÇü=‡T\vNÞÒ(ä$Ï(å-ÉTvÛrWn}ÙÔ}3ü—�1H)›ºéwµ,‹eçò¯wø^<˜êlè ÏImEa4LB™ÚTߨ¢W·[:‚i Z» Æ|Ä–i€]9J#å•5AGWìÎ4d:³¼ˆO¹ ^nÙ«·+{õvǤ`g$}й¾^qÙYŸ]¸³‚Œ&ñÙ¼¥ƒim%GÛG4º¬c\ÜÁ=¢;¤8ltfw�›ñâz-Û¸·Ã˜¬ VÍ7ãñ•‚bÛŽÙèØ³Þí(ò÷Ò÷s7ë•‚"WËgDE†)½m£Î¢#gÍ·v"­÷€ÂâöŽªÝg¼¦£Më[,Ó²»¦^_›ö–2ý‚5©”·$ÓqĽSiÄn°¬Öx³²ÖÓ;¦ËÖn-RöêíæòZè]óf¶·çr_ßò=¿°ø˜yoLïqLökÏøŒ|q}}kû0Øûú×aÖ÷–€=zÚ«§–mÖôÖ‡¼¡m0S“}êÖ�úu–äo¨Ë1Z?_GmÈöÍ;“]}–¢#íå,±l×¾·¶AÞ ²r¹÷²í5è¦ÿðýÜÍ­³8Š�à]Œä2€>É:=üágÖtØyÌÝØÞà™7s²Ùˆ‰Š 3Ÿ–ï,9îø>åÞËæ& Öé{ÒÚ­EŽ� £¡cm„ø6¼Ž¯íÌ”à$Ï(³‘¶ëwÇ“e§½*,9¦BËÓjÀè�qg$«†D º¶�õÁÒ«ÛŠ¯ÝÂâcæ5™âhãzvê—�ööÊ5lͳ`�óîßù{JÛFž–vÛÚî¸=Ëcõ¥Óý܈ÙòÊÇNW}c‹^)(nY|Ì1aà»FRø½3^3 eíôZwrÏ}}Ÿ¤ÖQHþ£È¬ë/ÇuT¯f¤vݺšÖ„ý‚¶ÑØ@WU[gî�àÏX⪪¶NË6îµ=&wã>Ç{�×Åeç‚¶Å_)(RaÉ16‰î#¢"ÃÌDa}c‹žÎÝÜaRµ°ø˜¹lÃ�ˆA*X5ßñØ]¿;n{ÿ´²«ì÷øªÚ:ÇDm}c‹r_ßP‡%yFù´?�£±C=§PÚÊF]²l㾞­1Ò^.Ö‡³Ö¨þ³q�Mu%}p³vk‘ù}te�imk;m¤g +;íµ-ã»»Õþƒ1‹ÃS@Ü:’Ëú¤Œ©ã̤K}c‹zf�²Wo÷©�Œ„°ç‰åæT?»uKg¦™ꌅy£ŒÊN{5cažÙZ‘�Ñ+ŸyÓÞÒ€{Ñ‘³f-1!ÎlÜ·w°“Íϰi�ïnö›ö”*{ÍvÇš­ŠËÎêèß ß(ÇúÆ=üÌš€'Ýe§½z:w³Yn/÷R¹¡o2‰å•5Ú´÷°ÙˆîˆqÝUÕÖiÆ‹ë:˜Æum4¬C]WÏèØVÕÖ){µo2¨¸ì¬y-÷´Ñ±1ÊÉloPûæÖd^‘ÙHïaJ1:�Ó;ùµxN{]™½&pä “Fìø×µÖ‘CÆÒþI#)\nÙ¹Þ¿ƒžb•l}M°¸ù~îfŸ˜6âÆH¤Œ÷té¦M£cc|’mN‡�¾Â:íüÕ‚bÛ¤Ï_-Ì ú`Èhsæï)Õ÷s7ë诪jëtôLkÝUdTrÞ’ÙflÎX˜gÛ¦4Úâ’hSö±>™q¿-¯¬ÑCϬѲ�{}îï­›6ŸÕŒ×›m4£žèh‰3»v�õg¡îG±xNšy�/۸ϱkŒ4űýùýÜÍ甽z»Yw9]Ÿ»JŽÛÖ}N¢"Ã|>_öšíšñâzíl{ã|ŠËÎ*{õv=ôÌÇ:Ø:0ªøè9Ÿ~]Td˜Yg•WÖ|>I>mÍÄ„8WK ø·ì@í,9ÐÖ¶ÞoÏ™nÉ^³]Ë6î5ï1Ƶl¦�õû{ø™5Çî,9n&§‡D 2û)è:¬¹  ÏÚ¶ê9 ‰³¬¿|,hÃ51!N‡¶äŒ:66?X¶q¯Ù`1¦ßTvݬ\‡D RÞ’Ì.yÝC")O©ò÷”*u‚ÇçÜâGDkÛÊÀ'ÿ+²3tô´W_6ÝÐÒ�{µtã^�Ž� øLÙ#"��×Ê+k4ý…Ö \¼ÖitÛšx+ç›kd~?w³¢"Ô”çsn’ôf^ë[! ùblc\+¡4ä2¦ŽSÞ’ÙZ¶qŸÊ+käyb¹’âæ³|Lüˆh½¹~Qȳ ÏIÓÎ’cú²é†y/IJˆóÙxÄø»=mÃÒLUÖ:b¬ì´Wž'–›÷(ëgNLˆÓ›y‹¸¸Ð)«æë¡¹«õeÓ ÇNî¡-¹æNôk·iíÖ"¥Nð´.?aÙŸ“™�¬µ&¯ŒdkÊx�½‘k{_ðïŒútP Úÿí4;aÃÒL}ÙÔ¢�%ÇUtä¬ŠŽœ5¼ôDܬX�¡¢²³ª¨¬Ñ¦½¥Ê˜:Ž?è³²Ò§(o©**kTXrLGÏx͘µ¶é¬õ`um�d¹¦·­zNÓ_\¯ŠÊ3æüÛá’l7ëJòŒ hSJ hïJ­›þѦì[6,ÍTRBœÙŸ0ú+NâGD«`ÕsÞÛÚaž'–ÛÖ5yKf‡ü`0*2Lo®_P‡#z­ï;oædsF�µý™“™¦M{Kêk¢rÞÌÉ>‰Wëˆa£}é_÷u›’̲-;í šD5úvvåbôm­ñùÕ©mf�UýY�O�i×¾NLˆÓ›ëÝ×™F;»ú³ëÊßSªâ²s÷˜!ƒÌ26¾ë5b´A**kl¯±y3'«ºöºíègë÷g³q�¢1ŒÎ².�á$É3J•E¿Ð¼™“ÍQÌÖ%câGD«`å|Û�‡FÇÆ¨`å|ŸÙ2vë;[cÐn4uê�ùC"�óm«žó‰£S^ߨ¢øÑz9;#hÛàVYú²<ú:»v°Ñ¦›7s²¼ÖùŒ46â5DE†éÔîU*X9ß'v[g ¶µÃ#œcÍ¿MéßÞMLˆÓ;½Ü‡‚~Þë|ê‰ q*X9_'w¯ ©o�·$S/gghHÄ €ºæÍõ‹flvÄ®+¯¬ñyß¼%³×Þ°4So®_P§{mTdX@ÝwôŒ·ËËÖh žÜ½Ê1F¬çn¶u-£¨ýëLkûÚ¨3mÉíÔÃ#¹o�)è�9¹{•6,Í4?£ÿìÆCî¼%³}Öj7®«m«ž3ËvˆÍ&�FÞÀx­ÑomŸ •¬C[r{m Ùí7oÞ¤स¸X%%%’$Ï#£5vÒ˜ Ç_Éx²\U[g&xÝœ›õunËÅ(“`¯/•[OY\pÒüÿ7ÞxƒxìÖëÚHÁqÔd}c‹*>jMZGÒ¦¼=ue2ÒÍõ`]­;û��©SºªºÕ² ¥<»³ÎìÌ{/ݰWwÜ|�ëò9Q‘ƒhô1,‹lEE†©°ä˜¹f²��%ÇÌß‘8Fw©0—¸ qx;©þ¬Nù{J•ûú>Ç{Œ±!Ÿd¿Ù/z#—€­ŒÔds)‚‡ŸY£—³3­QߌÖå?^×Îâãæ¦Ûé©É!o„ÂoÝÌ->6š‚¹­î1ãT\vNUµuz:w³ÏIÓ¨o¶~Ç—ÿغI ±ÌINf£’û ’ËÀV’g” VÎWöšíªª­Óó«ÿÉö¸ôÔdÇÍÒ€ÎÊßSj>Ü0Ì›ÉÆ�·“¬ô):zÆ«�%Ç}6Rô7oædÛM…ÑûH.@/Ší³k6Ð×d¥OQÊ�^)(Rye�¹ûÑ_7jX¤æM»ßöwÕWµëÝuôüe¾vPÿòê,  �‡ä2€[¶»­óœ“‘¤W÷œÒîw½!uvï‰Ñ¿þüI‡ÆAƒfÿü *>©Ó ÿ®Rx �ñH<�TòÁ'’¤¿{þ/õ_º_»ßõ†”ÌŠ©ŸÎyÈñ÷�>«þþ]=EÅï_²�Q€xngF¸å—®©üR�†„ߥEéI~—Žž¿¢òK×né}ã‡Ö¿¼:KCÂï’$·5@�ñH<nì<|QõÍ_)ñž¥<8R£†Eš³ nÕ¼icÍ™ŸÔQØñ 8$—ÜcJSúÃcq·Òãóó[q·Ù8¨¾Ú@aÄ#@<�€k»ßõJ’ži›NŸÓ¶y˜ñó[e<ì©oúŠÂˆG`À!¹ à–SšŒÆAú#÷˜?ïŠ ½¡åk³# €xˆGâp£újƒŽž¿Ò‡ßãóß®˜M I—?o$â°H.è4cJÓ¨a‘æŽôé�Œ1§6pé–ÞߘR,µnÞ€xˆGâpÃØXsæÃ÷˜ë“ǬG¿+éÖg¼ºç¤ªÛ’Y3Û’dˆG` aC?�fL]òß±wÞ´ûõêžSúeqE§v±¯¾Ú ÷þP«W÷œ”Ô:µÉ˜N €xˆGâծíÉ,cö€á™i÷ë½?Ôš³ œF9^þ¼ÑŒ9%|b>è™ùð=J3”ˆG`À!¹  S¬Sšžù®oçù™ï¶vžË/Õéèù+æ¨-ïý¡Va›ƒþ�!áwé_^�Å´&€xˆGâp¥øýKªþ¼QCÂï x 3oÚXýhÛ¿›³ œøTÞ¨W÷œ úw¥'ê§s¢Àâ�H.ècJӣߎ5§4Œ©Mïý¡V»èØyæÑoÇ*ñžåd$¼?â ‰G #Åï·®}~·íhǨð»õeó×AgŒ0 Áú»”o�$âÐH.è\ç¹mJSG£«v½û¡þîù¿´Yõè·cõ¯?’ˆG€x$�.Ußô•ù°§£ÑŽÁfÄ‹d$@<‚ä2׌)MFØIÅ'uú²ùkíz÷Cåd$Qpñ�Ä#Ð#ŒDÖ¨a‘Úúß§9·àïëòç��žM€x:’Ë:Ñyn�ÒôÌwï×Ö¿ Ò8ø‡µëݵ¹¨œÎ3@<Ä#ñô˜Ýmɬô‡ï š¤ÊÉHÒ�¶ý{ÐÙˆGÎþŒ"à†uJ“ÿ.¿þži[«úóFss#Ä#@<�@w*¿tMå—ê$©Ã8Ö�7�@<Ée®•ü�ð»”þȘ Ç¦<8R£†EJ’6•Sxñ�Ä#Ðí6UH’ï‰éps¯¨ˆ»5óá{Ú^G<Ä#·H.p¥}JÓ˜�Ž7žN—|ð‰ª¯6P€ñ�Ä#ЭJ>h[¢fÚý!?�Ùñ ÓXs@ȬSše$†ôšô‡ïÑ�¶ý»¤ÖQ]ìî �ñH<Ý¥újƒ‡Ö)öAãñ‘1ú霉’¤/›¿’$=úí‘ú陳 :ÃxÏø[x€x$�¾Žä2€�%�ª–¢E®^?|pÀk~:ç¡[îD»=€x$â¸ýÅÜ©8òMʃ#ƒn<Ö™÷ˆG⸱,À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.\#¹ p�ä2À5’Ë×H.àÿgïþƒã¬ï<Ád›+6’å-lƒu¶c9 E5»�- W,×ün†µ $³ªFr {S$¡�ãTÈíÆky˜KÈli,WH\·3)ZsµÙ'fÍ�Y°<ò΄Y$2[Yƒ±R°�:%V¤ÝâyènuËjYòéõªJÅýôÓÏóô÷ÑCw¿ûÓŸ/@Ù„Ë”M¸ \§FÇÿ\±çÙ3±iíÂX|ÙÅ1&p!<õò{Åëc®_Ÿ‰Øý_ÞŽŠ _þàzÈ%\†y¦ïäXô�|÷¢:¦Ÿ/~rܹÁõèØÀßü™÷"½rF€¡ŠŠŠs}@¿E‡y`ùòåÝ1]uEE\uþì·¶¶Ö óêz›pmºq=žÿë/÷ÚK,¯�Ì'™L&jjj.úã¼ÐÁrMMMd20¸/®G§ræÉ›ƒÊÊʸ ûˆÞÞÞ¼Ð÷n\ß>òn¼‘3�X}}ýû»|ùro˜ó×c®žžžLoß~ÝÂøÔêŠ ×å§?ýé¸âŠ+\�¸gÈèèh>|8Μ9^q÷�‹¢ïÿïí1`­\¹2n¸á† >N®G·ÚÚÚøæ7¿yQËÓO?û÷ïOoÿÑ ‹âïÞ�<1þÚ¸xñâØ¹sçEYÀsízLŒŒŒÄ¾}û""âž{î‰ÊÊJ' .Âe˜*++#“É\�†ÝÝÝñøã�§·“`yñeãU÷n\˜dõööF&“‰††'×ã,:xð`^°|ãê±yÝ‚¢×å�üãØ±c‡ÊE\�3ôḽ½=/X¾·aQÔ,Žt‚Û$`>~üxœˆ²öïßïu.0á20cžÏ5Xžìô€¦ÿ¡9›ÍæË÷6ÿ©ïd>U» /`>r䈟"B™¯�…“g–óšh‚M˜gë³\ŠþË0»rß¿^uEEÞ³·^“ÿYÑ{R¸°„ËÀŒx>—`¹ðô€¦odd$ÚÛÛÓ Ä’`¹ØbSñ©Úy=`�9¢R J8zôè„ׯ;®›Þ[ïblf³YíiàMµÏr)ú/Ãì]›…í0 ?[Þ•ùà‹×ÁÁÁ¼*gàü.çd6‚å¼Óf˜¶Ü`9"âŽëN;XÎ}#Ÿ0?óÌ3®I(0000ágöçúÚX8Á¦þçpî×i9}–'{],ì¿ìº„s»6sƒâ›7,(úþõê‚jæÇܯzà.Ó6›ÁrBÀ Ó“Ífó‚åÏ×/œÒbSý �[©åš„üŹ=Η-Ž{m¼úŠŠ¸ãºüþç¹!605Óé³<Ù{ÕÂþËæ%€s{[ªF¡ÍëÄúšüÇçŸp˜–ó,ç¾i0CyoÊs'›É`9qWfâ5yôèQƒÏ¼V¬Çy9“gNEaÿó—^zɇi(Ótû,—RØùÈ‘#Þ§Â4ÕÄ›PžÜ/I××Ĥí0н&æ¶ÇèììtíÁy$\¦¬0XŽˆØ°¼"þ¶,žzù½¼ÿõ�|ïœ÷×wò½ ÛýÛþ±Ø°<ÿC€€&^Ÿ_Q1«Ár"é5™üqtt4öíÛç =óîqa�ó™ ¬J)œ`sÿþý^¡„™ì³<Ùkâ]™…y?Ñ×Î.·Ƈ�·º(WîûÑ“'OæM Ì®E†˜ªboŽ¿V:DþæÍÓ®h}g,:ònYÇvÍ5×ÄòåË�(æ�£G�NøEÁù–W_Q÷6,ŠowŸ‰_ŸCßÞÞ;vìˆÊÊJ'ˆ9­°ÇùUK#ÞüuÄS/ç¿>^µ4â+§_×1úÎXü㉱85š¿üŸ|d⮕••‘Édœ(¸6f²Ïòd¯‰w\·0¾ß;þ>öÈ‘#ñ±�},œ(ñ>6÷—w·^³pZ­jÆ¿ÜYß{þLDŒ·ÇÈd2qÍ5×d˜eÂe`Öüì—cQ·¼bÚ�În`` ¯+"â�_ŽÅÿþÔ™ ë~xÑøÏËù™a¡ï=&~rüì×çàà`´··ÇÎ�;�$æôâÜ`9"â�Óo-þåè¿ýÍÓîíú÷ƒcñèŸÚ—®Ùl6¾ñ�oørÞwðàÁYé³\ʧjÄË'Çâǃã_2íß¿?jkk£¶¶ÖÉ€###yëkÆß«N×'VVÄÇWTÄ?ž¯ÚÙÙ_ÿú×½Â,.S¶cÇŽ¼7æÅtwwÇ©S§ft¿555g­öÈd2ª–™—zzzbtttJëþúÌø¯ ¦.÷�|oJÁrbpp0Ž=ªb„9«Ü«§FÞ‹šÅÓûÐüâ±w 8LCaUäÍœ—¶5w\· ~ö˱xã—ciÿe¿è�|ÌûEÁtÚaº+³0v?ýÁ¯é:;;ãž{î1Ø0‹„ËÀ”M¥ââèÑ£3./_¾•Ë̪‘‘‘Èf³éí�¯¨˜Õ 6ë–/ˆO¯ý`û¹ÓÀÌQ¹ À¬êììÌëƒþ�'Æ¢õñwòÖ¹jiÄÝŸº¬ìÉý¦2¹æèèhd³Ùرc‡“3Hå2³ª§§ç¬ë¼q:â'Çß+{Û?9>µÉ5_zé¥p2` —˜UõõõSZoäí±²·}rdj�Ù°aCÔÖÖ:0ƒ´Å`V}á _(yßÁƒãñÇŸ‘ýüÞïý^ÜvÛmΕ˔M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù[844½½½±yóæ³näðáÃQ__ÕÕÕ³r É1UUUE&“É»¯««+=†êêêØºukTWW—\ÿBJŽs*ã p±*.8p ¶mÛ?üp´´´”Ü@WWWlÙ²%""ÆÆÆÎù€†††bûöíqß}÷å…±{÷îhnnŽl6›.okk‹Ý»wçmcÍš5ÑÓÓStýóeïÞ½166­­­é²þþþhllœ±±¸PжÅèééIÿ]Ü–Z·¾¾þœ¦§§'Ö­[�>úè„jãd?…Ë÷îÝÍÍÍñè£�Æ®]»"“É”\ÿ|زeK´¶¶–|ª–€K]ÑÊå$]³fMô÷÷GWWWZq[jÝR÷—£§§'†††Š†¯(¹~UUUZ�ÜÔÔTrý󥫫+"&ÛMMM*–€9¡hårÒ8ié0YõrÒ›y&*„Ë ªK…¸RrLkÖ¬™µþÓÚ„Êå$�—ÛÚÚ¢««+úûûcíÚµyë MÚ~¢««+{ì±t�êêêhjjŠæææ ë>|8{챈¬“~Ék×®�žžžxì±ÇbÍš5ÑÒÒ’®ŸkEEEìÞ½;½¿pýÂcîì쌮®®ŠˆñŠâûè%ëçVBg2™¸ï¾ûòÆ£¿¿?:;;ÓçZxL„ô»víš°ŸdÉ1­]»6š››‹íɾêë룩©)úûûc÷îÝÑßßÖçÓÕÕ���麥ΠÀd&„Ë…}�[ZZbïÞ½ÑÖÖ6ab¼ÜÞÌ…áòöíÛ£££#""ªªª¢ºº:^ýõ8pà@tttÄ¡C‡ÒÊÞŽŽŽ4XŽoiqàÀ�غukz{÷îÝi`ÚÖÖ–VWGŒ¦]]]±uëÖhii™°~îñnÙ²% psŸÍfóŽ)""›Í¦®ßÑÑ{öìI«»{zz¢­­-]§¿¿?ÚÚÚÒ 9¹¿ªª*/\Š-[¶äµ"J�i×®]yÛMÆ#Ùvooï„û»ººâµ×^KÇ?ÑÚÚšö¨®¯¯�þþþøÅ/~ˆ®®®xøá‡]À”Lh‹‘„œIUnžvvv e#&NP·mÛ¶èèèˆ5kÖÄ¡C‡bhh(úûûã…^ˆúúú Aì�âСC1D���ÅØØXX¶¿èêêÊë]üæ›oÆØØXZ]\¬]Fn°¼k×®ô1¥Ž©««+ –÷ìÙ“®?66{ö쉈ñ=·Zxll,·C‡ÅØØXºÍbÞ¹ÁòæÍ›ãµ×^‹þþþŠG}4ªªªb÷îÝyÕä¹Ûzì±ÇbÏž=ñðÃO8¶$DÎ}ÌÞ½{£¾¾>^{íµ´_õ¡C‡ÒžÕ…_”2!\Nz('íÖ®]›VV L“ŠÛªªª8pà@^[‡L&“n£Xø™»ßbÇT¸Ÿˆâ½�‹­¿}ûöŠûî»/ÚÚÚÒÇd2™4έ†ÎmaÑÚÚš·�ÖÖÖ4POÂåˆHCôbÏ#9ÞÜåmmmi°ÜÕÕ•×f£©©) ö Ç=9Î$$Îmý‘<¦P¼755åí§±±1}Laˆ PJ^¸\ª‡r>Â…AtÄ¡l[[[Ñ>Ì���QUUùaf©ÞÍI…mUUUÑpy*ë÷ôôDWWWTUUMh!‘lcÏž=iÕoÄxåñk¯½V2¬}ýõ×',KžC}}ý„û ïþþþt÷ˆÒ�wîq%Ï/©$®¯¯Ÿ;…Û©ªª*:ƥƥ¥¥%²ÙlôööÆ–-[¢±±1ZZZ¢¹¹9ª««‹VŒ”’W¹\ª‡rD¤U­���‘r&áhnån±7QXõ[,¨N”j—Q¬jºÔú“µÜ(eûöí±lÙ²´mD[[[´µµÅ£�>šö{.lÉQ*\.ÖÂ#YÖÔÔTòr'ù+ÜV©çRê¹VWWGWWW477§Ûiii‰uëÖi‡”-/\.Uõ1‚VUU¥U»¥&Í‹ˆIƒåþþþ }‰' ¥KM„Wª·q±õ“0»T…p¡ÖÖÖèè航ªªØ³gOÚ"cll,úûûÓ}–x>‡³Sá䊹ËJ…ËÅúM'ª««#›ÍÆk¯½»v튪ªªèïï�-[¶˜€²ä…Ëg &“6 ˜V5pÄ-*’I#&o'‘´Ë(bëm\lýÜI÷ ÅöíÛc÷îÝih�ôBîêêŠÖÖÖhllÌ x“È…ûHã©¶ðHö_JR%ž[Ý<Ù9ÊÝWr^úûûc÷îÝyý²×®]mmmÑßßŸŽ¡~Ë@9Òp¹XEq¡¤/qgggZ \Ø›9¢ødwÉ>’�3·_ðÙú'ç¶Þˆ˜|2¿bë'j± ·££#:::âСCQ]]�D pKõ[Nö]¬¥H±~Ò…­D %~noå³�£bí7úûû£­­­dOéŽŽŽ’cPJ.— fs­]»6¶nÝ===E+—ÓV Ih™ŠÛo¿=†††bëÖ­SªÆ-Õ_øl½� ×Oö•[½›<çdYR¹[,0ÎÝþí·ßž÷|K­›û¼‹ÂI`ÜÙÙYt?Û¶mK�+ Š'«ØÎ½¿0ð¯ªªŠˆ˜pN">˜X°T%4@1Âå³…Œ¹°…ÚE|п.Z  IDATöbûöí±e˖ؽ{wlÛ¶-Ö­[===±yóæt�D2AÞ¶mÛòúÿ–:¦rz'Çœô‹¾þúëÓcºþúëchh(~øát[ÕÕÕiËŽ-[¶¤-3n¿ýöزeK^;�bz{{cË–-é8• „ÓÉõ®¿þúضm[Þ~†††b×®]yã}¶sTêþ$TÎ='»wïŽ-[¶ÄÞ½{£ªªJ[  ,‹’”ªú-ÔØØkÖ¬‰×_½äćŠÖÖÖèêêÊkÕ°gÏž¼v‰ŽŽŽhiiIÛ>ìÙ³'"Š‡È“õ6.:WWWGOOO´´´ÄáÇóßŽŽŽ ëg³Ùhkk‹½{÷¦Ál}}}<úè£ÑÔÔˆ_üâÑÓÓ“Cò|>]]]i{ŒÉúIg³ÙÈd2ÑÑÑ‘¸oÞ¼9ÚÚÚJVl—:GÅÚoDŒWI'}–sÏIUUU477G[[Û”';ˆ(.OÅdäEŒ‡›I€›À–ÒÔÔT´çï /¼0aÙÚµkÓJ穬Ÿû¸ä9vuuM¢'½ˆ;::бc­®®.:†mmm“V·¶¶FkkkÚ>c²±:Û9zíµ×&='Éã“ó'P¦kÑlïàbìå{¶êì qüÕÕÕçm_Beà\-0”K¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”M¸ @Ù„Ë”m‘!¦jdd$']gtttÆ÷;::/½ôÒ¤ë¬^½:*++�$€óD¸ LÉÈÈHüëý¯g%<>›ÁÁÁhooŸt�åË—Ç7¾ñ ' à<ј’ÑÑѲƒå«¯¨˜öþj*ËûÏÓÉ“'�$€óHå20%Ë—/�æææèììÌ[~Õ±xÑØ„õÿyíÂX|Ù9„Ë‹#n¿na¼xìÝ ÷�ž©ˆ7~™¿Ïææf' à<.SÖÐБ0��ÅÝ7.:§ ¹”ÍëÄæuùÌ£ïŒÅ·»Ïä-knnN� €óC[  , yUÂoœŽøv÷™}glÖ÷�Ëoœþ`™`àÂ.e»³`àâ"\¦å|Ì‚e€‹�p˜¶ó0 –.NÂeàœÌfÀ,X¸x —�s6³`àâ&\fÄLÌ‚e€‹Ÿp˜130 –. Âe`F�KÀ,X¸t—�7�€Y° pi.³¢œ€Y° pé.³f*³`àÒ´ÈÀÌ8ú_ûãèí7’�¸³³3">˜ïmÿÏ�`àÒ$\f]©€9ùwB° péÐÎA&“‰Å‹—ý¸Å‹G&“™WcU¬E†`àÒ¥rÎAmmmtttˆ)*¬`N–™ëFFFâäÉ“Q[[k0˜3„ËÀyU0 –™ë¢½½=FGGãž{î™w¿Z`î.ç]CCC,_¾<""®¹æœÕÓÓÙl6FGG#"¢»»[¸ Àœ!\.¡2s]ww÷„0###€9Ä~0ÊË0ר\€”ÍfãÈ‘#éí/ŠøõãÀÜ£rfHa°|ÕqWÆ÷¸ÌM>ñÀ9‰ÎÎÎèééI—ݸzAÜqÝ‚øÙ/Ç s’pÎÁÈÈH´··Çàà`ºìÆÕ â®ÌÂ÷o —˜›„ËP ðgíÀܰ|ùò¸çž{¢¶¶vƶY,XþôÚŠ¸ãã 8sžp –an:yòdôôôÌX¸<00ûöí‹“'O¦Ë>_¿0>U[z:ƒÁÁÁhoow2˜„ËP¦���hoo�ÑÑÑtÙق刈ÑÑÑx饗 s‚p&‘ù‡ÿfàwlÕª8qÕªÛÞÑ£Gcß¾}i°üáEw߸0ê––k*DÄ»Íx||E…? f„p¦¨»»;:;;ÓÛ^qoâ¸úŠÒ�mÍâˆû›—Å©‘÷.ŠçP*€r —` ƒåe‹#î¾qò`9Q³8¢f±P€¹E¸ g±ÿþxúé§ÓÛW]Q÷n\‹/Ób€ùK¸ “Èf³qäÈ‘ô¶`Æ — ˆ‘‘‘Ø¿^°|ãêqÇu ËÂe˜`dd$ÚÛÛcpp0]vãêqWf¡Á€÷ — ‡`™sÑÓÓÛ·o/y&“‰êêêÈd2±uëÖÝg}}}ttt¤Ë·lÙ?üp¬]»6"ÆÛ¼tvvFsss´´´8aÀ9.@ާŸ~:/X¾ýº…±yÝÔtuuEWWפ÷'šššâÑG=ç}8p ºººbÍš5鲞žžt_I°1.>|8š››�,àœ — GmmmÞí'޾uË+âê+ôYæìzzz""bëÖ­ÑÚÚZt�ÄÞ½{ãÀ�‘ÍfϹ‚8 ‘3™Ì„ãØ¼ysÞºÕÕÕ±yóæ¼u¦K¸ 92™L477GgggDDüúLÄ·»ÏĽ ‹ÌœÕáÇ#"¢±±1‹®ÓØØCCCÑÙÙ9#ároooú·›HÂåÂùÀ�N0cüÎ 444Ä׿þõX¼xqD|0ÿäø˜Á¡¤¡¡¡èïï�ˆ8kepr Óßߟ†Õ“­344‘f'ár©€ûlr·;•ç�ì˜_„ËPDmmmìØ±#/`þÞógâïÞ38•°ž-ÔM‚Ûúúú¼åýýý±mÛ¶X¶lY¬[·.£¢¢"¶mÛV4ìMöY¸�$”Î ¹[[[£¢¢"ÚÚÚò_QQ‘ïîÝ»cݺu±nݺôJLj믿>–-[–þ2™aKKKTTT¨”€9N[ (! ˜³Ùl:Éß÷{ß�ˆˆOÕú~–|Iïã ·˜¤íJa+‹-[¶ÄÐÐPÔ××Ç}÷Ýã“ðe³ÙèêêŠ^x!ª««ó‘f'ÇQUU•7™_±V¹Ë¶lÙ/¼ðBÚ&›ÍFÜ~ûíñÚk¯åÿîÝ»Ó�º¹¹9ª««£««+:::bll¬h«`î.À$’€¹½½=/`~ùä{qWÆË(˜J+Š¡¡¡Ø¾}{Ú>#™ô¯¿¿? –wíÚ•W]ÜÚÚMMMqøðáhmm�l6›Þ7Ùd~…Án±jædݽ{÷F}}}ôôô¤�tkkk,[¶,úûû£¿¿?]~àÀ�hkk‹ªªªèêêÊÛ^KKKìÝ»7"&†ÛÀÜãS1œEeeå„€ùǃcqFÀL* o{{{c÷îÝîïïï�®®®4X~øá‡Óðµ­­-†††bëÖ­yÁrDDuuu´µµÅ–-[â±Ç+ºÏb�q±Ì…�oòø5kÖDWWW^Utî¿CCC±mÛ¶ˆ™ ìŽŽŽ´*{ºýž€K‡OÄ0IÀ¼ÿþ8räHD|0ßqÝÂX|YEÉÇ~ïy“Îu¹àuuu¥ÅŬY³&:::¢©©)}lÈvtt}LÔæö]Îí…œò&-)еÊ( |“m´µµM““<"Ò@:›Í¦m;Š…ÇÕÕÕ±yóæ8|ø°–0—`Š*++£¥¥%""/`þÙ/ÎĽ ‹ŠÌ}'ß›“Áree¥?ˆ¹Aï®]»Š®³víÚÈd2B×Ü^Íå´‘Hö¹yóætÙÐÐÐY{+î·ªª*ý».v\¹ÛO&è+¶~!•Ë0÷ — L---±xñâxæ™g""â�Óßî.0ÏE þrä½…m-Î&©ž,ŒÍ­".Üg±ök֬ɫD.VÍ|¶ÑÅé×_}²B&ó€ùC¸ Óðû¿ÿûQ[[›¶3xãtÄ¿ûÛ3ñG7.Š«¯(0oذ!¾ò•¯¼9¨TÛ‰©HBÜb=ŽIÅp}}ý„Ç«F. v§ZÍ|¶Ç ¹ �shhhB¸ ÌM LOCCC477§·O�ŽW0ÿì—ú+Ï7Å&Ö›ªä1¹ý” íÝ»7"òÛQL6™_±Ày*ÕÌÅžSîýkÖ¬™ôX“ãTµ óƒpÎACCC|ùË_ŽÅ‹GDįόÌ}'ß38óD©‰õ¦*é³üØc�½¿­­-úûûcÍš5i¸œÛ/9·Oód“ùå[©ÞÌ…Ï©pûÉv“9W6›=§ nàÒ#\€stÍ5×ÄŽ;ò懎¼? `žJ±SÕÔÔUUUÑßßÛ¶mK«‚‡††b÷îݱ{÷èèèH+�‹ÃCCCEû7—Ó›¹ðþÂà9é'ÝÕÕ·ß~{>|8>Û¶m‹mÛ¶¥ë©\€ùA¸ 3 ¶¶6vìØ555é²ï÷¾'`žóÎÖ»ølª««£««+ªªª"›ÍƲeËâúë¯�eË–E[[[TUUÅ¡C‡¢©©iÂ>‹U(çöeŽ(>Á^©ÞÌ“m?b¼Êúᇎªªª8pà@466Fccc<ú裱k×®t=•Ë0?˜ÐfHmmmìܹ3ÚÛÛcpp0""~<¨ÿò\—Édb×®]ç¨f2™èïï�l6›NÞ·uëÖhjjЦ¦¦ ÕÅ���±víÚ¼ÌÕÕÕ±k×® ÕÓI_ðÜã;Û1'÷çÚ‰–––hllL'ï[»vm455�t˜Û„Ë0ƒ*++cÇŽy3s[nÀ{.ª««£µµ5Z[[§µÏ¤Š¸PÒÊ"WZ—³ýþþþxýõ×£ªª*2™Ì„ãLÂeUË0h‹3, ˜Up2—d³Ùhll,~÷÷÷§N%æá2Ì‚ÊÊÊøÂ¾7nL—ÕÖÖ.YIEòáÇcûöíéd~Û·o�믿>""î»ï¾iMj\š´Å€YÔÒÒûØÇbtt4®¼òʉÊÊJÃ%§±±1~øáضm[tttDGGGÞý÷Ýwß„eÀÜ&\€Y–ÉdÒÌûØÇbÇŽ…KR2™_WWWô÷÷GÄx¯è¦¦&Ë0 —`�ŒŒäMî÷ÒK/.ik×®�±I €K›pfÉÀÀ@´··Çèè¨Á`Î1¡ÌÁ2s�Êe˜aÝÝݱÿþ4Xþð¢ˆ_Ÿ1.Ì-*—`uwwGggg^°|oƒïr˜{|Ú€òøã�ÇÁƒÓÛW]Qw߸(jæá2Ì€l6GŽIo_uEEÜ»qa,¾ÌØ07 —àËëk"î¾qa,¾¬¢èú�?þ¸AƒyâÔ©S€9K¸ Ó422ííí188˜.»qõ‚¸+³pÒÇå¶Î€K• ý`Ê –¯º¢Â Á���10ÿ»¿=t㢸úŠ À¼$\†KD励"“™Òº+2u·ÞŸlnŽÃ;wÆ©¾¾Y?ŽÉŽ/Y~¼§×‰¼\{çgâÍÍñ·;ÿ­p9GCCCDD0Ÿ¯`¾·AÀ Àü$\†KÐ+O>ÃÇO½oÉÊQ»iS|hÉ’X²jeüÖž?�Ç>W¼uú´�ã¬Vf2q×¾d JhhhˆÊÊÊÈf³1::¿>30ß•YŸX)``~.Ã%èÕ'ŸŠã==%ï¿|éwcã×î�Ú›nŠË—.�O47Çó=tÁŽ÷ÿjÜâ¤1gd2™Ø±cG´··§ó÷ž?Ÿ¯_Ÿª-=•Á©ÑˆŸ/~}Æå{sÔpñ.ÃôÖéÓqäÁoEíÁÿµ7ÝtAÃe˜kjkkcÇŽñÝï~7N�:ßï}7"¢dÀü—ÿN¼áÌ!Âe˜£Þ:}:NôöÆŠúúX²jeÉõjêê¢vÓMéí·‡‡cà¹îíµ»¢¾>""~õóŸçm7Y>ôê«ñÖéÓqùÒ¥±ú¦›bÉÊeKáã†�ŸˆÁçž‹·NŸŽ%«VÅG®¼2ÞùÕ¯¦Ý{zE}}¬¼>¿Ÿtî>Îvl½ùwâCK–Lx\M]]\ö‘�L—\KV­?‡ï?·ˆˆ�gŸ+ù\._º4ª?úшˆ8ÑÛ[ôŸz¹/ž{®èã–Õ­O—-«[cccEÇ®Øß͉žÞíï}±«­­��;wF{{{ FÄxÀüòÉ÷â®ÌÄ—WÁ20S*++ á2ÌS5uuq×¾Xt¾¾ô¥è{âÉøoßýîŒôjþ�½Ñ›íŒ³Ù Ëÿ¦u{,Yµ*þéî‰Ë—.�p,/f³Ñ›í,ºíº[o-ú¸·NŸŽÿöÝ}Q¹bEÔ·4ljÞÞøÑ}­ewí¦MqÿX2œëôé8úÈ#%�í†/~1>zËÍE�íÈ·þ,®ýì�±¢¾~¸DŒ‡½ŸøÃ?Œk?{ç„í~²¥%NôôÄó}gB˜»lýúøíŽ=±ÿ¶ïÿjÔnÚ4aÃÇŽçMö˜û¸Ü±�ˆ¼±»|éÒØüÀŸ”œ¼±ÔqÍU•••i‹Œ$`þñàXDœ)0'~ï÷~Ï„€iY¾|yÔÖÖ. Âe˜Ã’Êà·‡‡ó–×ÔÕÅoíùó4ôxî¹8õòxø¿lÚËêÖGÝ­·D͆ºø/Û¿<ë“®»ù樻õ–ˆ2‡�K+v#ÆÃÔ_�øyô=ñDÞã>ÙÒõ-Íés<ÑÛ§^î‹•×gbE}}l¼ÿ«Ó9k7mŠÆ?} ÝöÀ³Ï¦“(&ctùÒ¥%�mãý÷§Ïiøøñ8ÑÓoÿ*Vf2±¬n}4þ铎ëoíù󨩫K?ðìsñöðpÔl¨‹Ú›nŠ™LüÖž?�ÿ÷ûã’UÏÉ6†��7_y%Þ>}:Vd2±dåÊ “=&ã÷¡�,I«—ßì{%ÞþÕpú·qùÒ¥yÇ•;±d2æÉqͧI$+++cçÎ�‘ÍfãÈ‘#10¿9z&þ膅±ø²‰ýÝvÛmþÀ%O¸ sTRu1ðì³é¿“€ðò¥Kãíáá8üõ�y“¾˜ÍƵŸ½3nøâ£¦®.þÙ¿Ý~kV�µîÖ[â;W¢kçμ ´¦®.~÷/ÿ"""®ùÌynM]],¿Ù÷JüÍöíi˜ùbv¼¢ùŸ}ñ iZ®�÷µè¶“1Z™É¤•¾½åæ¼c[™É¤ÁòÀsÏÅ‘¿•÷ød| +šsÏ]rÜ?ýá#úe×ÔÕÅowìI«ˆÿóÝÿªèvjêêŠVE7|íþXË-qùÒ¥Q»iSô=ñDœêë‹Ýך÷¼ž衼¿�ÚM›ÒãúÏwÿ«¼à>ó�÷u¼È-7ÇOðÃyu͵´´DD¤sßɱøv÷™¸·aQÑ€.u \z–Õ­¯-ò¿õ·Ü¿Ó±'®½ó31^uûbç_¥�ÍmÓðßùn^x˜øé~˜öä]Ë-±dÕªYN…ÁrDÄ©¾¾xåÉ'#"&„Ä¿‘óü Ã߈ˆ¾'žˆ;;§u,µ›6¥cԛͭÀ=ÞÓ“ö4Nz§Çö~+‹áãÇ'ËÉø–j¥qùÒ¥é¹;ÑÛ[t"ÆS}}ñßùn:.+Kµ¨èí�,'ç=qeý'§<.•+Æû>¿uútÑŠð¾'žˆ�çž‹½½ñÎð¯æåµÙÒÒŸûÜçÒÛoœŽøv÷™øÙ/Çü‡ €9Gå2\‚r«’'óöðpüÃw¾›Ú&ýw‡�ŸÐÊ!×ó}'joºéýÇÜ4«U¨I+Œb’¶ …V¿l¯<ùTÉö ?ýÁã“ÍÍédzS5ðì³±ÿ¶ËÖ¯/¾'Ž¿Ð+êëó*�/_º4·ÉŽíè#�¤•×¹rû#¿øp¶ä¾ûžx"­®^½iSÑã|õɧŠ>ö­Ó§cøøñ÷ÛcLý‹ƒw~5œ>ÇkïüLüô‡�LX§ëß|}Þ_Ÿ¿ù›¿‹/ŽÎ÷¿ÜHf˜k„Ë0%=~_ìü« ¡mÒÇøÄ$¡iDÄð±cñöðp|hÉ’X‘ÉÌj¸\ØúlVf2i ;ôÊ+“®ûæ+¯¤Ï¹o�>]4°]Q_KV­Šeë×�(oÙúõé¿>É¿uút¼Ù÷JÚß8‘TGD\ö‘�Lzì§úú¢¦®.j6Ô•<‡¥üêĉX²reYc2ðìsiX×¾ŸhnŽŸ÷öÆÀ³ÏMúÁ|ÔÐÐû÷ï�ÑÑÑøµl€9H¸ — ç¿ó�xóåâÕ½ùÊ+SšH­TEpá¶VÔ×—]ù[®S/÷Mû±³h.Yµ*~ãŽ;¢fC]¬(Ñz¢ÐeeŒÕÛ¿šª¯¼þƒý4þ:¥í|äÊçåïnøØ±è~ð[ÑðµûãCK–¤=›“�ýT__¼úä“ñêS?š7“ùM¦¡¡!jkk£½½=FGGý‡ €9G¸ — 7_î›´]ç.™œnÂØ÷½Ã'ŽÇà³ÏÅ•õŸŒõ·Ü’w©Iú¦#éé|1xöÙ8ð½Q»iS¬ÞtSÚ$b¼ÿsÍûÍÿeû—‹öežojkkcÇŽfæ$á2ÌSS©FN&ª+·mÅù´dÕªˆÚWf2i°<|üxü�G)èW¯_?á±åTR_ö‘�}|Ò ãG÷µ^”cþÖéÓÑ÷ÄiÏîÚM›bE&µ›nŠ%+WÆåK—ÆæˆGÿà\h10ã߈öööŒÕ«W愆æ—7ûÆ{¯ÈLÞ‡xɪUiî¹´­˜ ¹!ï•õŸœtÝ$ /Ço|öÎô߇¿¾3~úƒ­/ÖëøÍœЫ‹ôdN\¾tiÔÔMþøb÷çí¿®nF+¥§bɪU&xöÙxþ¡‡âÑù1ðÜsﯷ2VN±•È|PYY;vìˆ/ù˱sçNÀœ \†y& Ikêê& /?zóÍ鿟}ö¢{¯<ùdDD¬¾é¦’뵟½sZákRÕ}¢··dk‡Ë—.-\¿uútÚ΢v’c»æ3Ÿ)ºüÄ „Ø-h¹‘kɪUñ»ùñ¹ƒÿ)¾vÿyóÛÿïÿ·ÿÇïÇ _úbéóòÄ“.²*++ãå—_Žööö0 \ò„Ë0Ïü�GIÿ½ñk÷ ?kêêâ7>sGDŒW:_Œ½sÿÇÇŸÇåK—Æoíùó Õ´u·ÞŸøÃ?œÖ¶“6 +ê닎ÏåK—ÆÆnÿìb IDATû¿Z28~ñálDŒWïn~àO&¬wí�Ÿ‰ú–æ¢�=Õ×—†Ó×Þù™¨»õÖ¢ûßüÀŸL‹™V89á‰÷¿˜¨½é¦’UÉëoùàK‰Ü*ìùndd$xà�8xð`¼ôÒKqðàAƒÀ%OÏe˜g†�‹ç¿ó�¸á‹_ŒšººØúýÿ?éìL[_ÔnÚ×¾ßâíááè~ðÁ‹òyœêë‹#ßú³ØxÿW£¦®.þ×ÿÆÐ+¯Ä©—û¢fC]¬Èdâíáá>~<–¬\YÖ¶Ÿ}.�¨î·öüy¼øp6Þ|õÕøÈ•WÆ’U«â“ÍͱdÕÊx{x8­r¾|éÒxëô鈯ÿé‰kïüL¬Èdbë÷ÿCü¼·7Þ:=5Æ+Æs[¨ûÁoÅïþÅ¿�-Yïÿj¬¾©!ž}.†�ËÛÄx÷L†ÿ¹ýµ7ÞÿÕXËÍ1|üD<ÿÐCñbç_Eí¦Mñ¡%KâÓüI¼úäS1ð~Uû‡–,‰k?{g¬x?tþéIÇc¾;zôhìÛ·/oB¿‘‘À%O¸ óÐOðÃxgøWñϾø…¸|éÒ¸áK_š°Î›}¯D÷ƒ^”UˉdB¹äy¬ÈdÒpsøøñ8üõ�qý_*;\î{≸²þ“±þ–[¢¦®.ÿ�?�8†?|$^}òÉøÝ¿ü‹ˆˆ¸²¾> Z#"žè¡x{x8®½ó3qùÒ¥Q›Ó9Ûä±…†�‹¿iÝ›ÿô�X²reÔnÚ”÷øÜcxþ¡‡ftL“Êé¤j»vÓ¦xëôéxþ¡‡ÒãúíŽ=qùÒ¥qígïL¿ˆ˜íãºT=þøãª”˜³„Ëp‰øyOOôfÇÿ=|âÄ9o¯ï‰'bàÙg㣷ܜ\¾=<¯<ñd^PZê8¦²<"¢7Û™®3•åSÝnî󸲾>–½ßCú;¾ôø?råŠñ1;v¬¬ñé~ð[ñóÞcõ¦›ò*Œ�¿Ð¯>õTº½ä9ób6Gy$j7mŠÊ+&ÛdNõõÅ£ÿò¢îÖ[ãÊúOæµý8õr_¼Z¢byøÄ‰ô˜&û;yõÉ§âø =1Rd�Ã_ß½åæX‘Éć–,É›ÐñT__øƒÏçÝ?•ãšoFFFbß¾}ñÒK/ù�s–p.Ç{zÒÉøfÊ[§OÇOðÃøé~xÎÇ1Ùñ½˜Í–µ|*Û­©«KCÌ·NŸŽ�gŸ-Ú&í#†�—È÷=ñDZ]J±ç°¢¾>í›üÖéÓE·‘Û‡ù�_ ŸÓ1ä>vlJã:Ù6Ïöw1�¿›ù¤XŒõ5�]ß{þŒ`Î.—¤îýR¬¨¯��çž‹®óõ¢ëä·¢8Õ´7Þ{o,«[¯<ùdt?ø­³Û‰zœÐ9¢XŒ›7,ˆ[¯Y}'ß3@Ì) p)J' ¼é¦¢ýˆkêêbãý_�ˆñþËSiE1cÇÖ÷rDD¬¾é¦Xù~è\+3™ø§_¸'"Æû/k#qé‰ööö¼`ùË"¾´qaÜzÍBÀœ¤r¸$ý¤³3Ößrs|hÉ’hüÓâT__8×l¨‹š÷û/¿=<GJTÏ–;ÿ*j7mŠË—.�ßîØ3é±u?ø “y‰+ÕãîÅâË*Š>fttT?f.yÂeà’ôÖéÓñ7­ÛÓö5u„¶‰½½ñü·:ï•ÁÃÇŽÅß´n�†¯}-–Õ­/ylÝ~«ì‰¹¸LÖc2ƒƒƒÑÞÞn¸¤ —�KÖ©¾¾øÑ}­±dÕªX™ÉDåŠ1râDïé¹ Áí©¾¾xü¦®.V\Ÿ‰Ë>²$"Æ{?¿ùÊ+BåKÜÈÈHìÛ·/¯úøÃ‹"î¾qaÔ-/ÞqªT3pá-[l `:„ËÀ%oøØ±è»HÃÚS}}z*Ï1Óiƒqõqûu ãÅcïD¸ˆ,¾¬"þy­9`:„Ë0EÓmƒ‘ؼnAl^g.]æá2œÅtÚ`À\'\€IL· ÌuÂe(á\Û`À\&\€Ú`ÀÙ — ÇÉ“'ã�ÐÎB¸ 9º»»ó‚å«–FÜÛp™�€~Û 92™L,^¼8½ýÆéˆï=&Fß38�C¸ ðÿ·w÷ÑQÖ÷Þï?ˆBfHH¨™Ü;à ÔLRºÏ!D U1±õ„¥]w3ˆ¸V%({c·¢ µ•nZ» ÅLö^[ñ),¼!BÙ a�½) �‰I4î&؉BA-ç�ẘÉÌ$™$�§÷k­®5™™ëº~ó�‘&Ÿ|óý~ìv»V¯^­¤¤$ó¾?7^Ò¿~ð�þ÷WÌ—h'>>^«W¯ÖøCó¾3ç¥ýàøüï.Ö¢E‹ô³Ÿý,`LÆ»˘ D¸ @‡œN'c2�p€N0&€`×SºfÑ¢EJNN–ÛíÖùóç%ùÆdÔœþ»L®¨†uxüŸü»>jü»ÎM- Ú.òWL€¡�p€c2^}õU�>žÂ,Âe®²ªª*™]ËçÏŸWSSá2`@#\à*zï½÷´sçN t—¸ êëëåv»uòäIŠ”—èe¡º•'ÅI翦¿|u‰ÂezI¨nåï\/e'×̉×icÅ× 0h.Ð öíÛ§�;wêüùóæ}“⤟¤Þ ¸(ê|—è�Ó§OËívëÓO? ¸ÿžÉ×);yxØã:¤êêj €!¯ý÷ÒÂeº)T·òÿ=L?q×?ŒÖ᱇¢€€�p€µµµéw¿û]ÄÝÊ·%Wí™o) FRÌu@—ˆ€Çã‘ÛíîV·ò̉×).j˜N¶ü�BíÜ–x]§ßSè_—è‚¶¶6ÉãñÜÇ„aÊN®¨ºöMð­¶aºÕ6œ‚<Âe:ª[yL”ôçp9âù³=ÀÐD¸ @mmm*..Ú|/Òne#ÂeB¨ªªRQQ‘NŸ>mÞ÷�륇@·2á2AŠ‹‹µoß¾€ûnIðmÚG·2>„ËøÙ·o_@°ü�륟8¯×­6Beüñw½tàoßHñ^¢´Cç2Ð�šïM¦ÀwqÄȈžç�wJ’vîÜ©óçÏK’vW}«cùV?I½^ÿ0šf$ÂeøikkÓÉ“'îóßÈ*R§OŸÖ§Ÿ~p_RR’¢££LMZ­V>Àtç�wÊétª°°PÕÕÕ’¤¿x¥�ß(;y¸fNä \†$©ªªJ¿ûÝïÌ.½ÞpèÐ!:t(èþgŸ}Vv»½ßÖ"..NgΜáC B‘üÛ¯•+WjçÎ�zï½÷$ùFd¼ûñ·úsãßõ ózÅEQSÀÐE¸ IÒ§Ÿ~Úi°<â;#:=OWžãñxúu¸¼zõjÕ××ó¡™èèènýÛsß}÷ÉétÊív›ÝQsú’~}àkº˜Cá2$ùþÜãñŒÅ¸aäõ=v”$iTL”'�íô<‰“ƪå˯t®ÅTÕtN__øÆ|<))ÉœgÚ_EGG+99™“Ýn×êÕ«Ãv1/™6\Q70‹0´ »téÒ%ÊÉ7sù7¿ùM@Àœ6ûf�›šØ­ó�ø¸AGö|b~�””¤§žzj@Í\€öêë뺘%é;×K?q^¯[mÌ翾¤ÿ:yIû†:½¥ºéïª9í‹·~üãë¾ûî£(À5B¸Œ½0,ìÿVîܹSÿùŸÿpÿ­¶az0%|ó�Ïÿ®w?þ–W á2pm1(¢££õÔSO)))ɼïÈžOtâã†.Ÿƒ`ÀPø·rÑ¢EzòÉ'gÞÿçÆKZ»ïý¹1ôïm?j X®¦øøxŠ\Ct.#¤îv0,Šÿ^†êbþAÒ0Í›ØÅ¼±âkÕžñÝž>}:ßø½Èn·ËétRà"\FX‘̈²ªª*ýîw¿ÓùóçÍû⢤�Ãåˆ÷ý¡�¸üä“O²y(`@c,ŠdDÁ2€¡.99Y¿üå/•’’bÞwæ¼´éзz÷ãouþk~— \è\F§:ë`&X€@�Gn·;¨‹YòÎ�Ë€��Îetª£f‚eæt:Cv1Ÿ9Omƒ�Ëè²PÌþ– XEE…Š‹‹º˜%:—�Ëè²PÌ‚e-==]«W¯ÖäÉ“)`P!\FDBÌËбøøx­\¹R .TTT”’’’d·Û) `@c,º¥­­Mn·[’är¹–€!†p1Æb"F¸ ˆá2 b×S‚�áôéÓÚ¹s§NŸ>M1ô‰øøxÝwß}Š��§€py ðx<:tè…Чìv»î¼óN ‹1P´µµQülú :— ¸Ó§×Äx ׯ™±ñ:Ã( ÐáòtÃ…‹²´¶Rׄ×j¥ c1£sÀ óíðá:%I:7š®Ûž:g¹RÃêêj½÷Þ{e€›>}ºâuè!ÂeƒÂÅ‘#Õ3Z­V«Zbc)ÈURUU¥ªª* 1ÞǧžzŠBz„pÀ€öíðájLLÔ_n¤@}úé§Ðc„ˬSÿ#Q§¾û]ýýúàm¢ÇØ4ü†‘ŠIœH¡€ËN+£€^C¸ `Àùvøp�LJÒÙ±�3c­7Ž×�“Óc›¨‘Fcí.zá2€åÛáÃUý½Éú[t´y߈Q1rdÌSŒ�.e€k…pÀ€*X¶Mù¿4ñÿ¼—â\c„ËŒÆË“fÌÕ�ŽT Ю£‚“IIj‰½2G™` o.è÷ÎGE©)áFó뤔L‚e€>F¸  ßûßö$óöûÙ�?¤(}Œp@¿Öj±¨Õj5¿žø�lÞÐ.è×¾ô‡ñÝIN�´ÄR€~€p@¿Öj±˜·‡Ð.è·Zbcõ÷믗$�C×2@?B¸  ßj‹Š2oÇÙo¦ ýá2€~ë|ô•p9&ñ& Ð�.è·¾>ܼ=|ÄH Ð�\O 0Ô××kß¾};v¬~üãS ‡—ô[ç¬VóvŒm"ôȾ}ûtèÐ!IRSS“\.Ez€±šššÌÛ‡’Ûí¦(@.`H"`z†pC3Ð}„Ër¢ÇØÌÛÌ@÷.`È™ð�Ùúî$§ù539Âe IŽŒyÌ@.`È"`º�pC3Ð=„Ëò˜�È."`"u=%€¾s¢ú¸ÚZ¿ ûøX›]cíWåš‘œ»©¡^M�õжŒÖ¸É·˜÷W­�$MIM¿¦ëió¶èDÍÇA뀞rdÌ“$ýµÖ#É0K’Ëå¢8@;„ËЇ6þ|±šê;}Þì…�hÎCO)ÚÓãk¾^𜪎VhÉ3ÊH\Ô¥cöoÑÞâ­šóÐJ3Ì=Q}\ë–ÍS”e´^}ÿÓk¼ž­ÚñÚzÍÈ^¤‡Ÿ-àƒ W0]C¸ }¤ÍÛbËvÇÔ�Áq›·Eõ5kOñ]Ò%=¸üÅ_·ªÝÆ'ª?–$�›<5Ä}·ôÊzüÏÝùzŽG| D‚€èá2ô#œ•¤Šö…}Þží[ôÆ+ÏioñVÍ^øH�Æd¡l”etDç¹_ ’§¤¦kÕÆwzm=‘„Ô9KVjöÂG—\UÌ@ÇØÐúˆ1¯8¹“âÙ‹1owe„FGºÓm\yÄ·Îö�ôØD»¦¤¥÷0\î^÷ó¸É·hJZz¯Œ €Ž°É�ËÐG*»1¢­µÅ¼½ãµõ’¤»ï_²¶y[´÷Í­’¤9­¼rÍ#WFb´y[´ãµßèDÍq�sÜ¢äÔéJ»#;èšWFPÀÆõýÏo\ûàîbU­P[k‹ÆÚìJNMWƽ‹ÂžÛÑQy¤BwoWSc½Æ9nÑÝ —…×'ª�ëè‡¥Š·ÙÎ鿞6o‹Ž|Xª£–ª­µEi·gkFö°atå‘ ý°T'jŽk¬Í®».ոɷ¨òH…ª<Jv¦kJZú ÿLž>}ZÅÅÅòx<ü ø¡ƒ�púH}M׺v�@Øÿ¹'ª�«d[èp×÷øÇ*Ù¶^vÇÔ€Ç�k65ÔëŸîÿGµy[Ìkì)Þ¢Ù ÑË_:—8£Ù¸~”etPx½ñ_›ç5”ïÚ®½Å[µjãÛoÕQ_@36Ñ®u�Ï x­ÆšÖºÿP£#”jÇkëu÷Â¥’™¯Çx½©·gm”Xy¤BG>Ü­Uß XW›·EÿeqÀu�õ.y¦@'ª�koñV-yfhlèñx–�0˜�`ŒÅ€>ÐÔPo°u.·y[ôÆ+ÏIò�Ï0ºx�À7ÜH�Ê3’}Çù:…îÞ®»ï_ªÍ¥U*<بžðÊ{Š·­F í.ûw@û3‚åžxA…Ux°QkÝ”Ý1ÕH_î.n¿ž7^yNç¾jÑã¿*TáÁFm.­2_[ù®í�¾6ã<£FÇhÝãó•ìL×Z÷Ux°Q«6¾£(ËhU©xmmÞ­{|¾*�T(95]ÿúÖ¬wÛKËuôÃÒNߣÁ¤­­�ÿ81$Œ£ÛĈ�cDˆÎeèF*…î\nó¶¨ÒsH;¶­7Ÿ›ã×!|eTEèÐ3ÔÈ ÿ`uÎC+•³äÊùf/zD'ª?ÖÁÝÛU¾k»9¢ÍÛòZ¡f%Ÿ¨>®6o‹’SÓæD�›|‹r–¬ÔÆŸ/ÖÑKõàòƒÖ“윮'^v›_G[c4%5]UG+6>”ü;¾ƒ×Sy¤BKž)—1%-]ã&ßbnJh(yÍWÛäÔt=½é�€õ>½é­\ð³û9ҙЃARJ¦ìÎò+ÐÌÀ„ËÐ*ý‚ÎÅ3l>7Ê2Z.1`æo¨QþÌnã´ÀQ’o³ˆ´;²ÌyÇí¯o³Œ³ÕÍl¨:Z¡ò]ÛÞ´;²ƒFR뉲ŒÖÃ!ÆNó¥ý¯Øñ}KP=ÃÍv>ݸbSC½öûfR‡ºv´5Æ ¤;ÛpÀÐÓ溺:}ñÅ�>oüøñš0aÂU[‡ÇãQKK‹bbbät:#>þÀ�!×ÙÓó^«uu„ËÐÚwã¶o³kl¢]i·g)íŽì MíªÂŒ½�°Æ5g/\òšÑ–àÍîÂm:ª›yÜä[”z{–Ž~Xªm/-×Þâ­º{áR3ìm¿!ž±ž´Û³Cn´g<îÿÚýCäPõõÚÚ¼-f²±†#ì–$ÙSƒj{åõLUÕÑŠ°>€¡­¯f·Û­µk×vùù999Z¾|¹233{uyyy:pà€fΜ©²²²ˆ�7ÖóüóÏkÍš5½vÞkµÎ¡Žpú€†>þ«B¥Ý‘ѱÆ8‰(Ëè�ÁhØÖã»?ÜõšÚu÷Jþ!ò-A×oßÍ,IO¼ìVɶõÚS¼E'ª�kÛK˵ãµõzà‰‚®kt?§Ý‘Õa�Úo$î¾p¯Í¨‡Ý15辎jeôÇT>°BH#2JJJTRR¢ÂÂBFx ×.À5ÖÙ¼å®îX#üõEý»wÃuê†:.äf~aº™ 9KVjöÂ¥ÚS¼U{Š·¨©¡^¾X<ñ‚9‹Ù–s¨Î`ÿ‘þë øv¶¹a¨ùÐm­_uX ÿ×>ç-èºþ0oذ¡ÃQn·[EEE’¤Å‹Ëétöʨ‰«ÉX_YçÌ™3%‰‘í.º,Å•kÞ>æ.êÒ16§S ΔˆŽé•oð²³5*áF�;õ¥jvïz<ÎáPòüùŠ›ìPœÃ¡S�>{�$ux\bILÔ¤{fK’NyŽ©ÑãáC „ÿãŽÂÍÎŽʶy[t´¼TRè‘áøgœ7Ô8 ß¹‚CáÊ£jkýJSœÓm�Q´5Æ ™_/xNwo׎Âߘár¸YÎí×Û>Ø 5¤£�ÚX›ï˜à0Üx}í•ïÚ®6oKØîpøù£�f§ÓÙḋÌÌL9�N­X±B’”ŸŸ/·ÛݯkšŸŸß¯ÖÓ£9£ë( «ns¹ÌÿM[¶¬KÇÜètšÇô¶‘V«R\¹!Ï}SÖ=ºÍåÒMY÷=çpè® ¿•#;Kq‡$)ÁéÔ¯·ÃãúŠÍéÔ�þ°5è~KB‚YÛûy×.¿g‰‰š�¿A6Þ/`È Õ! c|E¨ã÷oõ›·<B’ù¸?#L�·ÙÍQ¡ÆIH¡;z×-›§�O»‚BìhkŒ~¶À¼®æV†yªF¡Æ_t¥›9Ôzýr㹡Âå6o‹v¼¶>è5@GóôÝIW¾Ï;tèP¿ pýƒî;vð†¡W.ºåæóû< »wË–n…Ö·-vi¤Õ*Iª--Õ1w‘޹‹t¶¶¶ÿ}ƒš�­»ó7˜!8®8‡Csßx] ËÔ{ãŒÑ†½—ÇPH¾6Two”e´Þxå¹€ãNT׎ÂßH’~¦Àïþã$Ât3#)ö¾ü ÑP3¢CmºFSƒÎÓ•nfC¸Í Ón÷èwoWù®íÏ_÷øü+¯‘ÍüDòý{?˜cccÍÑÍÍÍæý�G³fÍÒ¬Y³äéà¯!óòò4kÖ,åååux�’’Íš5KÆ Ó˜1c4wî\s$G$:º^ss³ Ìë 6L©©©]«¬¬LsçÎÕĉ5lØ0Íš5K+V¬P]]]Èç5jÿ~ë4î/**Òܹs5fÌó¼5¥¨¨H³fÍ X�ñZÜnw—jß‹è¶é«Vi×#�è‚×Û'×·$ÚÂ>vxã&�°Xt±µ5豋$élM­*^^×åãúBtBBØÇÎÖÖjožïÏÚZO�âÙÏŸ;�:ãЙq“§ªêh…Þxå9ÕW¬hëhù Tm8¡œ‡Vê�Wž `=¾?Ñ~ø™müùb55ÖkJjºšêÍpuÉ3�q¨qFàܾ›9ç¡•Z÷ø<ù`·Ö=>Ïo´ÆW*ßí;ÿƒË_4ŸßYÀzã¾óŸ;ÙÜÐ8OûyÌSÒÒu÷Â¥Ú[¼UÛ^Z®½Å[ÍçÛSm�Q›·…p@Äúó&_|ñEÐ}ÍÍÍæÈ‡ŽP�Ç£èÒ¥KaŸ³xñâ€ðµ¹¹ÙÜHÐívëÝwßUlll—Öîzuuuš;wnPîñxäñxTRR¢üü|íß¿?ìµV¬X4v£¬¬Leeer»ÝÚ¿Ьg£FF@ß~�3gÎÔ¬Y³‚Æg´?oû5577kîܹa�ËÏÏ×Ì™3UVVÖaíû �Ë€n³$Útknn¿\Û™š5z<:SSÓÁsª»u\qÁëU£Ç£F�G­ | `€0ÂP©ûc1rZ©äÔtµy[´§x‹J¶­W¼-I/¸÷™ÏñE�îÝäÔt¥Ý‘­%Ïè‹O�«dÛz•ïÚ®x›]�ÿªP÷. ¸N¨¸2L—ð”´t­uÿQɩ骮sÞÍyh¥~¶ ähèªþØÁìñxÌ®Üöio8pà€Ün·bb´Z IDATbTXX¨£G�jÿþýš3gޤ+�Â=µxñby<ÅÄÄhÆ Ú¿е<OØ™ÍP~~¾Æ�¯ÂÂBíß¿?àX#ì�TAA�ÊÊÊ”’’b®«°°P)))æšBuûËsæÌÑ»ï¾k;~üxy<ôÛÏ:�Ë€ˆ�­©Õ –Q²ØlºyÁ|�,/ï•ÍäâÝ0j”$éÜ—_^õÀ´µ±gݾ —¿Iøúܹ.‡Ñ#­VÅÞt“ùu$Çö„%1Q£n¼±G×4^¯$�:v¬×Öæÿ¾KRógŸu¹Þ¿ž‘×ßëâ_“kõpíLIKWáÁÆ�#Ú£§7½c†´ã&O5CØÙ‹17Í3ŒM´\3ãÞEJ»=K'.w=‡ f7—VÝ—³d¥r–¬ ùüq“oÑÓ›Þ‘t% ›¼iaûõtµFkÝ º/Ôëõ÷àò:¦Û˸wQP¨ntrÌÐý©ƒ¹®®N‹/6¿¾Zk0ÂPÿîÜÌÌL¹\.™Ý¸m>ØÙë0‚Øüüü ×‘™™©ÌÌL8p@EEEZ³fMÈ󤤤¨¬¬,h�999Ú±c‡êêêTWW§ &tymÍÍÍÊÍÍ ú%BNNŽœN§¾øâ‹ Y×%%%æë wlff¦ŽõâÏ_½�p±‹çZuxÓ&Ý�¿ARÏÇc¤¸ruÓ=YAc.ZõQQ‘jKKˆ,ÈôR\¹Jqù:¨ÿ=sVÀsN;¦=Ë}¿þŸeûƒ®kwÌ]¤�Üî�Çù³$&êÖŸþTöŒæÜfÃ'o¾¥?ÿÛ¿…¬Ã¤¬,MY0?äìä ^¯ªÞ~[ÇÜWfƒÙœN³¾cýÆZýŸcÜêº·åæ†¬måÛoé“·Þ:Æÿ¼ÿž9K7/˜¯[ss^¯±æÊ·ßéÖûÞQ%é”ǣÛ6‡ UmN§nsåÍ/®Ù]ª#¯¾ª™¿xQ ))aëbs:õýe�½‘ÖeÊüµ½àõê³Ò÷>¡ÞKãëöŸ³W®’çϪIkC£>{¿´Ûõ0x… n»*Ú²k¸·\Ís÷Tå‘ í(\¯hKŒÿUaÐã/�ñ066€îºVsQQ‘8tss³<OÀÈ…”””«.»Ýî�£(òóóURR¢––åçç÷(\îÌš5käñx‚ÆZ´N¨uæåå™p¤á²ñ:Û‹��•ËåÒÚµkÕÜÜp^#L?~|ÈÎöØØX•””hâĉýö3N¸ è–F�GŸ¼õ¶n^0ß�qxÓ¦ˆÎ1ÒjÕÌ_O;¦£,ã˜$K¢MéO¯Ò�)):´n]Ÿ¿æ8‡Cwmø­üµ66êÜ©S3i’FX,ºùþJHuê�+ž §¯Z%Gv–ùµqœ��´Zu›Ë¥§S{.ÏPî í¯{¶¦Vϵ*!%E–D›¦-[&{F†¬~.l`Ùþþï]w×ܾޯû>*!A›/¨Mp:u׆ßê�+ž ˜ÙÙš¾êŸÍ¯/¶¶êbk«,6›ÙYŠ›Üñæ‡Ó{L7ß¿ èý3i’Y—§S‡Öý:l]¦-[¦›ÌY—›ï_ ¸ÉŽˆëÒ¾ÖF´ñ~]�Ï UÑÖÑfgõ‰êã�Û;^[¯Ê#вŒÖì…K)€»sWGnÌœ9S%%%WåuÆÄÄ„ �ccc•““6ïªÌÌLÅÄĨ¥¥E+V¬Ð_|¡ÜÜÜ€Øè^îHNNNØuºd·¯m¸ÏíÏk¬×²;ú,L˜0A)))ý¶{™pÐm.*’=cF·Çc¤=ú¨,×––êO›_5ÃýéUaë8ÒjÕ­¹¹ºy�¯{wÊ‚ù›.Z•öèÏ$ùBåÏ®6¯=Òj5;–Ã~cŸ�mËí_—$³ÞöŒ µ6ž û “›ÌשcÇôQ¡Û¼~œÃ¡ô§Ÿª‹ñ^úw0ïÍ[P3{F†ù~Þ¼YŸ¼ùV@Müß/Gv¶jvïæ è�q“oQrjºªŽVèy×]f¸lÌo޲ŒÖÃÏ0@¯¹ÚsJJJ‡å#:êæí©ÎÎmªmØyyyfðš5k´fÍM˜0Á!aÌNîékˆ4\Ž´6þvÖ!=a„~.³¡ Û.x½:äüM_µ*䈃PÚ‡®/¯ èmmhPÙ3Ϛݛ¾éÄ>{­Éóç›ã<»: •|#1Œ±öWF=L¹d^lmÕ¡—×Í‘nôxtÌ/àNHíù7{–ÄD³«öÔ±c*{æÙ€ë^ðz}¡îÁƒ¾ot³³d ó�àÙšZíYžÀ6z<¡ë�|ƒçp˜£(þ´ùÕ :^ðzuxÓ&�­©õÕ#%ðÜ·åþÔ ¦ÿ×ÒGÚ ^¯ö,Ïëpîñ­?ý©ïóÕØ¨Ï®êŠþÈí6ßÇŽ>s­��Ú³w«ÍÛ¢žÝ'×/ßµÝ÷ÿ–+?yË0'8�š�¿A‡7m6ÿÔßætêûË3;koÞlÎã5s›§ÌŸ§³ÕÕaµªö*…¤Ÿ•¾¯)óçËb³iúª–Å– ÏÞߣ ^¯aù'o½­Ö†µ64¬ñK�'`β}Æ Ý¶Ø6H•¤6¿1ó/ê“7ßÒ×çÎu8á‚׫Û7kÚc¾úݵá·úÓ¦ÍæµãM[ö˜.SóÉ[o_³ þ£)¦¯Z¥#¯¾j©–ÄDM™7O7ß¿ ìñþ¿¸˜¶l™œNÕ—ÔE¯W N§nʺ§Ã ïoÚ¬Ì_¼¨‘V«æ¼þúÓæWuòàAó}œ2žÙY’|ÝÕW«[~Ê‚ùºté’FX­ª//Wå›oÉ>c†FZ­Aï—±.ãóuêØ± ÿ +šêewL•$�›|Ë5¿¾Ñ-e­±‰ö€Ç’SÓ5%•pÀàp­:–'L˜Ði7q¤bcc•““rÓ½p× –†;G(áÂà®æ…ÉÝ9_GëéÉ:��½=·Û­––Íœ9Óçð¦Ma¥IYYJp:ÆÑÇÜEúäÍ·‚î¯//פ¬,�´Z•ùÒ/$ùF\� ô‚׫Ï®ÖÌ_¼(‹Í¦iË–iÚ²eAÏ;uì˜þì7‡ìO›_ÕôUÿ¬‘V«îÎß  ^¯škkÍ`×x}¶T§RRdKuê#÷•ó„ѲgdèÔ±cÚ³<¯Ãõ~òæ[•`ÓÍ æ+ÎáÐÝùB>¯¶´T‡7mºfŸ‹ª·ßÖ¤¬{d±ÙäÈÎ’#;Kgjj4b”Å ÙÏÖÔªòí·5ýrguûùÂ{W¬ÐÌ_¼¨„”³&þ­ûµyl¨ÏŒñøH«5ì cFvojôxÌ_6ë>SS£úòr5z<ª--Õ¤¬¬߯³5µ:ð,6 {Æ&Úõô¦wúìúÆ(Œö!òƒË_äÍ0h ¤Qè{%%%Ú±c‡&L˜ Ï?ÿ<äsüÇ{.´S—7èê|Þ�ÜnÅMvt8¶âåuú¬ô}ݶإ„ËjKKõYéû«¿?m~U’/ta±˜Ùù¯1ÔZ�ûÚBŒèð¸šíZúˆ’çÏ7Rƒˆ¶Ÿ#\³{·.x½Jq¹Ì1! N§oÓ¿òr}TôojmhPÛ©+s‚ýÇ"´64hoÞ M[¶Lc“Î}±µÕ|OB½–Û6édy¹¦Ü¿@ö�³¢ëTå›o…¬­ÿyýkI­Â¹àõj×ÒGôýÇÕ¤,_‡pœßf£†#­VÝ”u�$)!Õ._ðzµgyžÙÙJʘa~¾ÎTûæ·64„ —�÷äLuµù™óÿ|¶66ª¶ô}U…˜‘ÝuÙ›·BÓ_f~Öý¯]ñò:Õ—Ԥ쬠÷«£u€äëJnj¬ï•îßÊ£жŒîrws›·E'j>îô˜ÕKê~×ô‰êãjkýªË¯±ò¨¯SšŽh× Á2"•““£;v¨®®NsçÎÕ† ÌîðææfhÍš5’¤™3gv¹üZvéÒ¥K¼•ýßÎ�;õÞ{ïù~ÈþKƒùsXƒ”1Ò` h–ÄĈF´ïÂêµiµê‹¥×Ç<üÏ2ß<³cî"}Ô…�<úò}éÍÏ0y¾ŸfÞžžû‘/Hþ§?P¼Í׉¼í—Ë6蛽ð=°<¸V//›§ª£züW…J»Ã÷KÍ=Û·è�WžÓœ‡VjÜä©zã•çÔÔP/IжÆhÉ¿ä›Ïm¯|×víxm½ù|É×ýÀ/„<æŸü@M õZµñMIK¸~êíYzâå+ÿ^/žaS”e´^}ÿSí-Þª=Å[ÖõÀ/(ãÞEA×8Q}\;^û�Ž|øËÞÙ Ñœ‡žR´5fÈ~=gÞþýïßáÏžI)™²;Èt@,£»\.—Šüþú5cCÄþ8sù:ÞB@rÁëP�™‘}`öÇÚ^ðz#ªáÜÿç ÍÎß GvvØçØüþTìË0]ïýé}éÍÏ€¡Á/1Ê£çߥK—¤9­ÔŒl_غ§x‹^/¡Su¹“׿sØè&®¯ùX¾XÉÎtÍyh¥’SÓÕæmÑÆŸ/ j%iÛK˵í¥å:çmÑÝ —jÎC+u÷Â¥jj¨×ÆŸ/67î3´y[ÌpxÜä©A×÷_“”OIM×¶—–ëõ‚Õ²;¦¬Ë?7”ïÚ®u�Ïבv+9Õ÷:æ<´Rñ6»öoÑÆẎÀUA°Œžp»Ý*,,TJ»¿ä•|¡ò† ÌÍû#Æb€ãÜ©SJp:5*Á¦úòò °|¤Õªï/{L’otEcÃeHŒ@öDõqݽpiÀÌâ´;²´ñ狵·x«f/|ÄÜ8ϤÛo¦W_ã;בvk­û�!ï+O»tôÃRíxí7�È%ÛÖ«|×v³sÚÿ|i·gkÝãóôÆ+Ï)íö,³SØXs¼ÍÐ=l\ßt…±Ö*Ï!Å'$é_ßúï€k<—{§êk>VSC½ySC½¶½´\’´ä™‚€®æÙ —ê9×]ª/äsÛÏBö¿~Àl壡»©�ãÃmØ'Ž÷0fBG[cÌæöÚ¼_ñá醿ÖzÂ3`0~Ãw”¶àI]?â;K° \A¸ œÖ†}òæ[æ˜ *Bu ·w¾Õ¤†šc8~âr7qZè@ĄĕÙɾcâmv]ºúúñ6»âmv¥Ý‘%É6�±ÿµ:‰o³�Ãð_SÈìQ£Ã®)Ê2úr·õT>D�ˆ��§¾ýúo:w¦A1¶‰G° "\€"Ôx M õA¡­ȆÛLÏèJn϶s{Ooz'â5³ Û_¿³næÀsw`£.2î]r¼"ãt:õé§Ÿª©©‰b`P:yò¤ÎŸ?ß­c –�`„Ë0@áªÿf}þöo‘ä „� 9ÔfzþÝÄm­-!Ïup·o¾r¨ñ•G*Bv<ïÙ¾EGËK5gñJ¿yËÁ!²ÿø‹Îº™ý] ¤¯<>ÖfWU‡5;®7^yNÉÎtÂç.ˆŽŽ–Ë墴֯_¯êêêˆ�#XB»ŽÀÀ`lfw:ÄÆtM õ:¸»X’”óЕ5ôfzÇÍÛG>( :מí[ÔÔP¯x›= \6º˜C…ÛFˆ[y¤"`¤E¨pÛ¸¯+ÝÌí¯Ñþµá´†ûkó¶hÛKy#> RË@x„Ë0øofwÎÛ¢�?_lvÿV©Ðó‹ïR›·Ew/\Ð *°õŸm|p÷ví-Þj>¶·x«Þxå9IÒÃϬaöÂ¥’¤¯­7�ió¶hÇkëµîñù’¤%Ï„Ëæf‚ÊÁ÷…›Íl7Þ#íö,ÅÛìjj¨¨É‘Kµîñù:Q}\vÇTsí ‚e cŒÅ€À3»Œ{iÛKËuäƒÝϹ{áR=¸üEóëpã'ŒÎáœ%+Uy¤B¯¬Öë«ÍÇ£,£õð3A!oÚÙšóÐJíxm}Ð1’ôÀ/t:75Ô«ÍÛb®»ýõïг™Û?Þ~dF´5FO¼\¨——ÍÓ‘vÕ$95]Oüª0 �€® X:G¸ €ÿfv÷.ÒX›]å»¶«©±^c/�¯h·µ~¥9­T´etÀýþ³‹�ãÊwm7Ï?{áÒ°alÎ’•J»#Kå»¶¯)1xsÀ9­ º?íö,MIMom‰ ù\ÃØD»æ<´2äfã&ߢõoý·Êwmב}c>ÂÕº‚`èša—.]ºDú¿�;wê½÷Þ“$%ü¥A‰ ƒžçûiæíé¹/ éZ<ïºK'ª�kÕÆwz˜65ÔëŸü@’Tx°‘Ùs¨è9óöïÿ{ ‚!ÇC¿ÿãžÅбM zÁ2ÐuÌ\€ ÜXˆH#)’S J =‚e 2„ËÐÏ…ÛÌ®;z+¤€Á†`ˆá2ôsW6Àëy lÌIößL†:‚e {ØÐú¹Œ{iJjzØÍî"‘óÐJIt.€�`è>ÂeèçÆ&Ú{%X–Ô£Í`°!Xz†pCNÝíVÛÙFók‚e rÌ\À�C° ôá2†,‚e û—0$,=C¸ €!!::Ú¼M° ôú0´4~nÞž3oÛív è1Âe†€³'+ÍÛßûÞ÷( Ç—äÎ�iÔ…ÖfIRTT”’““)  Ç—äþ¿ ó¶Óé¤ €^A¸ À vîL£þZë1¿ž>}:Eô Âe±ºÿÚeÞNIIa$ ×.0HÕ”¿£¯NÕ™_/Z´ˆ¢z á2ƒÐ—5GÆa,\¸PñññÐk®§ .'�•©ÞóŸæ×Ó§O×�wÞIa½Šp€A⛋SÝí èXNJJ’Ëå¢8€^G¸ À ð×Z�>ÿwéÛ¯ÿfÞ—’’B° ¸j— Î�iÔÙúJ}YsDZ›ûáÈ~€«ŠpÀ€pòXEÀ�÷Í…ó:w¦A’/XöïR6ÄÅÅiÑ¢Er:� pU.ü7',**JwÞy§î»ï>Џ&—ô[£››õUl,…ˆ‹‹Srr²œN§¾÷½ï)::š¢®ÂeýÖøº/ÔÛ¢ #FPŒ>ÔjµèœÕ*IšŸêêê\ÿNænÝ™4ZwÝ=š†Ð/ÎýÉ««í�·#¸ÛCÓŸÈÒ³‹ž¤!ô‹Ÿä¯Ñéãg"nGYˆAw Ü€DpbÁ]ˆAw ÝN ’ÓÇÏèÜÙóºg\ŠâFÄéÞq©4 0ÈÈÜ‹~°B��›­s×DÜvÍ¢ z|ÜlíÙú^Ÿ¼÷ÅÏ/©¢ìý�Ç7[��›­‹Ÿ_²ë¸rU‹~°B?É_£7_/ÓOò×hí¢�®Û”Ž+Wµg[h{lzå—}ÚV}i~Îb=>n¶N;C@Ÿ ¸;À:®\Õ¹³^IÒ©cg"û>ýÓyIÒ=÷¥Üð{W”½¯ù9‹uµ½3àñO®ïÏðø8Ýu÷hëñ£þ¨sg½§™ù�ifþcšþD–ëöᓳ^-þÁ ­=ò·sž®¶zàÁû‡Üwn‚à}ñ=eܹëÁÚáñqºÚÞ©ÚßÔk¼K0òâç—ºƒ‚}P ¡r÷’BƒŸwÝ=ZëÊVix|\ÀãGtPsžÈÒó¯Ìµï¸rÕqû�ðÑ�?êâç—”3++äo VäwµÕ  šïüΤÑqÇp:úÁÝvúz¦î”G&ëÔ±3ª­¨×³‹žtÌ€=ׇ²öìÑà`òˆ;†;˜MÖð”G&Gµý@0™ÎNÙ¹ã‡XÆnðw>T÷±‰àîûäO^I]Ù²Sr¾£ÊòTQþ~@f¬a2>ÝÊ |rÖ«�~וÉ:"~¸î—¢ïý×ï„d‡šÑÌûž>~&`a´ÓÇ»‚�—ž¢w ·þm‚Á·ÝÖµMðßÍ¿í:®\Õ�ŠCêh¿ª«W:5üŽ8Íœó˜kÆêÅÏ/©¶¢^W¯t•Š~Gœ˜|H ô“³^uvtêÓë¥:Û;öéâç—ôeë%Ý™4:$ÞqåªÕNçþt^÷Ü—âøÁïe^ûÔ±3:}üŒ.~~I÷Þ—ªGfMëQ®ùÎï¹/ÅjŸOþäÕ]w�Vά,ÇÀ½ùµlïº{´*Ë?Ð�ŠCzöÅ'C†&KõÞûRï¸rU¯-ÞèX¯wÄÃõú®•V@ð“³^ý$¿{á¶‹Ÿ_ÒOò×è{�|G«¶.—$ëï;kK¬¿Û™—üz�FÜ1<`{û>W– =ÛÞSÇ•«ϯ,ÿ `ŸŒ¯—«¢ü}Çvÿàýzµd™õú›ÿy»•É,Io¾^&Iú·?ü«õï�üQ VäkÖÜÇ­íŽÖ׎uå ¿™z½Áïa¼2o­:®\Õ¿ýá_µvñÆ€ú¾µª×þŠz­Ûµ2ê¯ùÎ/¶^ÒsÓÿ! }*Ë?PÁŠ|M"°ÌDEùûª,ÿ@3ós îº}Þ=[ßs\lîÍ×Ëôòφ¼b ª { Ý»îmeTv\¹ªÊòB¶ÿÔa�°Ž+WõÜôЩcgôÀäûUòëuzÿì>í¬-±^Ë'Íû¬+[eÕ¡�™ÿ˜Ö•­²2…M€Ød€šíMMÛï=ò­+[¥ue«tï¸Ô�í�7_/Ó›¯—Éï÷kÁŠ|í¬-Qɯ×YûôÚâ�AÍMÿôKU”¿¯;“FëåŸ-Tɯ×igm‰^þÙB ��Ó©cgÚdÁŠ|ÍÌLRW¬Ù'`µž³B�Ö×ÚÅuñóKzöÅ'µ³¶DïŸÝ§•%ËtÏ}):uìŒ6ÿóö�ï¨ãÊUÝu÷h­˜·V?ûR+K–igm‰V–,Óðø8�;ëUíoê{ü�W– GfMSɯשä×ë”3+KW®jó?ý2 ø,ÉÊ´¾'(°o.Ÿw϶÷¬öyÿì>ýÛþUϾø¤$©4(È €ØFpw9ÕÐ�~=ˆZ[,üä¬× †Ú3AÒï=òý¼|••Õy×Ý£µjërëÖ|4õq¿lm“ÔU?wüõ¬aû>Ý3.%`{«>ïõò¦„Aðöf_+Ë?Ððø8­+[¥Ys×]w�Ö½ãRµjërÝ™4Ú*‰ u”kS¯áñqÚú›ušþD–î—ª»î­éOdiéÏZÛö <¸ONõ„;®\µ·/ÿla@]ã)9“µòzÖòÑÚãúÄ–l>ßÅÏ/éžômýÍÏ5%gòõ2“•s=óõj{g�¾s©+@ýü+suï¸TÝ;.UK__¨&wíop°Ø”¾pZέ~ò©ëÏ™™ÿ¸õøˆ;†ëÙEOê�É÷ˆ}w� nÚ³1§äL¶‚Ÿûm�7³í�I£­ìÔSÇÎèÔ±3g@ƒ™ÅÏŽø£õ˜ÉF•Bõ2ï\úÁ)kØm{S`Ê#“K<»èI+{TêÎ&¶gÞÚu¸NݲY»Ûµ;ZYþ�:®\Õ“ïw,Ep×Ý£­Àêi[Ù¼Öðø8-x%?äy&¨:<>®Gßù�I£Ê'æû°²íÁf§ö4ûëø•¤õ!¥1ž]ô¤V–,³‚Óˆ}ÔÜ@&€`�•ÿ˜v¬+×�Šz+yÎ,ÂeË�=z «öë”G&»Ö{}àÁû¥m��™ìQÌ ø›C× .:mÿÑõ@²)›,8¸jÊ?Hݪ]ûy^íÝÙ¥öÏnß6¸ýºƒ�Ýûj2¡g¹ì“Ù�ÓdzpÍw4+ÿqÇ6v*‡Íwþì¢'ÿî$¶Ò›ì¼à›ÓEIÊ™•¥ÊòtêØ=7ý43ÿ1kÁ6·Åã»î ·lØœ'²´gÛ{:uìŒ.~~IwÝ=ÚÊRo ð�>~V’4å‘︾‡SMU+,í¸rµ»Ì‚- Ô$ƒƒ‹NÛ›m‡ÇÇ9f™:é¸rU;Ö•;–°;íÁËOJZï¯Ù'{�Û)9“#¶Õ�¶×3äï9´±[9„h¾s·ïÌ©¼ƒ 컽‡õýýýÞq©ZW¶J›^ù¥¾l½Ôµ¸ÚÖ÷”óD–¬È�z8Ä‚»Ä­†®ÔUuÊ#“U[Q¯Šò÷õü+s»k®Ú¦VÍÞ0AºÓÙÁ§2[¥ÀröÀŸSÖ°Ûö&Øé¶ðW0³ \Ç•«º3©«Æî=÷¥høíŒÞÇÇÍi'·lU)4hnm“F‡Ýó<,ŽTÁ©üC4ß¹½´FÈ6�\Äw{ŸpõxÇ?x¿v(ÑÑÚãª(ÿ@§�wÕ7>÷§óZ·k%^€›ÁÝâTC×îÙEOª¶¢^ø£rfu—1èéíôý®«D‚=ðg‚˜ÁÁÀÓnA_—Ú¶NÛé�)lwêØ}ÙzI—ž¢{Ç¥j϶÷¬Z¸?/_²ýÑÚ®ÒÁ™À§]JZ„+!qÛmá÷«ãÊU ��ë^,.B9·öŠô��Hp¨v\¹j•´°g w/Zú>öºÌæó^üü’¾l½¤¸Ým6%g²¦äLÖ©cg´vÑ�»¾è�[yÄT nÙ°†Yàëâç—¬€_p0Öd¢^ Z,˨({ßÊ5åìÃÁÁO“5ôu©më´ý�w‡ÏŽÝüO¿Ô¦W~i:MàØ­LAåGྞs[àÍ¡ž°)í`üûŸ¶Eà‚¿£Hå¢ÍÜ5¯wΖðYË?°¾WóÝØ³‡ƒËOt\¹jí·ýóž:vF?É_£¯—‡¼Çøï׬üÇö7‚»Ä5ï “õiÿÚs=€àœ’ÓÝó‹ÿòÜSÇÎhǺ®àÞó¯ä[�›ÌZ§lT§ÅÁÂÕ¶uÚÞBO?Rï÷Í×ËtñóKVù©;`y±54ã×Ô¶¿nwûuïWÀãõ„ï—jÂMÕnÓ?ýR§Ž�Ñðø8-°µUwƲsðÖ­fr¤ï|x|œ*ÊÞøÛ'g½Ö÷¼ôõ…ÖãN5x¥î:ÅV Ûç5û{úø™€à°áôÄ>Ê2 “ .08%g²îL­/[MÛÀ`ܳ/>©£µÔ¹³^-úÁ MŸ•¥;ï­£Ž[‹“=ûâ“‹ˆ™ÀÞ§žóÚñz¹†ÇÇéÙEO,:f¤ºÕ¶uÛþ®»Gkfþcª,ÿ@‹°BϾø¤†ß§ÚßÔ[Ô•[—YÛç<‘¥ÚŠzU– Ût›¦ä|GçΞ×ÑÇuêØÝs_еNþç/þ—î¹/Eßû¯ßѽãR]ë /}}¡VÌ]£=ÛÞÓ9ÏyåÌÊÒÕö«ûµ®lU@‰ §:ÇöÏï–î;7�íëÊuÎs^wÝ=Z?¿d}_/ÿla@{Þs_ІÇÇéj{§Ö.Þ¨{îKÑÕ+�:zฆÇÇ)gVWûÙÚ»w\ª˜|¿N?£Wæ­Uά,MÉùŽ.~~I§Ž�QmE½†ÇÇY¼¸9ÜöÛá#ÕÐ�•ÿ˜•�qÇpý¼|•6½òK�>~FoÚ²4ï¹/%$°+uS+Ê?З­—TQþ¾˜|¿ž•­®«K^·ÌY§¬Öç_™+©+KöÍ×ˬǘ|¿¼’ ÿàýZ°"_{¶½§Šò÷UQþ¾µmɯ×é£Ô¹?�É6AÍ£µÇu´ö¸fÎéÊrv+!1þÁûµ®¬«­Ìsì¯53ÿ±€ý2ß‘SƲýó»Õã ö‰U77EÏ.zRíW²ˆïL­ç_Éù¾FÜ1\ ^™«¯—ì÷ÌüÇôü+sµè+®¿njÀóVn]¦Êò¬65íjÿîŠPB±å6¿ßï§Â[½zµŠ‹‹%ueÆ…E©ì™´Á‹�91ÁË»îÝoA>û>EzŸŽ+W­ Ýh÷铳^]mïŒêó:=¯¿?OÚ'R�ß¾í=÷¥8.Âé»ìÏ €ÞùIþ+©ñàÁƒÊÎÎvÜŽÌÝÕÓ Ýø(ëÄÔ>�¸cx�÷©'ݾxÞ`¶Ï�eâ»ÀàcA5ˆAw Ü€DpbÁ]ˆA·Ó=s±õ’N?CCèW;:£ÚŽànÕþ¦^µ¿©§! *Ê2D!11‘F0àÂÅ&ÉÜ�¬Y³ÔØØ(¯×Kc™™™ÊÌÌtýûm~¿ßO3@l¡,Ä Ê2 GõÙgŸõè9#GŽTFF†âââh@ Š£G�J’¦L™BcƒìÀ�’¤Gy„Æè�è�ý¸Awµ#Gލ¬¬¬WÏ]¸paØú @Ùµk—Üõx<š7o� �þØÒÒBè�è�ý¸AÿyõêÕ«iD£±±QÍÍͽzî7¿ùM¥§§Óˆ´�Y’>ûì3]¾|™ ý ?Òú#ú#n dî¢WFÞ� QÉß»M[ËWºüù×4†ÄÀl˜Ç¸ Ðú#ý ? ?"ÖÜE¯ŒJþ†ÆM¹'ì6guŽà.†ÄÀ<öþoJ’.œù34@è�ôG€þ€þˆ›Á]7ýÀ<ñ¿Ýoý› ?ôGú#@@ÄÍ‚š»ˆZss³UswÔ·5:RY†Ï¾ÒåÏ|’¤´´4jîbÐæ1ÿÏhu^¹¦¯/uH¢†@è�ôG€þÐé�ˆmw5‚»ˆå�™ ?ôGú#@@Ä͆à.¢Fp±>03@ôG€þHè�ý‘þˆ› Á]D�à.n†�™ ?ôGú#@è�ôGÜ,î"jwq³ Ì Ðý ?Òú#@¤?âf@pQ#¸‹›i`f€è�ý‘þÐú#ý±îvš@¬Ì·ÿÍíž'ÏG^Ý1z¸ÆÜ;:ª×ùâ“Kºr骆'Äéö¿¹]ÿñ×ÿ�$ëµçÍ›Gc7ЇÝñ_4æÞQú¿þövú#@è�ôG€þô‚»bÊ‘#GfIú�¿þ‡Î=gýû»yã5æÞQæ6ý¡ê´ëß�=ª´´4M�:•Fú¹?^ø¿ÐÉÏÒú#pS8pàý`|Ì¢ Ül|_^é“m L¼zåZÄmâââhL€þÐé�ýBæ.€˜2uêT]»vM����{<}üñǽzÍoûÛ!5¡GŽIÝ$ Šþ(I—/_¦?ôG×=òÈ#6lý`|Á]19avÒÛÁ9==]¹¹¹4,pfú#@@è�ÀÀ£,Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1èvšÀͦåÌŸuù3_Øm:¯ü; Ðú#ý ?ôGú#bÁ]7�Î+ÿÎà ÐÐú#ú#nz”epSÈÌÌ”ç ?ôGôG€þ –Ûü~¿Ÿf@4ª««USS#IJÿ^ªÆM¹'ìög�ž“ç#¯$iÆŒÊÍÍ¥ѯ:;;ÕÒÒÒ£ç$''+..ŽÆè�ý‘þÐú#ý1‡² nqqqJOO§!ú#ú#@@Ä-�² ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ º�&z®±±QŸ}ö ô³aÆ)33S#Gޤ?C´?^¾|Y���ºví�ô³‘#G*##CqqqÖc���jjjÒåË—i `ç¦---jjj¢‘€�‘‘¡äädâ:‚»@9rDeee40@<�þþïÿžþ ÑþøÎ;ïp2  … *33Óúwcc#c!0æ¦7näB'0@ª««µy󿀋�·2Ê2=DV0°Â�è�Àà÷G»ÀÀjiia,QgggÈc�‡À.0Èãá­ŒÌ]àÜûÍJKºƒ†úÁ'[é�@ŒõÇÇ&&ÑX@?hn½¢OþÜÁX ’ËíÕ±�Û¢Úö#þFßKE£ýà£æ6}ÕñW"Á]à¤%Ý¡Ç'}‹†úAOƒ»ôG`ðû#}è/ŸEÜe,úÇÇ­W¢îþß#þ†~ô“æÖ+wP–bÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆAw Ü€DpbÁ]ˆA·Ó¢Q]ß ¦æ�o½¬ó_´I’¦MJWFZ²r³&Ð@À x­´J’”8"N‹žÉéÑs½­mzû·G$Iߟ˜®¬Ié7´/ö×ûÑŸªÔ¤Q|A¸iõ¶ÿ”WÖ…?_ÖØoŽT~îC4d¿o}ñûp\áfàkïTu}£šš/èTs‹$)eÌ(¥$�Œê˜¾Ñ¹Zý �~Ò#Izµ ï¦×²Ù±ms§e*#}, †4‚»"N‚wב·µÍq€—¤Ô¤QÚ±j>“ä^Ø]sD¾ö«ZüÌt=VY×`M@gdeöh’^²·V[÷ÕJ’޽½ê†÷åü—µvG÷I3Á]ÜÌìÇ{b|œ<•ë”Õoþ¡“M›˜ÞçÁ]_{§ 7½£Ò¢ù7E›ö}µ �ù8®pKóµwjë¾Zëø ä±þ/#-Y¯ä)/{B¿ÌÕ�ðXH†Jp××Þ©×J«”›5¡W}Ú<¿do­kÛ®ÝQ¥¬IéZÿòÓaƒ¼[÷ÖêûÓcPP–€ë@÷è ëµvG•ØŸ–¬Ü¬L½Z�§iÓ•2f¤¤®+��¾°^åÕ‡i¸xô…õúqñ¯äk¿Fc WÏîÎÖ­®oèÑsk5ZýšI(pcãeÁš�ƒºõ'}ÝqM[÷ˆ:¼ª®Áºhcè�ªºUÕ5¸fKõ·C'<òµwòEÀMÄ×Þ©Gn°~ßs³2µaélÇŒÛòêÃÖœ°do­¾n¿ÖçwrL›”®W5tÊ1œÿâò �}%{÷œï¾Uô\È]8¾öN•ì­Õk¥Uòµwê©ü…<•ëBöü0XÈÜbÙÆ}Ö5gÆT½·a‘kfßâg¦«tU÷ÄaÁ g/·’Äø8+˜ämmsÌ&pReËò¬`p³)X³“+ ÏPÂcÝå—›•U@51>NoÙ’n“"Ÿ÷vµï„ˆóáŶ…‹�äîK =”eÀ^·³'…ò_]�§’½û•5)ݺblg^œ-5i”æÌ˜ªE³sÕG_X/IZÿòÓJI¥ÂM飯¾ÁºŠ�š4Jë_~ÚÊ>lò\ÐkoU˜3Ò’µ~éì�råÕ‡õöo�è�o'kã²ÙV1}óÚ‰ñqZüLNÄ}ûÑŸêº0β�ûtúã®,è·†<&Ioÿöˆµò¬Ù&R»e¤%kñ3Ó5gÆTÚ[\FúX¥Œ©ó_\VÍ¡Fm\6;ìöö¾1gÆC®“ÝŸ–VëÐIOŸwÑôÓƒûA“ç‚ 7¿c=^£Ÿ–V”5ÉËž WœkÝa°»æˆ^+í®ž§¼¬ Z¿ôi×É{ý �¶î« ¸½=1>NÓ&¥kÑìÊÍ Ì ßt}Ý~M‡Nzú¤<Ãîš#·Š†+Mÿ8ÿÅeÇþÖÔÜ¢Ó·(eÌ �æŸ IDAT(ÇÛs}í�zªp›$Yc¡Óï�¹+'x1™ú½]s$`\6}Ò­ß÷éå›öYõ£] ÕÒ¿¾þž‘¹z2V­ùi%ÄÇé§¥Õu¬ƒ«Šf<Šf¬±�7ážë4'´�oUu óÚhçâÌ)‡¦’½û­ÿß°tv�æ…‹fçhë¾®sšpc’Û1ìöûí6W‹f 3ë¶„ ¢š$£®§ÝÇ3ŽØ·)Ü´O‰ñq®Ç8ç¿h‹¸MjÒ(mXú´|í×4ÍÖ6�¾°Þq?œÚÈ̯ƒÇ̬ësM§ï)Ò˜i?6í¿»æpHù¿Ô¤Qš61]ÿ\�ëšým~#Ü·×î¨Òé�[ÂΜÎÂ}>ô‚»‚?�i´²&¥»žŒ5y.è©üEÀ�¼}X»£J»kŽèßþÇ߇LhÍÀÔÔÜâøÞÖ6ý°p›6,}ZÓ&¦Ô¥²Þ¿¹E�¾°^n/ ØÇó_\Vý �ü~éÁg‹­IHˆaúºãš|í�Z»£JUu *]5ßuß¾?Ñý$ôTsKÈÕÝàǼ­mŽmÓä¹àøyÌgúqñ¯TUßµod_ÞêÁ¥-ßôŽ•9®ïÚ3óÝ&îÁæàã®dï~}¸½0êã.š¾búcÈD³ãšõxyõaÇ,”ªº:á‘§r� 7½²¨”¯½Så5‡ÕÔ|AÇö…¶ÉÞZ-sX|Äœ™ß·À4PZ4_>[¬¯;®©`ÍNM›”Þãße³�©SÝ>·±ÒÞ?œú[jÒÈë}Þãxqã�-3¬©¹Åñd­º¾Aõ'iæ„RWf^Áš�J3Rï®ÑÚÆ>½VZðškwTY�´Ü¬Ly*×Y}ÆS¹Îªçiæ¼á~æÌ˜jý>˜þækï´‚�ŽýÆ´yµ Ïzû\¼©¹…9åaÿ-Îp¸#2{àî”[°ñúñh?Çxwý‹J3²Wóž‚5;ÊH˜cô/Ç߲ưàcÑþyx½¾°éWö1ÅÌã–ozGUu ÖØgï—æüÊþX$ö¹ð²Mû”>s…–oÚ×£² nûa?¯³Ÿ¯9}>3¿.¯9¬B—‹¦æ|×>f¦Œ©Ü¬ ×Û¦{Í{~_{§Öý>™…âÌ÷b~ûþrü-kÝÔÜâz,E:0¿o‘æ¸1w8JÑ7WíÍÊ¢æ$lå‚îÛqL�z³ ›·µMË7ís�˜nXú´V.ÈSFúX%ÆÇ)?÷!ë ª¯½S~¿_Çö)?÷!¥&�²N¶ÍdÛmÒ+u]¹Ý¿½0àʸ}±¸¾,žŸ‘>6 c2ę�Ìg{»½»a‘?3Ýj7ó¹ÌD¢ª®Áõ*:n‘þÕqn¿ÚïT’ÁœP&Œ¦ýÛ •ŸûPÀq÷Þ†EÖI`ý Ï€/(¼_écõVÑsÖ‰ˆ·µM¹Y™ÚÿfW–~b|œ²&¥ëÝõ/Z¯a¿}Ý×Þi}æñiÉÚ¿½0 °”‘>Vûß,´UN'Ü€}Ì0ÇJU]C�úGyu÷-”¯äiã²ÙÖɹïö_?Q4PæoY“Ò•2¦{ì2ãIjÒ(«l‹ä|á§æPàïEð ­=`”wýÒÞoRÆŒtí7æDrë¾ZÇ»SLŸn®ú¹6.›­• òt|O‘ë…e»Hfîeæ“ö²MÍ-J3RÇö)/{‚µ�}< ‚˜,ÊiÓCÅJM¥�Ëf[¯ïmm ~Ùçy–>­·Šž³~L WRaù¦}Öë}¸½P+äYïoæâö9%2Ÿ¯ãZÀyBo˜ñÈ-“S’Þ]ÿbÀ9F^öÛSdÇÑÎ{ìÇÍ¢Ù9!‹qÛÇ0_{gÈùž}þiúUÀ˜²½ÐÚ§òšÃÖØ7>­û=Ƨu�_õäN޼ì ÉBf|�þüzýíäëÁg‹#{ÝöÃ~^éó½·aQ@€×í¼Îï÷kÿöBkÌl®ú¹R“F…¬§aïãö÷0Aòàs…’½Ýã´9&Ìó³&u�#O s÷�¹ãÏ|¾àó‡�Ëf[çáæ¸1wœ8Y'\ñÃúä5Í@“›•éz–Ÿû�5 ¹¦F s¼ÆžId&ØÁ¢™mp©Å™Ÿû�5™ØÅbU}õ=˜÷ Wësñ3Ó­}sËJÆ­Ãxq *™@NnVfÈñÞä¹`M&?3Ýur¼rAžuÜ™“Õ� ž9õS{�ÂS“FYûlÏ^Ü]sØúÝ[¦œÉì`ÁHDRZÔ}’hî8‰†½\ŠÛ¢9éc­NOúžÉêùýÉæ€Çí%�LV�½4Sp1'£Uu ¶ìžÙ®ýÆ^#Ò~aÉnÎŒ‡¢ºKÈØ5YAvÑ_¦Mt.5fÏ \üŒóz f^j¨ùÚ;µhvŽr³2ïšqz}ûóM†eʘ‘ŽsaÓßÌo�Û\|ÎŒ©QÍ)ÝJG`àœ€àWnV¦ã�‰ñqwV:ÝÑÌ~ŒF3†ÙÏ÷¼­móO§~•§Ü¬ aŒ½•Ÿû�Ží)ÒœSCúPSs‹ìMŸ¹¢Wç‚Uu ÖXnÌ´/bîv^——=Áq~îkïÔ«yš61=`¡e·s…`¦Žr¸cÂ-#:øüÁ­„ˆý·ÏmN€Cp@À·ñuûµ~½€âüYáë÷ÚSNW+3Ò"_…íí•í”1#Ã^å5޵2ªýd:Ò¢2&¨uúãÏ8€oqyÙÂfèÕŸè^Ü ß!k×~|ç]¿Ø)X.¤_N¸]úƒý÷Á­/;ý>Ø÷?Ü­àö¿‘%�p‚Ë3,ˆògs,N‹ð›o‚?n%œ˜q$¸¾»kì�¬àqÎü–Ø/üV„é7fñIÖ¢¡nûNpÆî»ØÅ Œ5ÁÙyÑŽ5&;ö½ ‹\ûŒ¯½S·é6Ç¿™ 3¹aæÒ‰ñqŽseû˜)0f>wð… î9Y¿ÍÃOö>Íù�½>p¸}7Ç ý|Ï~Þn ܸl¶ö¿Y¨÷6,ê—±û­¢çôåÁ}¸½Ð ”Úy[Ûôãâ_© ¸g¥Kì}0ÜØ•g3›Ã¶ŸÛÜcÿ›…®ó`ok›.Øî^³?næáÆdû�@nçá¾?ûç;5Àç· T0 R",`ÿû×uwS’FF|�ÞÔ¤r›ˆ»ñ¶¶õxQƒž²œ^+­ ›¡en1çöH]"^+­Ru}£|í�ìݶ¬ §“KŸí‚N¤[Úì¯Ûä¹³‹�o½l}³Jz$LHÉâg¦«ª®Q‡Nv- R²w¿k¶]ð‰ßïO6‡=íwØØoÛ {Ÿ=ÁZ(´º¾ÁÚ¸�6)= û½þ„Ç:Á3'ì‹‘^ï7ÑdP�OK¶êÚ;Iˆ"€<VÕ5D†ªC'=:åi‘¯£S‡Nxtþ‹ËaçqöUëÙ6)Ýaß }Édè…›Sº­��“ЇÁ]·Œîpçfös_„„ûñrúãÏ¢ÃÌùž½\ÖPøm.©PU× ªú+k·¼æ°î1ÌqÒpóF§À¨[vëƒ)QœƒúÚ;uêã:Ñõ:§š[ÂþÆØÛ|„dª”1£¶>>~ZZ¥Ÿ–†i‹�[¬ß@ô=‚»‚~´Gêü—­ßš¼ö2í©¹%$øM6¡Ÿ®rO›”.•v€ýÜ n ZsfLµq©ªk°2ìu3�jíöôdÎ>A�6À4”¹Ý1ôViÑ|=øl±¾î¸¦×J«•›5!ª±#8»¶¯äeOÐîš#ª?᱂»&3(/+S©I£4>-Y§š»N³&¥«ÉsÁú]È�p÷�›¾Ê>K1Licuè¤G[÷Õ*/{^Äœ×J«T²·Öq¼µãv7:6Ù/=™SD2¢›g�ÿ¢wc‚9–¢¹û1ü¹È…¨ÏUz2†9�ï EyÙ”—=A‹gçhú ëõuÇ5mÝW«ÅÏäô¨�ôöÓžÌe 7½ãZV%eÌH%ÄÇõy’‚ý÷Œ¹ôà"¸ @nÖmÝWkÝòÙ“Œ¼¿�üce¤%kÎŒ©µ»zjÚ;a³©{û™z˾Ò*IjÒ(åfeªº¾Q[÷ÕZÁ]{�L·EWz„±_µè>ÑƧ%Ô 'qÄ04DÕW.ÈÓòMïXå¢ù=µ /ê1°'w«L›˜®Ý5G¬€® Ü&Œf�óÓ&¦[Á]Iª²ÝfÛÛ O_dš…S’F)-ï'úºãš¬Ù©?¼½j@n]úBAñN+èb¹Ó&¥w•KKVFúXÕŸð„d=öö®4'¥«æG•ùg~Ã0¸cˆ¹àÖ›2öõnô¼*RpØ>/š3cªkA°Áž?–ìݯêúF=ðí䨲p3ÒÇjÃÒÙ*¸^n©§ ?_wô_F|pù¢„Ã4mRº2ÒÆ*#-ÙÇ×î¨êóà®}æ¼upÜ /{‚u dyÍmŒ2¸kpjjnÑ÷¯ß²©Ô‚ÝùA,+ГÁv &»Á“²“У>œ5AÕõ�jjn±2oì %D›=n;{Àæf8¼Òq�~†>çTža°~óó²'¨`ÍN+Kݪ·k{¯¬IéÚº¯ÖÊöª¾^¶!?è‚�Yp5š;|ÌIäøPÙx4sok›Öú¶X`0UÕ5X�ÝܬL½Uôœã… §;ÞìÛEÊà<ä¸fEwßKIÅXCògLµ~ïÊ«÷¨Ö¸½œ�ÛEýpìó¼Ä‹lÛ�FÄõø³¿~¸Ä"ok›NÜ¢[íÖë—Ýãs´cIOÎm»Ÿ3R:]æ|¤,i7»k[¯?gÆÔ€ÅÙzrÞyÈV–ÉÉ…?_ŽøZ\<,¨ ä¤ÒüÈoÝWÕ­5¾öNýô­jÛ‰XNÈ„Òi�§€ ©í6´¾ÌRˆFSsKØì"s‚Û›‰DoÊ[´[]øv+(Þ©‚â�®AÜ‚'¹YõÕªëV!ΓM°xF„ÛªjugôõD¸Óê¿ýašËBSN¿q�¾°^Ë7í‹Ø'»Ò¢ùV|­´ÚñÄ(5i”5îFê%{÷뇅ÛôZiU�Ê7t­4ži�+¦�ÛOâì¿åÕÝ'ŠÁcŸyN¤EÝ|í�¶Û‚ûf\_üÌtk¶î«åPÄ{PgÃÒÙ®çnc¤é»á28íýÍÎ>Nïv¹]ÛX»£JÅ;­òN\sftÏç 7¿õo~yõaë·qÚÄt×`[¸ùL@æo”õÕíóÃp¯<†Ù_?\ôÐ �ž\¾MÓŸ_õ‚¢áäegŒ­Q�×Ù‚¯Ñ–´¾HsÈîÅ3{ôYªêºÛ=\`×i~oŸƒü>L-Ü&ÏÇc°'ç?,ÜÖuCõa:x? ¸ „ýöäïþhMÄ“·ÂMÝŽ93¦Z“ˆÄø8ëjñîš#®¯Óä¹`©·¯È=�Öî¨r�„˜‰†}8{\ŸÛ›[R3ÒÇZ“¤·{Äu2W£òšÃ*·]­Ì �éw&X”0bXØÚfö ;?}«ÚõØ-¯>l“ùQfƒ˜ãÙíÄ´þ„gPŽa{6Ë‚5î«—ì­½žyYÛoµ½qs2åÌxéö{núlSs‹ëI�¯½S¯•VweVv=awë»&(ûû“+d?é´¯d]¸ùI]Y8ÁYTöú»f»Hãj^VßÕU´Ì\ÏFb�·µÍª�ÌÔÉö¶¶iù¦}ŽÛnzÇõ7Èôëêúưsñ×J«T^s˜Ez‡ˆÄø8+PçkïÔS…Û"5Ë«[eF SiÑ|×mßþíÇßO{ÒŽÓàÄüÆ{[Û\¥¾öNn~'d ËH0ÿtc²‘£Ý§hæÊf,Y¾é�ˆÇ®>ÒÝ.ö‹£ö ˜Áw£šEM%…½p²vG•õ}ôå˜iŸk»-df³êOxÛÁ|w7zþ`îb^” }ƒà.€Ð:{‚ôðµwêÁ­QAñ΀ÁdÓg®°n5sª[¹xvŽ5 =ºpCH–Mý �]¸Áš„¬,È”ϼu_mÈ„¹ª®Áš �OKYíÜ\U­?áÑÖ½�«yoÝ[«‚5;]W¨µ«®oС“�®iG_{§¾û£5!WzëOxôTá6«Ý^¤vÃÐd&iMÍ-Úºï@@ð(sÜy[Ûôè ëCNðÌqm&¶ÑÖU3'–ÞÖ6cªë¬cy ¥&�Ò¢ÙÝÚàÏÜL«²&ãÓ&¦sK+zѧGÌ|ZüL÷XY°&4sÎôIÓw‚ÇZ{æŒ)­0AÙ&ÛÊÝÁ'ÈÓlY¹öç„ë7?,ÜЧM¿1'âÓ&¦÷é¢9©I£‚]ng�¡Â~ÛóOK«ƒ.ÿmᆰfÌœ³do­~X¸M‡Nz¬lÝG_XﺈR×ïÅÓVß|táÇ9¥™‹KbN9ÄÎÉÌïmSs‹üÑ-ß´/à÷½kÑÜ=úÂzkŽfƉH%¶œæ=öÇ¢]�`ñ39Öq¾|Ó;®cXw2ÐC®óÏn Ù§‚â�ÖØåv|¾]sÄqìs“ðù ÖìÔ£/¬×îë¯cö§º¾AÅ;õà�Ö¸ŽÁöĤêC�çu‰ñqÖ˜ÕÔÜòù$Ì5ǧ%÷¨GðÀéÐîš#!smûïÍâg¦[‰kvjù¦}:tÒc}þG_XöNû÷÷Ý­ ÙvwÍ+8œ0b˜už‚¾EÍ]ŽÞ*zN #âlõw‡�8ŽOKÖþí…!Y·¦øüòMû¬ ƒ¹ýãü—­Á-aÄ0mX:»O®ÄöFˆa*Ù[«’½µÊš”°o)cFê­U¡W¾WäéÐ �¾î¸¦e›öiÙ¦}JMò™ \2ÍÂWMÍ-šþ|מÊuJ½^­tÕ|«Fâ ·)1>NiÉû&IïnXD}#„?ÌBæX‰f"•—=A–>­å›ÞQSs‹Òg®PFZ²ããÊ—¤Œ©w׿u–ýâgr´»æ°¾î¸fý–d¤%,ü`Þw m\6[ç¿èʘª?áQúÌÖo”ý3�OKÖ»^äàB¯”Í׃ÏëëŽk®'™û·Z+q¯ÝQ¥µ;ª”5)½«ü�-³}Ñìœ�`©=xd‚�Ó&¦kÿ›…Ž¿ Á'ƒ'ˆ¥ÝÿvËÎ߸l¶¾îèÔîš#ªªkPU]ƒuác úÍÊyªªoЩæmÝW«¼ì \xÁ�•Ÿû�JöÕêTs‹ÊkëÐI�Õgís:û8x¾µM²Óo=§é/¬×©æ«ÏÏÃ%9.–”‘>6dN))d¾+u-ºÆœrhÙ¸l¶2Ò’­ó s¾â&eÌH•=ñ7qüõyXúÌŽc͆¥OG}a.1>Nï®1d 3­ö×�3cªuG‹}þ¹hvŽ¶î« Sì�Â93¦>í³f~<öEê›’¬¶­?á Ä4çvNíbÎmíýó/Ç߲Ƭó_´Œ™NóëñiÉzw}ÏÇL3Ï>ÿÅe•ì­Uu}cÈoLˆaV›ïÄ~Œ˜9È©æÇclÎŒ©:ßzÙ1û×þý™@¾9oµ>³@*‹¡ö2w„�L|¸½0lÝŸ”1#µaéÓ:¾§Èõ‡:?÷!í·½Ž©j/å°{a�¯Rö¥ýÛ ­Ìªà};¶§È1蜑>6àyæ³I]�Ûc{ŠÂ®J¼² /$›Ë~›J~îCúÐöúf!{�ª·rB ç‰Þìî`n¸šk¡Äéúp{¡u¢ØÔÜ¢ú]BfbèÖ'ܤ&� é+özžû·j|ÚØAk«÷6,êZEüúDØüF™Ïl~£˜Œ¢·ìåÜd¤�Õ±=EåBì%KRÆŒTéªùŽ ¿¤&�Réªùw‹8Õ÷µ÷A§lâ¬IéÖk$Œ¶Ÿ¿Uô\@¿1'žöN¥Œ©W òÂÎ n”ý¢+å0Ô9̓͜nÎŒ©òT® Èž3 ¡ÚƒgÇ÷©tÕü€¾Ûu×Üõyøˆ¸°A¬ƒÆaûœr|Z²>ä¹8öûóT®ÓœS]ï Ÿ–¬ÒUóulOQTç–ÎÖ«yJ1,d¬ywý‹!w,F’‘>VÍU?ØÇ¦æ–€×ݰôiך°—ÍÖ»ë_ SÂ=71>.dì;tÒÓçmkæ‚Çö¹öû¾[s][qð˜iŸ_›1sÿöÂ^]\1Áuû�rÁ¿1Çöiã²ÙÖg ®Ám.2oXút@­ns\½UôœÕ¶ ‹ì™¸�y®9oí¾(Sû·Z"×­à6¿ßï§�êêjÕÔÔH’Ò¿—ªqSî »ýÙ£çäùÈ+Iš1c†rssoºvxlb’Ÿô­[æ¾’Ù›1ƒ¯ ›«–ƒaíŽî[`Ì•Uok›`íɾٟ×Óv1mîý†R» ”ťǬÿóÍ7é�ƒÀ~\›ÀO_¾æP\U·?>óÍÞŸþyëÿK ¤±úaÌíI_1Çð@»ö~“!(ŒÞyÿÄgúàd«ã¼š±0vôç|núóëuè¤'bæâ­8§ì ·^Ñ¿üöO’¤oûÛZ¾|yÀß=�6mÚ$Iº÷›#´$÷þ~Û—&Ïùlw‚ÜÈï½ýxˆôûítîÍÖÓq¡7cJ_�}ÁmÛÓö�æ¼®?ÇÌþœÇþíäKê*�î‚uð>ôõoÌ–ê3úäÏ’¤¥K—*=�¹ºDY=ЃCb|Ü�–¤&�êU°©·Ï‹¶M†z»áæt#Çõ@¾æ­´`ÌŠÇ0ýè¿yðÚUºðÅe}bškÖ ¯½S§>î ÐÙ³î˜SÞœú2Ø“ãÁ^> ?Ï{3¦ôÕ8t£mM{öç˜Ù›×^¶qŸn»-|�|{ù–ÄøaÌ † Ê20D•×¶jæ:Ù]sØú�[ô—SV†/™Þ7“ó_´©do­ 7¿ãúcD“œ[Åà#s€!(/+Óºþ»?Z£W ò”’4Rc¿9Rþ|Y»«�X‹çfeF½ “n_L+%i$ sÉŸñ�ªëåmmÓS…Û´ø™�ýf×w|áÏ]‹´™2‹fç�•;DÜ`ÊH«ÒUóU°f§¼­múqñ¯·ËÍÊt]¬ è­’½µÖÅcÎ Þ»™äeOТÙ9Úº¯6`!»`sfLu\ÔCÁ]·¬”1#W †Šü܇4mRº^+­RSs‹u{üø´d¥Œ©ü‘±‹~a¿M|Z²VäQúã&´qÙlåeOÐîšÃ:tÂc-ˆ6>-YiÉš3ã!¾÷!Žà.€[z¢ì¶00T¤&�"3nã²ÙdkÞ"²&¥À�a,¨1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ "¸ 1ˆà.Ä ‚»ƒî@ º�&zïrû_õÿ}ÑNCôG}èÇ1ޱ<ŸÿïΨ·ý÷¿þú!ÐOþý¯ÿ‡Fp@p¸Ç>nÓ±�Ûh€þ@Ò5gi€±¸¥}þ¿¯1P”ezhäÈ‘40€¾õ­oÑ�!ÜÃõQý?�‹‹£Q€A–œœL#lÔ¨Q4Âudî=”™\6¾u IDAT™©––µ´´Ð@?‹‹‹Ó#�6·ùý~?Í€hTWW«¦¦F’”þ½T�›rOØíÏ='ÏG^IÒŒ3”››K#}$bæ®ÏçSSS“$)+++â Ö××K’222”˜˜Ø/;mö)!!!äÊu]]�µ‰‰‰š9s¦]·Lf?£iW°‹Ü­¨¨Ðüùó%I;wîÔ¼yó\·­««ÓÃ?,Iê‹„`ŸÏ§ââbÍ�;7 (»eËëñ]»vY�ïÚµËÚW#%%E���ŽÛ”²²2}õÕWZ²d‰õ˜×ëUvvvŸµ€[KÄÕ­ÿã�7¢Ú6##ã†wÌëõêïþîï´sçÎ�l[ó>Á�¿üòË’¤™3gjóæÍ***Rff¦ëöá‰'žÐ¼y󔚚êøÈÚÐ3wMÒdÀÖÕÕY§nÛöEµ®®N>ŸÏ1ø¹zõj-Y²$à}åóù”�� ŠŠŠˆÛ³/Ám–™™©ƒö[é 7·ˆÁ]SvÉ’%zùå—µeË×னÍëö÷ž0�b§×r ÒÖÕÕ¹þm°êìš}JII ⦦¦†dó@´Âwí%–,Y¢Õ«W«²²R^¯×10.s×ëõê�7Þ°¶ILLÔ¬Y³4wîÜ€íÌ‚h•••’ºÆÅÅÅš9s¦Ub¡²²R)))š7ožµ½ ¤ÞvÛm*..¶þ¼}°²²2UTTÈçó)11QÙÙÙz饗ÛÃç󩬬ÌÊ*–º‚ÏsçÎ h¯×«²²2ë³ï“$K’ŠŠŠBÞ§²²Ròz½’ºÁsçÎu t›÷2¯íõzU\\l=wÞ¼y!mloëÊÊJk?SSS­Ï`ˆó‡±yóf¿$VV–ßï÷û_zé%¿$ÿܹsC¶=xð _’ßé%—,YbýM’?%%ÅúÿììlÿW_}em;sæÌ€mÍ ~¿ßï/**òKò¿ôÒK~¿ßïÏÊÊrÜ~æÌ™ŽÛ þÔÔTÇçfffì“ßï÷ïܹӟ˜˜è¸½$ÿÎ�;­mó›ß8nSTTd½·$BBBÀ{|õÕWþììlkû„„„€çoÞ¼Ùõ;***ò¯^½:ìûÚ™v þ>$ùçÍ›çx=üðê««SVV–>ýôSù|>ùý~íܹSRׂqæóG•••Ú¼y³6oÞlíŸÙ7“%lNqq±222ôé§ŸÊëõÊï÷ëàÁƒJHHЮ]»´k×.®|CXØànp ÝÔÔTeeeÉçóiË–-Û:,ëêêôÆo(!!!d!¶ÌÌLë5Þxã Ç×r*C`ö)ø}$çÚ¶NÛ¿üòËòù|š;w®¶lÙb=ǾOö’ö «W¯x�%K–XmÜ•º‚µæßÁŸÃéó­^½Z���VÀÛ^æaÞ¼yVù†àv7ŸïÓO?Ucc£–,YbíŸ Æ3�ïY³f¼Ovv¶U6"8ˆ `h‰*s×5ò²²€m�‚¨&(ºdÉÇ:¼ÙÙÙJHH�Lt«Ýëõz­ŒáàLÜh·ollT]]�B¥f»¢¢¢€ÀèÁƒÕÐÐà,=þ¼kÛedd„üÍ,RgöÉÔ#–º¯Nm%ɪE,u�ÍûlÙ²%¤²ù›icÃßxã�� îêÕ«uðàA×Ï `hp]PÍô³MgÍš¥””y½^íÚµKóæÍsÌRõz½Ök„ fffZÁN#8c8xŸ‚K?8•^pÛÞ”˜5kVH–¯Ô•�l/ɼŸ^¯7 C·®®Îú·S6±SP;8s×t322ª‹ô: ދʽçÍ›§-[¶èüùózøá‡•��­¢¢"egg[‹ÊÚ\3wÝ©Rhö®=Èh‚“öÌU§ ªœõjÏF Jº•kp ;m®äƒ›ââb}ã߰ʬ^½Z«W¯ÖæÍ›õé§ŸJ - )àlßÞ<6kÖ,×}0û�’’òZnŸÅí‰jllÔܹs­í~øa«æ/€¡Ï5¸ë`•º2?M]{†®S©„p�]{¬ @Úƒ˜ÁÏu[ˆ,Rm[ûö&˜ì–!lÉ’%Z½zµü~¿^zé%«Dƒßï—Ïç³2f{pþ ‘ö)xq;ûcnÁ]§RFbb¢víÚ¥O?ýT/½ô’õ}>üðÃõ† M®ÁÝH�A“eZQQÑ«lX©»DÂÌ™3­ÇÂe£ת•zVÛV \ô,˜ÏçSqq±Š‹‹­ ±©…[WW§-[¶(;;;àõL Üà÷pª lß_§v5Ïqb²¤íÙ½á¾#§÷òù|*++ XÀ.55U[¶l‘×ëµÚ�z»ÀÐçÜ — k˜º´eeeV6¬=Èhþßi±1ó&Èh¯©$ƒ½ôƒä^þÀm{Àt ¤VTTX Š%&&‚�¨uuuŽûks*iá”ÑköÏ-ðlè Vp7Òwd/ÿ`/•1oÞ<Çàmbb¢µÀ\¸ 3€¡Á1¸kƒÁ�Q»ÔÔTeee©±±Ñ1À9kÖ,%$$ÈëõZACÃçóéᇖÏçSVVVTÙ¨‘‚¸Ñno¡öìUó:/¿ü²¤îÀµSÀÖþï'žx"äu�¶µn·¶2ûäô>óçÏ·öË|æó9e,»µKvv¶$)ä; ×f†žÛÿÿöî>*ª;Ï÷ý'�´ŽX%œ}­@´lõ˜„‚éuî‰â™6�̸ ÑèIèhΚé$À�¾­=‰c§[ÏqZˆéž5m‹™u“¥#Ü#FÀ³fº¡ðš Eޱr�öR%Mr½”{[EíâI@ ߯µz¥ªöÓoÿjÓ1¿|VÔ: \YY™Ùú ¼BÔPYY©’’•——«¶¶Vùùùêéé‘ÇãQOO�²²²TSSq̵k×$I%%%ÊÌÌÔæÍ›•ŸŸ³õƒqý¡,¦&…R�Çcö—ÍÏÏ×Ù³gÍ{÷î5�IJJRQQ‘jkkµlÙ2¹Ýns1²ššs[,mmmZ¶l™²²²TYYs‘¹ââb­]»VÕÕÕ× ×æÍ›#*nûŽbmÿNŽ;fnohhPCCƒÍpÀÄeîÔ÷6\qq±222ôÉ'ŸÄ\x-33Seeefx(…‚àþa¥¡¢¢Beeeæbk;wî”dÝÎ` Þ¶±4KJJRCCƒå˜*++#ªˆ¥P_ಲ2UWW›Õ®Ú»w¯ÂöööÊëõšcp»ÝjhhбcÇÔÐÐ`¶g¨2Öãñ(33S�'¢ªvéÒ¥ª¨¨ˆº�Áªlc…ÞÆwRQQ¡šššˆp½¨¨H•••C^lÀ­sÇ5£TvŒ- \.WDÕê@ûe¿ÑÓP‚khhPffæ°Âϑ܇lw�º‘j•¶$ýæ7¿Ñoû[IÒ¼ÿ�©‹f¸ÿ‡'>Ö™ÿ~V’ô—ù—Z±b?qÀ(¹k¼.”””4¬°r¬ƒÝ‘Œi$aëHîc¸òÍ ¿.Ÿ¾Á@ü!Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@º‹)@}}}Ú¿¿.\¸ñyÿ÷ÃqâÄ }ôÑGŸ¥¤¤hÅŠJIIaÒ€a"ÜE”ßýîw:qâĨžóÂ… –áð·¿ýmåææ2éÀ0Ñ–QÇ û|ËyϨìCÕ.02w\»víÓ€þš››U]]m¾¿kÊ]ú³‚§oN¹S‰÷ØõÍ©C+úþòÊWêý, /¯~­?Ôÿ¿úêêWæ¶µk×Rµ Œ•»°”››«µkךú•>lþxXÁ®$}sê]J¼Ç®›?&ØFá.bêð~~>¨ãû[ô啯†|Ž/¯|¥ãû[ôùù ùÁ.pów1 › x v€±C¸‹A�$à%ØÆá.†d8/Á.0öw1dC x v€ñqǵk×®1 ŽææfUWW›ïgÌ´é¡Õ9’D° ŒÂ]ŒˆUÀ+‰`'„»±þo8‚]`lÑs#Ö¿¯�`{„»¸)ý^‚]`|Ж£Âï÷+!!A)))L0w Ñ–âá.Ä¡»˜‚øä÷ûõÅ_0âBJJ =¹e„»qhß¾}zï½÷˜q套^’Ãá`"%´eˆC~¿ŸIw¼^/“À(¢r7ÎMëûBßøú+&À„tuÊ}9u*À Ü�sÿ‹ß/[0ÈD˜�:ÓÓÕý­t&€1@[ˆC„»‡w î@"Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w î@º‹) ÕÕ©SÕ›8CA»]_ßyç û_™2Å|}âÄ }ôÑGL"& ‡Ã¡Õ«W3âá.`P_L›¦ÿ阥 Ý>âs\¸pA.\`21a|ôÑGZ´h‘“ .îô?gÍÒSïa"0)õõõ1 âá.ÀÒ×wÞ©s™êMJŠøÜ~O†ÓïÓTÛÝšjKb¢wÎþK�ú.u1âá.ÀÒ§³fE»S¦'Ê™÷¸Óîcr×îüæT&À¤@¸ ˆÒ™ž®K3SÌ÷ióÿƒÙÿQwMù&€ ‚páêÔ©êþVºùþnÇ|Ý÷¿>ÊÄ0Á|ƒ)„ëJO3_ß9åOäÌ{œI`"ÜDèIL4_Ï[ö$­˜ w¦Þ¤$ýw…:öÜ9åOX< € Œp`ê›6Í|��”Ê„0�± ÀRbúįڭ}cÇ€Ûl‰š—½H÷νÔ¯¹¸p�f¦;†tLã¡}ºÐå׈`bëëëÓ{ï½§””-Z´ˆ ’wa¾H¸Q¹;=ù[z¬çÚO©fÏŽ!í;?'WÏÿd¯ì‰7÷Ö�^óšËŸX?äãjߨ¡ó�~mØõŽùYËûuªÙ³CÙŒ8Üí ôêͪMÃÏáý»u¦µYÏÿt/=ĉßüæ7zï½÷$IgΜ‘ÛífRá.à†¯ï¼Ó|}甩z¬çÚ?�$M³ÍÐ ?õDl;ßå×ùN¿ú‚½:²·N·4«æ�zªtë¨\3%Í1ä ¸/Ыó�~IЍ’=ÝÚ,I7UU<’ñHÒ™Q¸6`|ùý~óõ‰'$‰€îâÓ¹öS’Bå@­f¦9ôÖk/«©nÿM‡»F ;?;wØÇ8œ #>a›ç¦çàF@¼pXÇímêâ€8GÀ $TÄ)£ju° Õ~Ã+hG~M#P^8ìqŽE•¬1žá„Í€Éãĉòx�ïôëÞ¹÷+ïÑ5Z¾:ºï­Uu­qýþç—B ¤5Úg·3ÓÊy¨PEßû›¨Ö áã9×~Jµoü�ZÞ¯3?{ò…W¢ªšköìÐo³®1{ýöOËûuª}ãïÌ1ä=ºFO¾ðŠeë‡Ó-ÍjªÛ§–ãõê ôjfºCO¾ðŠr–š×zò…Wh£(áî4õ] ý¼ÜÞ¨ÜÄ�Ó-ÍæëÁªhí“ÙátK³N·4«/Øk}þÖÐöÛ�0ó|§ß¬ü}ëµ—U³g‡RÒr8ê\û)½YµÉ\Ü,\xkhy¿>tý°àY’vý¨DoVmÒå@¯²*мì\]ôêðþ_jûó+Õ¸1ÞðJäó]~mvWŸ|tJó²s•’æÐ¹öSÚõ·%QÕÊ­ÇCמ™æˆš�[¢ö¼Zª]?*Ñå@¯æeçjšm†íÓ®¿-±œÛíÏ?®ÆCû”’:Ëﮕ¨ñÐ>5Õ틺àæeþûBýé—ùž ^n_TîâŽQQ:ØBbFÀ(I¯yÖü|°ÅÌn´RXuMIºvíšþëÛÿjV7Ú§=¯–êÈþÝzxõ³æçç;ýf ^AkÕÞátK³ZÞ¯“ùP¯TÿÎü¼/ЫmÏ=®sí§ÔxhŸyÆ=HÒ›U›ôÌ‹Uf%®$½¼öÏå÷} –÷ë"îÝêÚÆýùõn¥¤ÎÒÏ3çæ\û)mv7"P—BÕÍ{^-5´ oñÚ�JôÖk/«/Ð;ìÅÞCãÌ{\’ôǯ$*x¸]Q¹ ˆ;F9Ýžª²í÷¿#ûwk×�J´çÕRIÒ¼ì܈àÓ¨¦�ÕÒáŒEøk\sšm†^Øæ1\)Ô¶À¨ 6Ú"„Ž9e^?âüÞЀ‡¾æbmýÚ($ØÍp¶åx}Ôx$é…Ÿz"îÏ8N’ú‚7ªƒ�€¶àjÌÇ´é3´ñõw"îÛ*ï ôjÏOÊ$Ië^¬Šs‚=Që^¬2Cíá.ö:gÞãTðp›£r@²Ó©TW–¦Øl vu«»­MÁÎN&¸ÅŒ0ò\û)mîñ÷]\¸FO•¾b¾og`<è4ÛŒˆ×_Ã+sÃå,)”ß÷Ad˜jÑo7¼š7<85ÎydÿnM·'jùëÍ6ïÑ5Qá­qîÅ…k¢aIºÐºÇð;Ö‚pÆçÅÏü ªÊÖØ6Í6Ãü¬ñÐ>õzåp.TΒ¨kÏLw(%Í¡ ]~zíÀ£‚€Ûá.FÕÿÞpT’t%Сgÿó�‚ÐÝne¹×J’þ)Ù¨�)ÙéÔÕË—£ÆòpU¥R³²ÔÝ֦åeÛ¦ÚíúîΟ)ÙéŒøüHY¹r7nˆyÜ­´`ÕJ}øö�ˆÏÒ\.-¯Üi޽Ëëå!�޼<ù™ˆú½fàèp.´ü•ÿùÙ¹š™î0ÿΨx�u¬qîþU½F Ü?dÈ�ö÷G�¿5oÞ£kÌÅÉjöìÐáý»õðêõ!¯Õxr–XΑU€m5žð0ÛêÞ¬Ž1*ˆš‹éöD]èòºààæðpû"ÜŘ˜j·+wÃu¸¬ü–Žá�§ŸÖ‚'VéHYù°*nÿìûì^òuhzZª‚ÝÝn®“�N-Ú¸AÉNgT¸‹øbKOWî†*Õå“¿è˜,ÂÛ„÷¦ª¡÷Û� d�JU«ª]cŸþÎXTîZõó5¬{)Ôâ fÏ]èòG„¼Eßû�¹_xõ¯Uå¬q�þíÎx¯·~ȶh#„µ »­‰‹y\Nü…»ÿ÷// Îðp{"ÜŘIu¹´à‰Uúð×oß’ëß=gŽ<±*æö‹í¾ˆ†›µx±$)ØÕ¥CëŸÕ•@`HÇÝ ³òò¢*Œ WƒAu·µ™¯1±Í~䥺\LÄ Œ0Òèq;Òãcõ‚mm U¥F¶3…™- föѽ~\xØQ);H¸l´`hy¿N‡÷ïÖ™ÖfÕìÙ¡iÓgD-¦6/F k؆WóZ/îf=ž3­'¢æË–c1ú�ô;º•ü~?Á.€¸EÀ Àí‡pcê�§Ÿ–¿±iBö©ýýë¯ÇÜ6Õn—$uÔ¿ìvÜDsÑç›P­#€Ñ0X9£­ÕñF/ÙÐö…Q׌¥åý:õz5Í6à N�@8j1µ~áòùN¿Žìß­s¾SÚ°ës¿œ%…ÊYR¨_ý¸TMuûÔr¼Þ wcõÎ�ž£èŠáþ�ë`‹Ë�d¾�üz÷M}G·R__?d&´„»Ó4=9=æv^n/„»—|ºÛ9gB´g0¹˜aäÝߪzU U£Ö¾±CRt;#õû>P_ 7ª‚·ö�¿“ZlíÆ1Ñí Â'3‚Ͼ`¯ïÿ¥9†þçÎ{t�šêö™ãO¬ðÔªm‚UÅp¬j^ó˜‹Ëe?T Öãõj<´/ê¸ÆCûÌãb…ÏñÂ~O†î/|†:q‡€€Ûá.ÆÄ¹ÆF»»äX¼xTÚ3LµÛ5û‘‡åÈË3? tvé³¶6uÔ×GíŸå^«„Ô4óý}�<¢TW–$©ÍSúCoa¡¦§Þ£ËÝŸÉWW'[zºæ<òpÄyÒ\YºãúboïV°³3ê8+ÉN§æ­\){ú�1øõñ»‡£*�ÃÍ)(Ð=YYÇ] ÕíõF›ær)Õ•eÞ—qß’ÔímS—×qOÆøcÍmªË¥)6›yMc“å܆_;|îæ?þ¸’ç:‡u¿ƒIv:åÈ[Õªàb»OŸ65 º@Üœ‚9ò›÷u±Ý§Óï¼ñ=se5/³/Ž8~$ó2çúÜ4/7ö‹ý]FïëŠxFü�Mú´©é¦æ;œïô[.6-ï×™ýjO·4ë­×^Ö´é3,Ï}¦µY)iõ{õÖk/뙫$…ÂÑ=?)Ó¹öSr8ªø™„ÝÎÀ*”½wîýJIsèB—?âÜÆù›êöIŠ ‰Ï ÐZ"|ެÛ/ ^ÍÛÿ˜ù‹¿µ¯WSÝ>ýé·ZþÄz�ïò«õx½jö숸7À­AÀ Àí�pcæÄ¶íJ}ëMM±Ùnª=ÂU+õÀÚµf«CªKrhþª•:±m».únôÀ}°ßZ�…7V“7ÂÝÙ�(5+KÝmm¡ .55ê¸T—Ë Ñº½m vvFnªÝ®E~B‡Ÿë�µkÕò‹¿�:.ÙéÔÒ­[e uÃ9òòôÀÚµúoåÿ‡yŸ÷¸\zðzØÿ¾Û<Õ¡p7잌ñGÎK¡rþú¯¢æÖ¸æƒk×êØ¦Ms~m£Ÿ¯Õ9Œû óP 4�ƹ<±J¾ºz�ؾÝòøïîüYT/bã¸Ûÿ‹ù=s5Ôïc¨óììÔ’­¯ i^¬ž½þߥñ}-ÚðØÏÈ•@`DóObõ±£êt×�J4?'W}�Ïu®ý”–¯^¯[¢ü¾,«mï�»P9K µçÕRµ¯×½sšªçBm|ý˱ö_˜MŠK_ضWÛž{\�‡ö™çíÿ�¹�ÛS¥¯Dœ#¼ú×êºý[£'ðpúÿƪÎYR¨å«×ëÈþݪٳà t§Ùf˜ŸKñ¹˜L&¼L~„»3W5oÛ®üoq{ga¡¾óÜs’BÕ‰'««u©Ý§oÚlrä-Öœ‚%;�úîΟéгÿÙ /Û<Õ²¥¥jNA(Ôí¨¯W°«{Àk»»Íà׬šlkSW«×Ü>˜ð@ÑßÔ¤Žºz} Ê–ž®Ö>-[Zšmø¡‚��f`gKO×_üj·y�õïêÓÆFIÒÝs�räå)5+zK IDATKSív-ݺUŸ|R’ô™×«6�”–íRjVdUògCX (<(ìÝYyyZ°j¥léiúîΟŠ§ß“j»¾}@—»»$)昇*<Øõ75éÓë1ðM›M©.—æ<¢)6›œ…úãÉ“aùT»]Eoþ_a}“ëõYÛIIRÒœ9Z°j¥møaÌ Wãy2Žÿðíêözõe0¨{\®!Ï‹ìvÔ×ëRGÇ€ób<{}—i.—ù}u·µéãúwÍçÝ—j×>õ¿MÚ Þ¾àçš—�{SU»ë^¬RÍ;Ôò~½>ùè”ægçêÉ^Ñüœ\½YµIó²s#BÉó�~ÍËÎUÎ’Bå=ºF ¶ªÙ³C§[BÕ¼y�®Ñë×÷kãpÊìµÞÎ@ õàíîÞ;÷~íxû_uxÿn�nmÖ'�Ò×ï5硳׮$õžƒ¾°ãnÓk†½á÷Öì�ºßp ö×ï½ jÛS¥[•óP¡ZŽ×é\{(7‚3îpëð0¹îbLùåojQ{[zºf]òuèHyyD`åolÔÇõïjyåΨðø¤Ç£4—Ë w?®wÐ_ávvê¤Ç#éF¸ÛÕê5?Ì‚'V™ÁîïþóÈûôzåolTñõJæ?{îûúçuë%Ißyîû’B둲òˆ°°ËëÕ‡¿~[¹7hNA�léiJv:uÑçS—×{ýžÜf 8Ô±NµÛ•ó׺ï®.ZÿlÄÜvy½ú¸¾ÞœÛX­-=MWƒA|ò©ˆªàþcväåÉ=8L¨C(ØýðíQ Ø…¾÷z3Ÿ•·8"Ü ¯ò>±ý¿DUIw{½æ_8XYº5´-Ö÷ñic£9/ßyîû–aaÌË?¯[q|¬y¹ñìÅþ.g36‹õÞʽsï�ªä�uþðq…·uˆe~N®6æ¼3¬9J°'{<±¶Ÿïô›�õüœè`بžO¸ /“×7˜ŒµÛ¶ëj0(Izàé§£~U>–ù+W𝛷m³¬DìòzÍ ÇT—kÈç ³ …o—|–ö•@@'«CcMv:#ÂÅPÏÔÆ˜¿Nÿqý»æk£ÿëM�µà‘è¶í–s{Ñç3Çk‘Vþðó_X¶Û8ýöóõÝÃø^’çÎU°+Tüÿ\¿¾ÕØ.ù:,çc¶Ñc¸¾Þ2àô76šÏL޼<³ÃÉêjËïã¢Ï§?üüƒ>s±Ž?Yý�#š[zºù¬X}_¾º:µyª-ÛL£©ñÐ>•,NÓ¶ç�Úv¾3ÔwWÒM‡ð€ÑåÌ{\:çFÏþ'NÈ3ÄÂ0qîbÌí¤ë½T7nÒqÆâR—|öýøÝÁç¬äX³¥§›!_Ç»õ±ÇZÿ®>ù”þ)™Ð5¼ø’öýå sŽÆƒÔ»º ÃCåþ‹šb?Ò¾¯¾º:üOOFÌ‘•«—ƒ–÷e„Öác虉|æ\–÷Þ_xr¬g®»Õz^FÒwZ -'…þb ÿÇ[-Cå“�Nz<#¾0FÕ®ß÷�ZŽ×«/Ыsí§Ôr¼^ÛŸ¾ËW¯�jG¸õx˜|hË€qÞž!ÙéÔƒn÷ -Œðj(í iÙ.�¼>µ¥¦š¯/µÇ5¯ƒöBµ¥§kú=÷(y®SS®÷˜�¬Ž”ñ«ÿF/Ø�Æ{Éס»�s”<×9èü�•Ô¬,M±Ù”<ש»�NÝ=Çi¹ØYx%ì@÷kÌÆ=^ 4åãC[¬gn´5;}à€ÙkØ‘—'G^ž‚�]f«ˆÏNžœ´}v1±ä=ºF§[šÕT·O»6º£¶g?T âïý€‰€ êV¶hðz½êíí�¹=++KIIIã6ŽÄÄD¹†ùçìžžµ]_T¸ÿx�;&IÊÈÈPffæ„'`r!ÜŸ9±m»R¯÷œÍr¯Õ§´!g´tˆ@Æ+[zºxúi9òÇì;.¶aþ/oÉœ,XµR³¯/˜77tNµÛõàëAììÔ‘²rånÜh>ë¶ô49Ó ä, õ–ö76ꤧzÔƒe ¿u/Uéá5ëÕò~½ÎµŸ’ê3œ³¤@÷νŸ € Î*àMIIÑŠ+Æôºeeef:—Ë¥ââb•––Žz0iŒcéÒ¥jhhÖ±^¯WË–-“$=zTùùùæ6ãõæÍ›UQQ1aÇ ˜\w1nŒö ù?-гhãsQ±›õÍéÓãv^yyæœ.ù:ìîÒÅvŸ>»^¹¼¼rçmñœLµÛõÝ�?‹u¯ƒºÔÑ¡‹í>õtt¨ËëUîÆ fòh vu©c€¶ áúº»Çmn.ú|úíºuJv:5» @i.WÄ_j½ /mòvÀHÝ;÷~‚\ˆc3Òî3Ã]Iºpᄟ×ë•×ëUee¥Ž=:ìÊUn„»WVícKK|Ÿë¿¢ßÕ:ñ’šj·›U¥Sív-ÚðCI¡ó?ÿ…å"`i£ü‡Ù«Á ¦ØlCjc1ýžÔq�Ÿy+WÞè_\_¯“Õÿ8äÖ_†U';�#®`½ Ú6äVºèóéâ믛Ï�#/O³ 1Ãîï|ÿû„» ¦Ï|­êh:h¾Ÿ5k–V¯^=®c8zôhÌm^¯W�GmmmêééÑc�=¦ÖÖÖ ßZ`éÒ¥’4*-nVRR’9Z2ÀäF¸‹q׿=ƒ¿©Ér?£ÕBjÖÀÁfx…ç—·¨…@x×Yyy1ûÛÒÓõØ[oJ’^Ú¤©v»Ù†!V°k7šºÛÚäX¼XI³g¸Ÿ-=Ý Î‡ÒÂa4½nƒ]].2g:‡/b–šíŠî:b,‚v±Ý§Ô¬,%;�!¼•,÷ZuµzõååËcÞaªÝ®¤Ù³•<שK¾ŽˆçëJ _]�|uuÊõÇr,^lÙ�@²vÿæoþF ã:Ž�Úäç竬¬LÅÅŪ­­ÕÙ³gUSS3.}�oÆp['Œ%—Ë5¡Æ;ß` 0ÞŒö ÇâÅ–û�»^yhKO°rõA÷Z󵿱é–ÝÓ%_Ç€÷#I³yÄ|}©£C a ± T�:û‘‡Í×£ô~z}ž¦ÚíZðĪ˜ûÍ_¹Ò|ýq}ý¸Ì¥v_ ÝAšËe`^ôùÌïaþã+cö/f"çåFµë¼°{ïÏYX¨Ýn=\U©ä¹sÇåùz¸ªRßyî9Í.x$æ~ãÀ€ø4Q‚Ý¡ª¬¬4_{&ðoUp+îâ–0Ú3 äÌ�æbjK¶¾b¹°Ö‚U+Í*ÌŽúúˆ€4¼ºqVŒJÍÑÔvýœ¶ô4-Ú°!j{²ÓiV¥v·µ)ØÙiöÓhŒ‹6lPjX¸÷7ÔÅØ|uu vuI’xúiËðÜYX¨«Všã¯Eºº¯¯ê›4{¶e��ìtjÉÖWÌ÷ýû-ÿþz»[zZTïÞ©v»þâW»c.ÒÖåõš×Ïr¯U–Eœìt*ç¯ÿJR¨}C¬j둯<ïÿ�t\×çXVOµÛ#ž/€pñìJ±Û444hË–-Ú²eË€Çû VÁZSS£Ç{LË–-Ó²eËT^^®³gÏ{¼]¯§§GÕÕÕ×Y¶l™¶lÙ2äk544¨¤¤Ä<ö±ÇSuuµå¾gÏž5ÇÓÿüý?ï?®’’’!UýÖÖÖÆ<Îãñ iî7�¶ ¸eÂÛ3X¹èØK›´ôÇ[ÍPÎߨ¨K×CÆYyyfHwÉס?üüQç0zË.XµÒüuõÊ_6&÷ãolTG}½æÈYZìÊߨ¨//u·ÓirWƒA³r¹Ëë5ÛO,XµRÉÎ9êözìêÖÝsæÈ‘—'[zš¹�•ð@põoþo] túÀ;ƒöŒ=öÒ&-¯Ü©©v»–WîŒ9·Á®.{iÓ¸=׿«Ô¬,MµÛõè/ÿAgP°«[¶´Ôˆy4æ¤PÛåõê÷?ÿ¹¾óýï+ÙéÔ_üj·®ú2xÙ¬ö5¾'+ÍÛ¶ë/vÿRSl6=èvkV^žº[½–ß㑲òQ½÷Ka•·Ë+w*ØÙ¥î6¯š·m×Éê”#/OSl6åÿx«.ú|f¥qBjšy‹Ípÿ÷»^çÿ`€)ƒÝ�á®$mÞ¼9æ~æ>±Ú@”””DU744¨²²ReeeÚ¹sè‹Ǻž×ëÕc�=fâ644¨¢¢bÐk•——GT2jjjÌEçÂ{ëž={ÖÏÒ¥K#‚rã󬬬˜A®Çã‘ÛíÖÞ½{£¶õôôhÙ²eòZ´¢óx<*++Skk«Ž;6àÜF•»¸eú·g°ÒåõêHY¹YeêÈËÓƒn·t»ÍPï÷èHy¹eÔðÀ×öF»m¸æmÛÕæ©ÖÕ`P¶ô4-xb•t»#É#eåÆÍÛ¶™÷—êréA·[¹7hÁ«4ÅnS›§Z¿]·Î¬ÆtäE¶}ø¸þ]³�ªàLžët¬}>)+;oôÜvÔ×ëÐúgì=;Ú|uuúðíæ½óaÌcw[›þyÝz³RZŠ®rýð×o«á¥Mæ¼NµÛeKOÓÕ`PmžêŸ»`g§jž|Êœ—d§3ê{ vuéHYù¨W3wy½f…®ñÌÏk°³SGÊÊÍïÚX�ðA·[ÎÂMµÛÇl\¸þ‹ÓT²8M}�Þq¿öùN¿yýp¯mt«dqšïû%_ÀR<»555æë±X¤ìرcòx<ÊÈÈÐÎ�;uôèQíܹS’Bm!Œ ô¦þ QR¢³gÏ*11Q›7oÖÑ£GuôèQíÝ»7âZ±ª\�;¦ÊÊJedd˜ÇoÞ¼Ù<Öëõª¬¬lØã*//WCCƒ²²²´yóf�$Ý;÷~¾$@”xvÏž=«’’óýX-¦–••¥††³ê5??_n·[ùùùjkkÓ–-[äv»G.744˜AhMMMTkqq±\.—>ùäUVVƬrµgYY™ylmmíˆæ¸´´4¢"¸¸¸Xn·[ÙÙÙ’nTð*++ÍûÙ¼ysDøm›ŸŸ¯ÞÞ^~`œîbT�$\ê1]# ¯‚��úð×oG}>Puã@×JUä•@`ؽX�0r ûhÑ5«ù¹ iÎ.ú|êöl,7ó< ö½ vþ«Vêb»O=¬+�€å}}3¬È—1Bú[9/ƒ]w¸ãÂÍK°ÏPÑ÷~ ™éŽ[rýsíÖ!îâÂ5’¤ù9¹|I€=ب_®×ë�¨Ú-**³_í¯©©‰hg IIIIª©©Ñ}÷Ýgî3’ÊX)ÔÂÀ`'%%™�é@÷kœn·[[¶lQOO�¼^¯\,DÝ_FF†e«—Ë¥¢¢"ÕÖÖFUàVUUI µz°ªjv¹\ª¬¬Œæc‹pÀ¤ò�çž“j×a,®ÖŸ±PÜÕ`�jW ɽsï¿¥Õ±§[›¯�#²r·ø™ð匡¾KÝú þ &À„’œ±@é  ¸O{ö¬Ù7x Jj·ÛM¸ ãˆpÀ¤âoj’cñb-XµR—|¾~=lÓõÀÓO+õzEƒÑÛ·�¾@¯Îù>P‚m†î�{¿ÎwúÕT·O’4/;Wó³­+`ÍpÕ¹ÐlËp¾Ó¯ó]~ÍLshfºC-Çëåo?¥”4‡ægçZå{®ý”δžP_°W ¶De?Tó£ýBx…nÿë[ÝŸ$s\R¨Òw q�ïôëtk³.tù%IÙÜÖ-¾þòßôy÷Y~pL(ŸwŸÕŒÔû4=9Ír{¼/ž–˜˜(—Ë%—Ë¥âââ1]Œk°s»\.µµµÝTÿX—Ë¥¥K—êØ±còz½fˆkÜ[QQÑ -úW쎖ᶚ_n°c�`0öwL*'÷z”š•¥)6›r7nPîÆ ºèóizjª¦Úíæ~þ¦&�£*L\-ÇëµçÕR-_½^wh¿ï�\ˆ,gI¡žùÛʈ¾º}�^mîqIÒÞ¦.óó_½Zª3­Ízþ§{µç'¥f_^Ã3/V)ïÑ5Qc8×~Jo½örÔþoVmÒS¥[µ|õúˆÏÏwúÍE܃VãúϼX¥¼ôÐuN·6k×�J´¸p�^³^{^-3ûõJRÍž1ÇõVÕËQóQ³g‡æçäêùŸì½%½†o‡ÃÁ € ï««_X~OÁîµk×nù MGk7£ò·ºº:â3ãs—Ë¥²²2­]»vB?wÞaüÆÛXÒ€h„»&•‹>Ÿþyý³zpíÓšSP IJ[¨î’¯C§vOdLF Úz¼^—½zæÅ*ÍLsè\û)Õ¼±C-ï×)%m–ž*Ýjcô»�µ˜Yí§i¶zþ§{•`KTã¡}jªÛ§=¯–jfš#¢Úö\û)m~¥ú½Ê~¨@¯~V ö:×þ�Þ¬Ú¤7«6)%m–r–ÞóõªáyýªŠo,¦¶0j¬}Á^m~¥æ¹iùêõš™æ0ÇõÖk/G„»}�оç®W?UúŠf¦;Ôø\5oìÐé–fíúÛmØõÎmñŒ$$$hçÎ�òûýüÀ˜Pöï߯O?ý4æöx¯Ø�Ì’’’äñxTQQ¡šš544¨¡¡Á\tÌëõÊív«¡¡A{÷î�Ð÷˜xwL:ÁÎN5oÛ®æmÛ•ìtjÊõÔ.utèJ ÀÝÆŒ@ôr W¯x~g¶(˜Ÿ“«{¢ö¼Zª¦ºýáî�~·7ªfëiSÒfé…msÛüœ\õ{Õz¼^‡÷ÿÒ wû½Úõ£õzUô½Dô˽wîýš™æÐöç×[¯½î•·á!n¬j^c¬­Çë£*tçç䪩nŸyœáðþÝ:×~JçBm|ý�ˆ ÝæîÕËîïêtK³ÎµŸºmZ4$$$hÞ¼yüÀ˜P¦M›sÁîÈ Ö£¶¡¡ARhá±Ñ�™™©²²2³oCCƒjjjäñxÔÛÛ+�Ç£ÒÒÒa-Š6žÂ+™›;Z2ÀøùS`2»èó©ËëU—×K° 3(}ªtkTïY#í ôF´L° W�ϦÙfDÁ†‡W?+éF˜,I�‡öé|§_çBË…ÐæçäjšmF¨—n§?ìZF…îýQ×�UÍ»¸pMTë…þ¡® ‰kߨ!Iza›'ªõB‚=Ñï­ÛŒˆs§¤9bV ‡3X«7Öõ�±†W‡³j/q¡+4®Å…k­Ü�u¯€ñ÷G_«þØq£âôvkÅ�™™©¢¢"ÕÖÖÊëõê¾ûî3^£7##Cö~ÍÊÊ’×ëUvv¶Š‹‹•™™i.v&I‰‰‰–톣¸¸XK—.Õ±cÇäñxÔÐРââb%%%©§§G555fliiiDÈF ^YkÕÊÀQ¥P8Ú¿âõtK³Îwú#zûmŒ6ýCá¾@¯þÏ'þ½ú½ú¯oÿ«yN#�Ž —®æíÿyøu¥ÈÊÞyÙ¹:ÓÚ¬ó]~Ë9y³j“Žìß­å«×[.¸µâ-Ø­¬¬µ…¶’’’TSS#¯×ìæçç›mŒëõ¯† ÿ<33SÅÅŪ©©1Çær¹b¶bp¹\:zô¨ù:œñyÿë%%%Éãñ¨¢¢B ½j�ë[õÚ5Æ9PÞXãÉ8ùÝî[Däçç›÷bÜ�ËåšÐ=ƒ`²"ÜLzá�lëñú¨êÙ=¯–ê|§_çˆp×*\5ªi¥Pµoø¹ú½zëµ—%I¯~Ö qï�{¿RÒºÐå×áý»£®ÿÖk/«/е°Ù�V ÷G}6”jÞþÛÃS3îëLk³ŽìßÕ.¢ñÐ>Ù¿[Ól3ôðêgyˆ`‚‰ÇŠÝѨ‚µ:g¬óõóÌÌL••• ézIII1CÏÁúåfffZM<ÜùŠ5ž›§1V«ð×èqœ‘‘aîÓ¿þá5`lî&=£w^v®ÎµŸÒöç×òë�eÓ¡}jy¿NÓl3´î¥ šœk?¥¾@oÔbfFà›ýP�jߨ¡ ]~-~t�ÎwúUûÆ�ïôk^vnT€ûTé+Úõ£Õ¾±C_?Wö’ùÛ?Ðáý¿4ƒå~zc‘#DNIsD¶gZ�ö ƒWóößÞ¿õBñ÷~ ÆCût®ý”6»¿«å«×+Áž¨Ö÷ëÌ6O•n´/`ì…‡¸·k+ÜzTEE…ŠŠŠb.œÞvb¢/“á.`Ò3*Ws*PÞ£kôfÕ&íÚè6·ÏËÎÕS¥¯X¶_è˜çZ÷b•Þ¬zY�‡ö™A¨¤˜- r–ꙫôfÕ&ÞÿKÞÿˈë¿ðÓ½í¬@ ÿ<¼rת7o¸Ó1Z6$صñõwô«—ê\û)íyµÔÜ–’æÐº«&e/Þ¾KÝ7æ€`@œX±b…>úè#¥¤¤ìâ–ÉÏÏW[[›jkkµeË•––š­zzzTUU¥ªªÐ_–Q¹ ãàŽk×®]câËŽ;ÔÞÞ.IržùH¶`�I0*Ú¿=W—ívIÒ¿{¤D‰i÷MŠûú~Á<õzµa×;šŸ“«¾@¯ZŽ×ë|§_9K bö©5zê•«§[šµýùÇ•’æÐŽÿ*)¶ájÎ’ÂA«\û½:ÝÚ¬sí(Á6Cósr‡|ý¾@¯en¨Êøóˆ}ÃÛï�»0æ"pÆ}ô?×üìÜI»ÀÚ•`�ZüÌ|ÿÿðüà0D===fÀkÈÌÌŒXTM -H×ÐÐ@^Tî&5«ÅÄì‰Q=fû³ J­ªiï�{ÌvVì‰ÊYR¨œ%…þ~‚=Ñ2tìúCßpï#^]¾Øe¾ž6m? CRR’TQQ!�Ç£ÞÞÞˆ»*++rcÀÍ#ÜLjF ;/;wÎ5po[L|½�›¯Çba&»¤¤$UVVª²²R^¯×¬Øu¹\TêÀ-@¸ ˜Ôn² oú\g¼÷¶ÅÄÖÛõ?Ôuú¿›ï w¸9ü»n½o0€É,ÖbbÃÕèÕùNÿõs-dbãÌWWÿMMÍ÷sçÎå?HÄ=*w“ÚÆ×ß•ó$ص·©‹ �C_]ý7�ý—Cº¼±ÐËŠ+˜q�pLZ½]ÿCM#‚ÝÕ«WkÞ¼yL€¸G¸ &•+Á]¾Ø©‹ç>Ô;¼Û-Z¤?ÿó?g’L „»Kgÿ¥NwMù&qåòÅ.}ýå¿Yn[½z5Á.€I…p`ºóë¯Í×}—è/‹ÉaêÉ-[IDATîܹZ±b­L:„»SÊ…‹ú<)‰‰@ÜKNNÖ¼yó´hÑ"B]“á.À”ØÓ£¼múbÚ´Q9ß…”]š™")ÔëtÑ¢EL2ÆÔÌ™3•’’ÂD¸-î"Üùõײƒ£r®€Ýn¾NII¡‚€Qô ¦âá.Ä!Â]ˆC„»‡w ÝÅÄ7ß¼o3 ÀmˆÊ]ˆC„»q(77WÓ¦Mc"Ä�ääd¹\.&€Qtǵk×®1 _¨Ü€8D¸ qˆpâá.Ä!Â]ˆC„»‡w ýÿÖAí™6»8IEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2483566 ceilometer-27.0.0.0rc1/doc/source/contributor/2-2-collection-poll.png0000664000175000017500000010021715250267452024051 0ustar00zuulzuul‰PNG  IHDR/„æ4ÏêbKGDÿÿÿ ½§“ pHYs  šœtIMEß ! i¤‡C IDATxÚìÝ”Õå}/úþH:SêÐ$¢h ÓÞäjlÀäôfzOïQð$zcn0±bW LWoƒXe%åGNƒF#žjî9§3“Õ›¨!uL´˜Úf�§ b´ŒEÆ 11rÿ¾›ïžÙ{Ïža~|�×k-×r†ùùÌþ|ŸçyŸïóŒ9|øðáȘS4�EÂK “„—@& /€L^™tš& ?;v숽{÷jõõõQ[[«!£õØÙÙ;vìˆC‡i f555Q__ÕÕÕúBUUU1sæÌ¨©©éóo±sçN�#àœsΉ™3gžÔm0æðáǽ(¥££#î¸ã #8Qûò—¿¬!£õø­o}+žyæ�#䪫®ŠK/½4ÿöŽ;âž{îÑ00BÎ9眸ùæ›û¼ÿ/þâ/¢³³SÁY¾|yÙE.':��SVww·F€Tn¨aôëqÿþýFÐöíÛ ÞîèèÐ(0‚J­r\ÂÈ:Ùç‚§b¿1ö}ñ±é5 ƒ¿{~Ÿz„ã¬/š61jƽOƒÁëìúE<ûRÿ7 ~ëCccú¤34ŒòØô÷/˜¤Á`ü°}üÛÛ¿Ð!¼dÎû¾ø÷³ÏÑ0ÊDõÙ¨Ç�MŸÓ'0ä^Úw°¢ðrú¤3ô…��±©:„áѾï ðò��™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “NÓÐ×–m¹øÁ󹨽¯3v¿¶?""fL¯�y³ë¢aÞ, £àŽuÍùÿ_¾¨q@Ÿ{ «;îÚôDO-O«�ÆKf ÙÏó‰ êbÞì: NX»öí� ßmpý<ÔòtìùigLþPM\Ûp±†ÔVŒR¿éuÅñè@Wwüàù\lÙ–‹Ú;""bü¸ê¨Ÿ^õÓk+š“ËX-™fÜy<´mË–±³}OŸ¶�;».æ^`\Kö/!¥ù©íÑôµGb×¾ýE;°µŸˆ©“&ÆÊ?]0$áÇÉf˶\ìÞ·ßšAÙµo¬omÔ ´ù©íqû}=Øu+®’Ÿ'ùzË5 /9¡í~­3ÿz�ˆxvʨ¯›Üïç­om‹­ÏçbîuC~ÝOnHœ(7†³­8y%uëuÅñ$¹¾¯ÝøDèê.:¦‹ˆ˜:ib,_Ô×ÌŸ3,cµ­Ûrùð3Káå뚣aîÌŠúáRŸ_IÛ޷⺲m¶¾µ-tý,nºúr/ZF„ÇÆáH'¹èÖâʦ»óÁåŒéµ1÷‚ºX¾¨1æ^PSΪ‰ˆžåʦ»cíÆÇ5Ü\Ùtw\ñÅ•±ûµN�Á \3ÿâÔ€éé}n²êrüØ*88F‹n{`T¿ÿ®}û㣟¹­ PàĘ“]ñÅ•qû}ÍùpmÊY5qÍü9EçdŸ¿õobÑ­ âN4»öí�º?øóž¶yûÐàúï[(hÛd¾Û0ofŸ¶½â‹+ã¡–âãí+¾¸2>ëßÄ�®C^´Œ+/áÈD,¹Ó4czm¬Z²°è�¦‡ZžŽek6Å[oŠek‰ñc«!JÚkÞìžAÕî×:£eËŽ övy$&€ƒ³³½#n¿¯9n¾~tV¢ì~­³è¿’à2³Í½ .–__|ÅdóSÛãöuÍñB{G<Ôútì~m|ÿÞ¦!ýyæÎ®‹å‘�—ÇÚ÷=Ôòt›UO�41¿çKDOxÚ´æ‘‚§Ô�Bµ<¾Û™V«—.Œ;Ö5ÇúÖ¶üãÆUGã¼Yñ¥E Ež+¾¸2"">óæ”|T>ù‘t#ù�¾Û–?¹oåŸ.(Øl:Ù ;ýsõt²=§Ûžh'ý1pI µlÙ-[vÄ�®î²uÛüÔöük©Ô†îCýºK^óI­³3·'š¾öHŸ:H¿ÿû÷6ÅÎÜž¸ãþ–‚›$óf×|NóSÛ{VÁ¥®5×ο8V.YP²mvíÛw®k‰æ-Û ú�—ÌŠÅ /s%Í�]‹».‹»6=1$��¯om‹õ­O<‚6uÒĘ{A]Ÿ×pRé×lÓšM1a\u|dZmLW?x>ãÇUÇc+o,úý®lº;ÞêêŽ)gMŒu·?¼+éï/¼¬ /-U7óf×Å5ó/.z�é]Óµ<�ÿ¦Nš‹^ZÑaËÖlÊŸÛûç‚ÁH^ç+ÿtAL™41šÖcñkæÏ‰Å /«xÜÏðKƉˆ¸yã® ãªãæëcÙšžëêúÖ§K^S‹½†ë§×ÆòE�E¯©Åæ5ÅÆ™wmz¢ ëoNÕßø³Xm\ñÅ•%û¾RcÍÞÆ�­Š]ÝñÖÛ•¼«–,ŒÝ¯uÌuËÍëz·QÒgn}>×g|}ÓÕ—¸Ïœ0®:–/j(øûnÙ–‹»6=[·å Ú'ù,^xiÉëS±ƒ¡’Ï[¹dA4?µ½`î<�ùC©ßá% Z:ôkœ7s@ÊwV-.y!Lï×Òû"¾vcÏEîû÷,ës1}¡½#¶>Ÿ‹�L«ÍO {þí÷5Ç¿ìŽkçω+nXÕg0˜(ôØÊ :âݯuÆ–m¹8|¸g³æäw?¶*ÞzûPèꎇZŸŽæ-Û‹þlI§ü‰ J2“ïQìwJÿ|É>½ÙôÎÜžXtÛEÛmg{Gìlïˆæ§¶Ç÷+XˉíÚùç÷¼l~j{Ù}gÓ�Èû¸�¹=Eë¨÷ëîÑû“¼æ.ý1Þ>”¯•t¤ßŸÞs¨w-^qêøÑ†ù‰]±kÛÖçsñ£ +úÔK©¯›´góSÛãÚù— vàæë£eËöØýZgܱ®9ç üÔÓ]ÝûM÷îÇvíÛß§?J×GºN#"î¹AqǑߙÛÓçgÚ™Û“ú~¹¢óSÛóßã¾×ÔM©­b¶lËÅ–m¹hÞ²=Ö­¸®àk¦ææ§¶ÔÞ®}û+:ì Ýg¯[q�à’!‘¼.·>Ÿ‹;úOŽWW5ÝÝçc¶lËÅEŸ¹-žÝ°¢O½¥_·åú›u+®ëÓ?—»>4?µ=¶nËÅòE %÷ÝÛµo\Õtwɱøí÷5SfLrsfüت_ãæÍŠekz¯õ­mEÃË^ꈻ6õ=a{g{G\ÙtwÑqO±yM%¯ñôœªÔ5»Üø³ØX¬\ßW©¹³ëb}k[ìlïˆek6Ū%åCÏbãæróºÞcÍR}æÎöŽøü­k7>Þ§Ëõ™ºº jºÜ5&ý7xtå�}n’”š¯§?ïLÏÏ�:øü­ë[ŸŽGWÞè3„<6ÎIí­Ô¤a ¯RÊô…pñÂË"÷·_‰wž»?^ßüõXµdAþ®×7¬*¹ér\^3N<»aE¼¾ùë=ƒÇ#Ë÷ïÚôD,ºí�8|øp¬Z² ÿ1Éþ$‘¿kÕÛÖçsñPëÓ1czm|ÿÞ¦xã®�wž»¿ÏÏ6T�]¬Z² àNÜ5óçÄ÷ïmŠïßÛ”¿“w «;®ú³oäÛmù¢Æ‚vKV¾ílïȯàäÕxɬ|-$wÄKÕãúÖ¶üç›À$¯õñc«bÕ’ñúæ¯Ç;Ïݹ¿ýJÁë®Ø¤m¸5}í‘?¶*Ö­¸.rû•xvÊX¼ð²£5Ótwܱ®9¿éú뛿ÏnX GnÄìÚ·¿Ï©ì[¶åòÁ)gÕĺ×üÎÉç>Ôúô�ïʼnc¸êX—z¤o0��7­y$L4Ì›ÏnXïÝÛˆñc«bñÂËbù¢Æ˜1½¶ß•½ƒK‡2Ô’Gm—/jŒg7¬ˆÜß~%?&Lú¢«šîŽÃ‡çû£tq{¯›hÍOmÏ¿n{×wò¹é¯ßÛ²5›Š^žÝ°"®™?'tuçêb}:¸,57¦ÌŽt(U?}à󱩓&æ÷¾,X'¯Éäuž¼¾ŸÚ3s ãž¥«7å_ãs/¨+Ù‡-ºí�>ûC§çZIŸRj,–ÔA¹¾¯Réº[»ñ‰øà'ÿ8Ýú@´Y=˜y]:ØëÝgûý’ñuãà¤Ïœ{A]Ütd|û}Íטäoš¾N$mÝ´fSŸ¯Ýû‘üý¾oS̽ .tu—<´÷ßoÕ’ù6¦ÇÜ/ᘽðÒÑÓì†ÂÚ�GWK®Z² V/]˜ŸüLW7]}yþÀŸ]Ýq}™ ÚòE�qÿ-Ÿ‹úºž}I/™Up‡lg{G<~oSÜtõåù�¹éêËóáF©;aIhÑû`¢›®¾<;²št(7g®¯›\ð}¦œ51æÍ®‹y³ëòw¢Ön|"ÿ³>¶òƸùúÆ‚v»ùúÆXwdÌÎöŽx¨åi/Þ“\rpÏ–m¹’¯óôŠ�d°S8akÎÖ[µ¸`눩“&ÆÍ×7æ…;Û;F|ÃòÇÇã÷6ŵ ÇÔI£¾nr¬^º0½ÚÙÞ3¦×Æsß’? ²¾nrÜËçòƒÓÞwê“kÎø±Uñì÷ĵ üÎßYµ8?¨LÁ‚bæÍ®Ë÷7Éãã•Ú²-—Ÿt,^xY|gÕ₈�—ÌŠg¾%C-Y]®Üv.•ŒÅ“ñnOßîFÝhKŽs¹mÍ„±ý¯nK^çÉëaÞ캂C’súýys{òûs6Ì›�³©lÖ; _¶fS~|•ŒóÒc±ûoù\L9«¦ –Êõ}•š:ib<¶jq¾LVþ§ewÇ?ùÇqѧo�ek6E˖탚×ë3‹ý~ßYµ¸ à+5¯ÛµoAŸ™nç¤ýç^PßYµ¸ /M®é�4í¡–£[Õ$׈äëΛ]�³©ì�Åôß/™?$ß¿÷ü¡Ü˜€�^rÒ؆R² lÊY5%÷[IøS.x)v*rzUÉŒéµE;¬tç±ûµÎ¢_»Ôž›óf×å/ôåV´ µtTêQ‘k.Î.’A-'¯t}”d¥7~ï]+éU™×ÌŸSrÏ­›®¾<ÿºK¾ÞHi¼dVÑOì‹í 5a\u~åBzuyúz³jÉÂ’�±¤o’¬ou£€Òn¾¾±`‚UééãIX0~lUÉý2'Œ«ÎߤXßÚVqÝxIOVnåeòu{.;s{ò5’„2麹ùúƲustEKñ0¤a�× .)¥Æ¬é¾æšù Ú“�é]Ÿ óf•Ýã½\HõБ¾¹gEÓÂ~¯=¥Æâ3Žì;WLOdLy"™‘Ú—±XXTêõ0a\u~,u «»¢kSãÁR %Ay±ù^²¢ïšùsŠö ={;ö¬ÎŸ|dçP™7».Ú›¿‹^–ï³ò}à‘õI˜Yl[¢þìÌíÉ·²À¦¿.5¾ž{Añpv×¾ý±xáe=ט2{n§Wñ¦_É�ËróõRמ�Ì’ßÏ ’¡#¼„!’^é˜LŒJIÿûÖl±Á^ú}�óЇ|ã+ØW£Ü$(¹ïÚ·¿â‰è±Ø™Û“ï”û»ÛšL&‹MJ9¹$z”šxìÚ·?§µØªËtÝõW¯I­èꑚèýz¯t°ÞŸôï\®Ö¦NšXrå&ôî“óøx¥'ž¦of•z°·†Tߘ^}�üÿ”³jò_·÷ ÍG&”SΪÉO˜ÒuÓ0oVÙ¶Hêªw(Zl"UN:¸\µd�à’ }J‡µúš7_ßßYµ¸ìk÷Ç%jºu뎊®Åê1]Ó�ófUÔ·ïlïð”Á }Í„^¡\¯‡�ö5Éþœ¥ækÅÆZI_²%u°L¹ŸéÚ†‹ã¹‡o‰ï¬Z<ä{&NW«—.Œ7þçÚxvÊü#Ùiººãöûšã¢Oß: úHÏÑú;O¢!Uƒýµ_ïqjr�)èêŽ1cŠß$<î·O/2/X%ÜÏ8=¹¶–0pìᤰ½ÐÞ‘|üX¤W9ö7ÈKÿ{±Õ‘•<ú0\ƒÔô# éƒD†Kú{´lÙž?±®¿6.v'—kæÏ‰­ÏçzÖéõzXŸZ¹Qt£ôÔà£Ôê�‚ÁÓº‘«‰üÏUÁA•"Ô;„¹¾Ÿ�iÌ‘ß[]‡¼Ð(+y|| §�'¡=?í,»ç\z´u[®ì©ÄéšHúö-ÛrùúO‚ø¹GoK«Ûº-—¯£dwzB³ûµý×[ýôÉѲeGɉ^%�Cn}>W0ùÛú|{E§‘Ã`UªO@_Sì5ýB®#¼Ý�?¸\@”_ýÜO½ tÒãÄô)ÈýŽ)Û;*º¾0Lã�Ô8l°Aòî}©yX?O­”�V^&¯ß=¯uV¼oj±ùÞø äR_7¹ ½¶lËÅúÖ§óãèd_ÊR§­÷í·+?O"]ÃÇ2¯Ûú|.?ÆÝº-»_ë,ùdcúýý…ÂsgוÝ+û®MO”}R1ù›»92t„—œô�å Gî¸èêÐ�­rß_g4š§Ž g0:/´ïé3¨¨ýߪœì/™ËÖlŠ·Þ>µ¶ÅêÔ 'L${Aövœl*]Qyàm.ú7�ÓÇÓƒør{3‹¹ÔÅ íÁEòÿÉJ‰¹³ë¢eËŽØú|.®m¸¸ PIïw™žıÞ`›{AϤ)9uÖ ãoîÚøDÜžÚ[º`œ<¶*¦LšXQPTz,]Uö߇ëúÂÐK�ÃûšØóÓÎükk¤æG½÷k­ÄÖŒ?Ñ’ì_¹|Qc\ÙtwþF`¥}Ú@‚º‚кȼ®¿ÀùŽuͱvã%¯1õÓ'÷ Ó!r×�~ûùöÅ;„—œÔ晼$�É@&ýÌmù ƺ[®Ó˜ƒ4嬣–UK¬ü,g �0qâI²ZßÚ­[wÄê¥=ûÓ¤÷êï‘ðd 5Ò7Ò¡ýh´Û£+o¬p0_å…FE¯©u·|.¿eÑmijßRòc×ÌŸSQ�öžäôçÚùsò+A“kA2ÉHúù¤ÿOçJ+?¶ª`‚6~�“›c .×­¸./™}úÖØýZg,ºí�˜›:ä²néêMûØÍ½ .æÎ®‹)gÕÄ”I=|lÙ–«xåuñ/Û˜òøÒ0ofþ†Ò@ÇeÛŒà�žÁôaéˆÒC-Odžï¶ÅøqÕñXcÀ©“&ÆÍG̈žÕ�•ôkù»¥ƒÄ�¶Ë_\™Ž“ rî캨Ÿ^›ßúå¡–§û„—éqí�c|ºè±•7fbõìÉDxÉIÞQΊekzND[ÿݶŠ;¼t8ò‘içô¹èö÷x[ú.ÝHwbýí™UF"¸H_ô“ë R7-¼,Ö·¶Å®}ûó«“’Cf¦œUSòõ”®»þŽ ñÀ(<Ž=eRMÄó=BuÆP+öøx¥Ÿ7Ôêë&Ç”³jb÷k�«\Òû“%+0“Io²�ïq@ò(x¦”›˜¥1¬¹Ôå÷ Lá]ÝzlFÓ®}û Ì{üÞ¦•>¾Û_¸9~lµ¾î8Ò8oVþZ»vãýn?’–>X¦Ô>’/´ï)ûzHæG•ì›l;2˜ñÔ@¶`hÙ²=Æ�«ŽÓj�ùæÕî×:óóÏJÃát[ fܺkßþ²O7¥kx OA¥Oð¾fþœ’‡P{T?Àöw )¶J6=?Î5f¤9°‡“ZúÐ�ô…°?w¦:Éd/ªô!?è' ¬ôàŒáRîБ¤ ÒT:9+uêsÙIfªclîçóo¿¯9®lº{P§ßqbª¯›œ\%¯Ÿdð›>‘¼O@�ª»ô¡åjbüت!¤ EÈ1˜P¤Òßùʦ»cÙšM�º ‰Þ§�'�ñ•z-ö·‰}óSÛãŠ/®Œ;Ö5øñϤƛ·lÏOJÓ5�œIŸœü,½'¾}T?u“|�†~)¨Ô¼ÙuqÍ‘GØ·lË9±”ãBú½›Ëœ6^êÆ`RsÉ ¼R’ƒ}z×L¥cʵÏ�)íI7ú/™UÐTz@bÏ�mù¹K©…(åñMÏÿ*Y…›>œ­Ük'Y]œþ}*íSšŸÚÿiÙÝqùVÉ£æéÐmíÆ'*úœô „JW';¤¨¿ž1À•Ïé¿W©à²ÜX;飋]Cºº‹.øÈüaÑ­Ä¢[0oBÂKNz«–,ÈÿÿUMw÷`.ºõ�£ÿ_PW0PJز-Wòëèê.¸#=ûíÝqKñŽ=·'üô~ ¢¿Ó¶lË•$ÁnïN~¸êüäl}k[ÉÁÊÎÜž¸c]s4?µ=väöxá’—ìOײeG4?µ=ÿ+÷(OúÆÅ†ï¶• F¶lËåkb �"½ðRñLwíÛŸÿš#=1Hê°ÜdmíÆÇ£ù©ín!}=OŸ>^ª®’kþ®}ûËrM_{$¶lËÅ׿ýø€W�$!äžoÏ÷[½o>$“�;ïo‰]ÝEøJ×Í]›ž([7•žr<°1ÊÂTݶØÃ�ãà:pô©�R�TöŒ…Ÿ,úo7-eýw�<9 ]®m¸¸ ¾«Ÿ1Þ�®îhúÚ#þæÙ"Ý·óPËÓùNï3=P¥¾~zNÛwÞpq~ R*X\Tâ`ËÞó‡R¯©-ÛrñPëÓñPëÓúí!$¼ä¤W_79`èêŽ+¾¸2Ýú@´l9‚ìÌí‰õ­mQ÷y$uüت>{]Þ|}c~’qUÓÝ}îÈìÌíÉ?–LJFCóSÛcÑ­\ð·lËÅ7¬Êÿn½W­%wÜvíÛßçs[¶жó‘}‡Z·îÈŸ¦š|�›^–o·+nXÕgµ×–m¹¸êϾqt`ëäU &*§&#ÍùÉK“‘åGIJÉì=ÉIŠÇ�­Šå‹*{„©ñ’™G~kéSgŸºaU>|xT&ÉcX;Û;âŠ/®ì3èºkãù­4fL¯Í?¾ •Jïo•Ü[¶æ‘X¶fSŸƒ|®lº;?à¿éêËKÖó†Ö¶Øú|®Ïk9™€èê.yzqúÑñbÿžîÛÓuÓ{"2œu3a\uÜdB~ «;®/1¡‚¬H¯p¾³È�²d,œ®£ô*ÌúºÉùkÈC­OÇ_\-[¶Ç®}ûcë󹸲éî²7×–/*‹÷ivíÛ_0/÷”#߬[q]þzwÑgnËÏÉÒZ¶l�+›îŽ+›îÎÿ×­¸®ß§cÝö@A_‘Ìû’ñßÍ×7Vd§û°»6=ѧ;ÐÕ]°ØeñÂË ¾nzüÙ»’±l2‡L÷éí¼Jõ}å¤ç�K×lŠ+¾¸2îÚøD~N–ÔØ²5›¢îþ<ÿs•[ÝØräé†ô*Åäû$µV¬ÏLÂÁ)gÕT¼ohï9iÏ5¦ï‚œdN[*Øl¼dVþ:•<Õ·õùÜ‘ÅÛã¢OßZvUeÁßï†U}>v˶ܠæôÏž—� Ã’ÉGr§¤”)gÕÄc+oì³jr¸êxüÞ¦¸üÈ èʦ»c꤉1嬚‚“L+íd‡ËŒéµùß1é’Ÿmüت¢û-_øáýù IDATÔÍOm?r²óÑÏMÞª% òmا͎췷kßþ¸ü =´ÿÞ¦˜7»îH€¼0ßù/ºí�XtÛ1ov]ì~­³ ÓÍv#›’Õ»ë[ÛŽÊQÁʧd�¼è¶ò¡DR¯é×Ýø±Uñتů’¾fþÅñPk[¼ÐÞ‘¯•©“&¼Ž[yc~ô‘¾ÖílïÈ·ÕEŸ¹-ÿ;ïl?ºRtüت¸…ƒÈœôéã¥Ü¿âº¸²éîØýZg¬ÝøD¬ÝøDŸþ¨§žæôÙûlÞìºüžcéþ¨÷!AÉI Ò»_ëV–ºnô®›º?øó¨?²fºnfL¯–ºi¼dVþwIw�¬ª¯›\ðzMê%™ì'õ½|Qcþ†ã®}…׊ÕKÆ[owÇúÖ¶¢O3͘^s/¨+8(1uÒÄxlÕâ¸rÙ]q «;–®ÙK×lŠy³ëŠŽÅGò€úwmÃÅ1a\u|þÖ¿)¸Æ—2~lUÜËçúý;&ãÄ‹>s[ɾf ×Õd¾÷B{GA6a\uÁëõšùsòJ–¦û”ôç6Ì›YÐÿÕ×MîÓ÷MW¯oþzÅ}ɺ×Ų5›â­·•}R0=ÿ+vC.©ñ�íùy]îo¿S'MŒÆKfåç„åúÌd>=ЕÏ×6\k7=‘go}>—_홌ß{ÏKwïÛ‘êó[ucþï×üÔö>dNÛ{7ý÷KæûÆUGýôÚ>óÖûoùܨ|KÉ×êë&dzßRðX\ú‘ê†y3ãÙ +FuEÓã÷6åï8ílïÈÿl×ÌŸ�ßÛTôw›:ibÁç¥;ý¹ÔÅã÷6•=ÕqÕ’…ŸÛÓ!ì)舿·©`¯°ôÁH3¦×Æ÷ïm²ŒƒÒ‹ ê´ÒÉȵ Ç÷S¯ë¤^“�Ï5óçijß2 À<¹‰qMê1˜Þ¯ãÑœ,ÝËçbÝŠëò½äw>ú¸ýœ²×8¨¤Ê=¾—î+§VÞ§û£¤¿½¿Ä×¹ÿ–Ïõ9x«ÏD-F[U9uÒÄ‚½¶ÊÕåý·|.[ycþãw¶wäë&YYQªÿªºõø8Ç‹ûoù\~õä�®î|H²³½#æ^Pß¿·)n¾¾±ìÞsIÍ5Ì›™í'×…þš7»®`,žŒ)“ëÄŒéµñØÊ�)3ªñ’YÑÞüÕX¼ð²’›N9«&V-YíÍ_­hLuÍü‹cÝŠëbüت>}ͪ% Jö5ý�õÒ+}“~!â芻R_·÷ø³Øç~gÕâ¢s¸t›èêО­×6\íÍ_�kæÏ)Ù¶ãÇVåç«¥N*6¯Kß°¼éêËãû÷6•ì3/¼ì˜Æšé9c2ŽMÆïÉ8ö¦«/Ïÿ޾ÛÖçï÷Ü÷Ī% ~�ÓkcÕ’ñÜ÷Ą±ÕÏ’ë\þIŽ#×97G†Ö˜Ã£ñìÇ�\.kÖ¬‰ˆˆßúÐØø“†ÿí¤ùÝwæöÄ�·t’ƒ¹sÒ{èÑÚWçöûšów¸ßyîþüÅ>éhò³¥?o0í²e[®ìçõ¾3>ض?žÜ´îÙüÿó›ßT�£ ÷ën(Vø¦¿æ„±U™ ÓµÔòtüàùö˜2©¦ì^q?~ioDDŒWu\�Åùkîp¼ŽeÞ8˜ßo¨ÚäXç»•Ôøpö™ƒ¹Æ,]½)ÆŒ‰˜1­ôÕéÓÆçš·f†ð€Q÷ÖÛÝù=æÎ,®¬ÝøxþñÌJö¸†ÁHïΉ%9ô«ñ’YEWI¦O‘Ÿ+˜Ì á%£®aÞ¬¸ý¾æxëíCqÅ «bÕ’…1eRMLþPMìùig¬oiˇ›3¦×Ú·’!µkßþØóÓÎx««;¿Obýt[ƒœH/™•?ì룟¹-–/jŒÓΉñãªû\cæÍ´ª2C„—Œº©“&ƪ% cÙšMq «;>ëßý¸äÐJ[·åbÑm¼ï¦«/Ó0'�ôiá»öí/y�i˜7sÀ91¼„—p’˜rVMŸSá K®m¸8æÎ®‹µŸˆ­Ïçâ…ÔÉÐ3¦×Fã¼YV\2,Þz»»`î´|Q££OàkÌëšcg{Gþ3czmÔ¹Æø»g�ðN¢‹´�Y7uÒÄX½t¡†`DÝtõåqÓÕ—kˆ“äceåñåMd‘ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@&�¦ €“É »þ-^}ó�†(£fÜûâüÉ¢úý¦Œ¨Gõ£Ë+Ÿ!ÕýλñÕÿöb¼ùö;£Œª÷�‹.ŸÓ&�¡1P�êõˆzdííìŽu�¿¤!*ð~|J\rþ5êQ=¨òØ8CêÕÎn³ úůâ‡íû5êQ=¢Q�Œôëì�w5B…v¼Ò©P�êF�•—0J:»~®@=ê‘QôU�c=GÚ›‡"žÛûž†@=ªGÈ á%Ãæ·ÎŒ¸iÎé"åu¾w=ó+ �zT�¨G ¡É€3«"~¿îT Ñ«ŸÛ«P�ê²Ãm “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix dÒiš€Jýü¿ŠÿõZWÙ�yõÍn U¡C´'¨GõHv½úfwŒ3¦â�E=CZ/�]¿ÔkE=ªG†Ö±¼FÔ£z¤üœ�ÂKбŠ¿nýg 1TíÙÙ­=Q�ê‘ãØÿûÌ� EϾ´?ž}i¿†P�Œ2¯õÃÍcãRUU¥ ÃõX]]­a`ÕÖÖ¼]SS£Q`�yæ™Æ¬À¨³ò’²êêêⓟüdtttTôñÝÝÝñꫯj¸ 'ŽäœÜª««cΜ9êQ=’ázlhhˆêêêØ¿`«½Ô£zdà&Nœ—]vYÁûfΜ÷…êQ=2qþÙEÿíÎ�ÏFDÄg>ù;1åƒg¨G†ÝÌ™3cæÌ™ÇüuÔ£z„J/9î,üòßÅî#�Ï?¯»¦l‡ðÂ+ûãß}éôû5ëÏ›76ÔÇg.ý�>ÿö©¿èùüÿïÎÿ#æ~älP� Õ#”´ûõƒñ…¿ÞœûçŸ]¶Fv¿Ñwn|®ß¯{çÆç¢þ¼‰qï²ÏÍ…äó?qþÙC–€zT��ÂKŽ+-?|9?1‹ˆØ°ù'ýÞ=;ÚQM*Ú9íy£+v¾¼?®ÿë'cçËÿ+}BCƒzõ¨apõø£W""bƹã…Wödž'RqÀÿ¥«/,úþݯwņÍ?‰�/ï�…_þ»xæ¿,ÒÏ Õ#d™ð’ãÊú'’ïHîÜø\lx²òÉÙ÷¾ü‹¾ÿÀÛïÄŸÝÿ÷±aóOâî–¢ácçYAêÔ£z„A¹»ygDDüÕç/þÝ—þÇ‘› V´«\ÝÞØ8#>þ'�Æî7º¢åG/Ç5—þ®Æõ'§�sÜ8ðö;ÑzäÎÙ� õ1ùãz:‹¾|L_wÂØ÷Ç}riLþÀ¸ˆˆØðäO46¨GP�êlë�_�ÝotÅ䌋¹9;æôÜžúÙ|ìõSÞoæWJ·üð� êNÂKŽI'3ÿ£çÆ„±ï�†#�Ïú!šLÍ=²áòî7jlP� Õ# ¼�Ô]R7 ;·àýÇjƹ#"â­Ÿ½£±A=ÂICxÉq#YòŸt:‹ë#"¢õG¯Äî×�}BeRêÔ£z„Á*XÝ8£§.?z^Œÿõ÷ Éj興¿q_DDLù€C@@=ÂÉCxÉqaçËÿ»ßèŠñ¿þ¾høèy=�ÄÏÈßíºëÈÄmÐ³×Æ�_錈°Ÿ¨GP�ê¬åG/Ç�Ÿ½“?0.úpÏjèžÚ<ÖÕÐëŸüçØùòþˆˆøÄG&ipP�pÒp`Ç…»›_ˆˆž»eé“Ünlœ_øëÍñðæÜ NA=ðö;ñ÷/¾wn|.üì�ÿëï‹Ï|òw48¨GP�êä-=õ˜¬~N×ã†Í?ɯ†.wPÈ�Ÿ-úþÖ½’Jfœ;Ñá  á¤"¼$óÒKþ?siáÄ©á£çÅŸýúßÇ�Ÿ½ëŸüç²GuãÝý~¯¿úüïUtò¨GõêHì~ý`>ÌHö�MÔŸ÷›1ùãbÏ]qWóβ7îÜø\Ùïó‰ó'Å#ñï58¨G8©/ɼô’ÿÞ�¬%Kÿ7lþI<¼97¨»^3Î�3Î�_ºúB3P� Õ# X²EÔŒ+z’ñÔž{Þèêw5ô—®¾°èû'`\Ì=ÿlµêNJÂK2ïá͹ˆˆØóFWÙÕ![üjÙ¥ÿÝÍ7jLP� Õ# [=î~£«ìj­þVCéê‹4&¨G á%™¶ûõƒ±õǯFDϲüR^xe¼õ³_Ä�Ÿ‹ûþäR êÔ£z„Ñò×ó{Ö{„ôÎ�ÏÆþqß WCêNVÂK2-Yò?ã܉ñ½/ÿDz�Ï�Ÿ‹Ö½Þ~§àÐ@=‚zT�0\’S‹>z^Ÿ-ÒÞúY}üà÷õ»P�@¡S4YVê ‚Þ’Püì�hùÑËÔ#¨GõÃn÷ëóõØð±sË~lÃÇ΋ñ¿þ¾ˆèÿ @=G /ɬ–¾»ßè*˜|•2åƒgÄü#'É}£å�êÔ£z„á¯Ç#AÉ䌋†��×ïÇ'5›¬†Ô#Ð?á%™•,ùŸÿÑs+zÌ-¹»¶óåý±óåÕ€ A=ªGVwÙ¡á£çVôñÉji«¡A=•³ç%™tàíw*^òŸ¸æÒß�?»ÿïã­Ÿý"în~ÁÁ A=ªGÖzLÂ�þVA'êÏûÍø«Ïÿ^¼õ³£«¼¦|`\|éê �égI>ÊÆùàÕ#œp„—dÒ„±ï�îæüy¯m\TðöÜ�œ=¨¯“v¬ŸêQ=‚z„³¿tõEþ¼Å�õoOùàƒú:iÇúù Õ#d™ÇÆ€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™tš&`¸üÛ¡ˆï½ôž†HÙÿ3í�zT�¨Gõ¨ÉŠy3âOZ©!@=&¼dؼy(âïr¿Ò õ‡&Nœ¨@=¨óØ8CªººZ#ThÖ¬Yõ¨Q�¨GFXmmmœy晢UUUññ�\C Õ#Œº1‡>¬J;vìˆŽŽŽLýL/¾øb:t(êëëãôÓOõŸ§¦¦&fΜi2ËIY�‰ŽŽŽØ»woLŸ>=jjjÔ#êq„üò—¿Œ_|1"">üág¢_T�У»»;|ðÁ¨©©‰«®ºJƒÀ(×bUUU\uÕUú%eÂKNxßúÖ·â™gž‰ˆˆsÎ9'–.]ªó�QÖÖÖ>ø`DôÜI^ºtiÔÖÖj·ß~{ìÝ»7""fΜ7ÜpƒF�Œ¸çž{bÇŽñÉO~2,X Q@-ÂIÏcãœÐÒÁeDÄÞ½{cõêÕÑÝÝ­q`”tttÄ£�>šûСC±zõêÌ®…­_L‚ˈžÕ ßúÖ·4 dÀ“O>™K""6oÞ\ð602vìØÑ§s¹œ†�Q$¼ä„ž ¥ƒË„FOGGG¬^½::Tðþ$ÀìììÔH0Âýâ3Ï<�<òˆ‚QîÓ7öÒu«o„‘ÓÝÝ]ô¦Þƒ>hþ£HxÉI1A»ðœSâ?ןš[€ £31K—¿vZÄÿý¿Ÿ¿vZÏ¿:t(¾ñ�o¨KO>ùdŸ~ñÂsŽ7oÞmmm FAwwwÜsÏ=ù·'�1&~£ª°oFƃ>˜«þFUäÇ©���ÑÒÒ¢�`”œú—ù—©8‘ .?=óÔ8{ü˜8³jLüøõžm^</¾øb\xá…™:¬Nä‰Ù›o¾=Á›æœ¿]3&~÷§DÛî÷Ô% “¶¶¶øö·¿Ý§_œñ¡Sbï[‡ã�Ÿõ¼çÎ�QSScÿYa÷ß¼òÊ+©þñô˜ñ¡Â¾±»»;Î?ÿ|�ÃÜ_~ï{ßË¿ýù O�ßýÀ©±}_O-¾òÊ+Q[[úЇ4Œ0+/9¡” .ÕZ� #­»»;V¯^�ßg/ .Ï>cLDDœ}Ƙ>u™^�ÛD,9+"â·ÎŒ‚~ñÓ3O�IGj1¢gʼnýgaäôÞçòÓ3O‹3«úö�VGÃðêìì,غaîÔ1ñÛ5§ÄG>4&Îÿà˜‚ù¦¹#Œ<á%'Œþ‚Ë„FV:¸ŒˆøÃŸš.KÕe{{»CDàõÞCoÒcâóžVð1U§�‰›>^`:@ FF.—+¨ÑOMë JÒ}cz{‡G}TmÂ0Î%Ó�‹ÿ~]á�¾ô6G雂ÀÈðØ8'LgSIp™ð9Œ\mþÓ?ýSþíÿ\j\T{Jɺ¬:}Lüä_{êrïÞ½ÑÙÙ3gÎÔ�0@½÷˜�tFOHYuú˜>{ú©câ‚IcâéÝïÅ»ïE¼ûî»ñÜsÏŇ?üá?~¼Æ„aÐÝÝ_ýêWãÝwß�ˆ#«¢g�Öçã¦ÕŒ‰þ×ÃÑõNOm¾üòËÆ«0Äž|òÉØºukþíÏ_xj|pì)ýäÔßÏîí£þô§?õø8Œ0+/9î 4¸LX� #[›ÿñÃ¥ƒËļs W™<óÌ3ñä“OjL€d«†ôáXŸžY<¸LT�>&nšSx€–Gã`øÜsÏ=5Ú{Utº6Ó«¾öîÝ[ôTr`pzÄó©i§Äo×ô¯þvÍ)1wªÇÇa´XyÉqm°Áe L---±yóæ‚ÚløÝÊjsƇN‰Îîˆ}{êòÅ_tˆT( .{ŽÕ{«†bÎxÏZϿڳSŸÃ×G¦Ç¯_ühá*¯bµyÆû�ŽW÷îÝ«_„!rÏ=÷Ä믿=O)|vöi%?vêoŒ‰ç÷½?·g%ô믿^x¡F„ ¼ä¸u¬ÁeB€ C«­­-{ì±cªÍ:%^Úÿ^ü[Ï¢§ CzŽñ§¿WYp™è’óôSÇÄ9ã ¯®®ŽóÎ;OcÂ0óØ8Ç¥¡ .!‡¡Ñûdãó?8fеùù Os 2 À£�>Z\þçúS\–ê C£»»;î¹çžüÛ¿ufá¡ ýùß’ïmíǦ££#Z[[óojÚ)õ™½oii‰ÎÎN ÃÌÊKŽ;C\&¬À„c®]»6ÿvÏÉÆ§Æé§ŽÔ×KI*ˆ‡ˆ@…}c¹Ã±*íŽø—7 Cåë_ÿzþñÔ_;-bÉï�6 >294$½µÃÁƒÕ% ²<˜³–{\¼·©¿Qx�VGG‡'`˜ /9®'gC\¦'kL¸ädãäÔÔr'Äé§öìÁ÷£§ C)�<òHüà?È¿=w꘸lÚ±÷�Ó&î?kŸ=¼�îsYŠý/ahêñþá""Ùúô¨ÀT/¹‘жû½ˆè9ôÇãã0¼<6Îqc¸ƒË„GÈa`’à2}jêP—‰3«Â)ÈPB[[[ŸÃ±þðüÓ†ìëzæ©qá9G‡‹>ø`´µµix€\.×çñÔb§d¬š®ËG}Ô¶*0€qkº¿îÔ8³jà_çì3ÆÄ§¦­C��Ãð^r\©à2=(`Bÿº»»ã[ßúVap9ç´! .ÓÄt€©&ah÷˜-çÓ3Oí³ÿl.—ó€ ûÉcÙç²û_ÂàÇ­ézœwîà#‘߯;µ ¿ñ�ohd&ÂK2o¤ƒË„ú¦O6N‚ËÁR‰³Ïøá¾5 '£ŽŽŽ‚àrÒc†µo¼éã…æ=÷Üc¥Tàž{î)¸Á÷ù ‡fetÕé=5Ÿ¾©÷è£�jp(£¥¥¥`ÜúéYǾ-XºïÝ»wo´´´hhö¼$ÓF+¸LØJûÊW¾Rp²ñÿuÁiñÛ5cF¼&"ÂÉf¸ö˜-'9@ëù}ïÅÏßµÿ,Tb¨ö¹,Åþ—P¹\.ßþö·óo7üî©ñ»¿9fHê0}À]{{{Ìœ9SßClÌáÇk²¨wpYΤq6ïØÂÄÿö�ïÆÖ]••Ã9çœK—.�êêj(ÔgûÉÆµå•÷â¿¿ø«üÛÿøÇ㳟ý¬? '¼ÎÎθýöÛ VrÝréiÃ\¦½zðp¬m{7~Þ“›FMMM,_¾\½är¹X³fMþíOM;eH/æá¿ŠçööRUUK—.`BJwwwÜqÇù=)Ïÿà˜![�XÛöËø—7�Îo¾ùf Cè4M@VU\FDìëŠø_�ïÓæç•—=w¶;::¢®®ÎŠ“NïàrÒ¸ˆûyÄ÷^z¯ÏÇžÿÁ1Çôù«Ç?¾^¼6í´È(Ï<óLÔÔÔDCCƒ?'ôäëßøF>¸Œè©�ÿç{ïöùØ3«"Ï9}P‡$µ÷_Ÿ{7ÞtÊ1­Œ.öT™U=7#zžH˜>}zÌ™3Ç…“VïUÐçpL|äC÷ÍʼsO‰^ûUþññ{î¹ÇÖ*0D„—TlæÌ™qà 7ô{'¹µµuH¿ï™gžÙזּ¶V€Â ï�þè�Êþ{KKË�×ßüùó=úèìììS?þéáøñOÕçcÿ.÷«øÊ¿ø~íÉS •xôÑG£®®.jjjüq8)=øàƒ[­ÔT�é³ÍѤq=7ãÇ?}/à ?f­“?¼§³³3ZZZbÁ‚þpŒ„— ÈÌ™3û=Ý{¨ÃûYp"ùù wUÈS éÐN6±cÇŽ‚÷my¥øv*+.øJèWŽÿú¿ê÷ã6oÞsæÌqˆ#á%À1ª©©é÷é„¡¼¹×ßS Ó§O·ê*ðf÷{qfÕÀV_¾ðÚ{F�ð`ô÷tÂP†—žJ€ÒjkkËÞLhkk‹7ß|sH¾W¹½Ÿ§OŸnÕ% á%pB)w3!—Ë YxYWWçF ³S4�EÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Iº…� IDATx d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�IÂK “„—@& /€L^™$¼2Ix d’ðÈ$á%�I§i²ª­­-žy晲³wïÞaùÞ«W¯.ûï555ñÙÏ~Ö ` /ɬ|pÔ¾w{{{¿s饗Fmm­?À0ñØ8™5mÚ´Š?ö×N‹8ûŒ1Çôý~ëÌÊ?¶ªª*jjjü‘†‘•—dÖýÑÅêÕ«  ¿ðœS⣵}CÊß®9öþ¦9§Ç«Ç¡_.xÿ«#þû‹¿Ê¿]UUK—.�êêj$€adå%™U]]K—.�sÎ9'ÿ¾çö¾ov÷„•éÿ†ÊÙgŒ)øºU§�‰¿Ëõ .=.0ü„—dZ±óÛ;Ïv¼7ìßûÕƒ‡cmÛ»ñów{Þ\Œ,á%™7¦à`ô /9.Œd€)¸Èá%Ç�‘0ÿÿöî>*ª;Ï÷ýÅ ð¢@´FÒŠÝJeFs[â4šŽæÄMLœÕ® d�îÌ1êŠFWǤÕ)t:Dƒ д¥€$j¸ÀÞTQUˆÊÃÞÅûµÖ¬–]»jÿöoƒ>~¿¿Á%€u^ÂVz3À$¸°ÂKØNo˜—ÖCx [êÉ“àÀš/a[=`\Xá%lífL‚Kk#¼„íÝH€Ip `}„—ˆ×`\Øá%"FwL‚Kû ¼DDé*À$¸°ÂKDœp&Á%€½^""… 0 .ì…ð+T€Ip‰�`ïÞ½*((PUU“°µh¦‘Ì0wîÜ©¯¾úJK–,!¸DDs»Ý*))‘$µ¶¶ê‰'ž`R¶Ex‰ˆçp8”——ÇD ¢577kçÎ�fpiÀÎ/Àæš››•ŸŸ¯êêj&Q,^ž–}Ø‘Ãò•—_}õO :1‚KÿÜùqæ`%8¤£a"„­Ö¼L?yJq>O €%Õ¤¤¨îö”^¿NçàòÖhi©+ZS’£T^ÿ1ذläÈ‘#òx<æ×·FK+³¢5fX“ˆ8„—`×\öÕÚ›ôÂK°�½{÷jÏž=æ×·‹Òʃ3$|Åeuuµòóó™<€m^€Å¹Ýn•””˜_w\&8IºÊ¤"á%XXçàòIQZê _q™#ÝwÇ ËlÜó9ó7Œð,¨¹¹Yùùùª®®6�M;HK]×çO,‰Ð`ƒ˜°–› .ˆ$T^€…TUUÉív—‹¾?XÙãù·&ÀÀCx QUU¥üü|µ´´˜Ç~œ9Xw9 .á%X@çàòÖhé¡ï\6ÂKègGŽÑÎ�;‚Ë•YÑ3,ŠÉ h„—Ð�Nž<)�Çc~Mp @ú ùoÌ#IwÜEp @;*/ Í™3GUUU*))‘$�¨mÕoŽ]Ñ�3+f!fO¹xñ¢Þxã�.Ï1b„²³³år¹zäš•••fUísÏ=gw»Ý:{ö¬rss•šš*I*..Ö¡C‡4nÜ8åååñÀ á%ô3#¬ò0·6]ÑʬhÌâõzõüóÏwûy¼õÖ[=vÍÌÌÌ€ðò±Ç“$åææšÇŠŠŠôÆoœ ¼KÈËËSbb¢öîÝ+Iú²QÚz„³§K’233µeË–€×*++Íÿóx|¸YuénOU}@¤ ¼‹X°`�ÍVã/¥^aŸàú‡‰�¥¦¦ê…^�Çã¹éðÒ*ýÉp!¥q. ö€…dee´}¥­óÜ¥V&ç&”––JR—Á¥ÿë•••7}ÍC‡I ^^k€6„—`1YYYZ¿~½bbb$u˜'j 0o„Ñ.]»-ûâÅ‹a_óx<ºóÎ;¥¨¨(�?^�=öXÈ÷!eçk!ªÿ±¢¢"Íž=[«V­ øŒÙ³gkáÂ…’Ú*3-Zd^{öìÙ×è|¿�=ö˜Æ�oŽó…^�Ô¶YÐìÙ³åv»ùÆ` ´�€9�N­Y³FùùùjiiÑ×WdîB~—³ëw:w©UÿYgÿ ó«¦ïzäs‰¡I’|ðAóØÅ‹ÂÂììl�1B^¯Wn·[ÅÅÅúôÓO5bĈ kfgg‡‹åeQQ‘Š‹‹Î-..6�½ñÆZµj•†®ììlUVVšaf¨ëΞ=[/^Ô¸qãÌóŸþyUVVêÂ… *..fc ¶Ax e˜;vìPCCƒ$é�Ò«’6Àl¹Üªýã&ÏO¸ ±3·Ûm®7jTŒÊÆ¢¢"³-<œ'Ÿ|R[¶l1¿~ã�7$) ˜ô—ššªáÇëoû[@dW›õ\O5æ“O>²JÒøüÔÔTósŠ‹‹5|øð àÒàr¹töìYª.Ø á%Ø€`îܹS%%%’Ú̯¯´­ƒ3$*è=‰‰‰Z°`Á€ž7¯×kn¨®m|Ö¬YJMM5ÿ×ÿ½•••>|x@yg.—+ ¢Ó$‡ðy]µ‡‡«Æ D¯ù¾Ê.\¸ðšU•ìtÀN/À&‡ÙÎl˜'j[µµéŠVfE‡ 0:ÿ Ï»+\›wg�« C’/^ ¹ãy¨køWcú‡Ÿ�ïéZU��mæT^°“ALØK^^žî¹çóë/¥­G®¨år+“ÓIwB½pŒŠÍ®*CUY†ZïÒ8–™™yÍÍz®š†ú|C¸÷\¼x±Ë÷€U^€ -Y²D¹¹¹æ×_6JÿúÇ+:w‰¹ñ×��b†bì0îßVn’ת°ô¯Æ t†s¨õ4;_§3cÏÌÌL¾)Ø á%ØTVVV@€ÙÐ"íþËU&Æ�ôÝHxi„ƒgÏž `ËãñH’ÙÎ/u§qÿjL¡ÂO¡ªI;oÔù|cã!ZÆØ á%ØXVV–žzê)ÅÄÄ0�!Ÿtc¡ÝÂ… 5nÜ8UVVjõêÕ¯kÑ¢E’¤çž{Î ¯µ{ø�Vcv~Oçõ0�𴸸X�=ö˜*++UYY©^xA³gÏ6ÃWÂKvCx 67qâD­Y³†³“pAâõ(**ÒðáÃåv»5~üxÍž=[wÞy§>÷Üszþù烮éVVVšááµv7Þ®½;Üž©©©zë­·$ÉëøñãõüóÏTç²Þ%»!¼€àt:µfÍ%$$0~²³³Ö£¼^.—K^¯WO>ù¤¹cykk«rssõé§Ÿ—á®yñâEegg„ˆþçúŠÆ¹]�9Üëyyyúâ‹/ôÜsÏ);;[O>ù¤þð‡?´´Sy Àn¢™ˆ N§S6lP~~¾êëë•••5 çcÕªUZµjÕMNjjª¹á͵„ 3].WÈ�tŠŠŠ‚Žååå„�ݹ'ÿ]ÑSSSƒÆ`|ýàƒòCÀv¨¼€âp8´aÃ-[¶L%%%:rä“á¼^¯FŽ©;ï¼3äëÆ¦B7S� ý…ÊKˆ0GŽ1«S§NÉårÉáp01ÊØ¸ÇØUܨ°,--Õ–-[TYY©ÌÌÌ.+:Àª¨¼€â\ªªª˜˜–ššªçž{NR[[¹±YÏÂ… U\\¬ÌÌÌ�më`T^@„عs§<ÈD @Ï?ÿ¼òòòäv»UYY©ÊÊJÍš5K.—‹vq¶Fx Àív«¤¤„‰ÀBmÖvGx 6ÖÜÜ,�Ç#¯×k›>v�Î]jÕ——Z™ €­±æ%ØTss³òóóƒ‚Ë¥®ÁЉ&¸Ø•—`CõõõÚ±c‡ª««Íc÷Ý1Hó'frƒðl¦ªªJùùùjii1�ý8s°îrRLˆ,„—`#7\¾þúëLÀ–/À&Ž9¢�;wšÁå­ÑÒʬh�Åä⸅¿`ô€ 9rD�§ÛÁåÔ”Áº•ž¬Û‡EiJ2ÿ™ûãW[°¸½{÷jÏž=æ×·‹ÒO¦G+!&ü{²ÇRöx‚ €½^€…¹Ýn•””˜_ß>,J+g VÌæù/À‚š››µsç΀àòIQZ꬘!¬c/Àbš››•ŸŸ¯êêjóØô±ƒ´Ô5˜É (,ˆBp @*/À"ªªªTPP úúzóØ�3ë.'ÿΘ/Àªªª”ŸŸ¯––óÁ%` ã·b°¯×\.ú>Á%üf àr¹c~½û/WõIÕwL `@#¼ p:�Z³fM@€ùN)&``#¼‹p:�zå•W4vìXóØ;¥Wµû/W™À€Dx âp8´fÍš€óÐßémï&0à^€ÅæŒ3ÌcG«[õ›cWÔr¹•  „—`A‡Cyyyæ‰ÚVm=B€ 8/ÀÂòòòtÏ=÷˜_Ù(LÀ€ÍØCBzº†ÄÆvëܺÒÒ^Çå¦&5”—_óøÐøx�˜0A’tñÌ}ÓØÈîӒ%Kät:åñx$µ˜/¼¢•YÑ3,*ä{}ñ�ö�¼ª¯Y*`c„—61må %efvûüªÃ‡õù®wUëõöÊ8êJKõ»'W]óøÈ´4Ý»e³$é£U«{|<èyCãã5%'GǶog2,$++K’Ìóë+m˜áÌã5—z†Ãá`Ðoh�PΙ3uï–ÍrΜÉd Û’].=øÎÛÊxd1“aAYYYZ¿~½bbb$u˜ŸT}Çäè r¹\Lú •—6ôï³f‡þ#=]·ÄÅiÒ#‹å¼ûnIÒŒ§¦¿––ö[»¶¯®N¥n�ùgXÛh—KCãã™ s:�Z³f�òóóÕÒÒ¢¯¯Hï”^•$Ýå ýïQO=õ”&NœÈäl‡ÊËÒP^®Z¯WÅ?_¯ª�?–ÔÖõêó÷ޣ¶9­Y³F;vìÐéÓ§%IG«[%`"áe„êjÝ¡ññÊ~éE%…X€?!=] +Vh¼y*ÙôZ@Àu#n‰‹3¯sK\\Èã ééš»ùõ 1'¤§+kÝÓ�™©’×^ 9ÖPïKr¹”är©|ß~Åßž¢¤ÌLÕzK¯kÜ ééÊ~é%34ì<Æ5Â�Í9s¦f<ý³ ±9gΔsæL•¼ö óþC�-}þ|ÍxúgAÇãR’•ñÈbM˜wŸþ¸acÐî펤¤¶Ï�ŠÒÔ¼m ò’îtijnnÈ�­»Œàò[ŸOe»ÞÕ_½^Õz½íU�)𒛣¸ää�cKHO׬—_’$}ëóéÏÛw¨¡½*ΘßPÁ¤Á?¸ôÕÖêØ¶íºPQ¡Ë>Ÿœ3gš×¾wËfýÇO~2`1a‚’23å«­Õ O¡Y 9væLe,~XRÛfNFxyæÃõW¯WæÝ§´ymóöѪÕmchßh)}þ|sN�gUëõ*.%EÉ.—ù¼þþ‰eª:|¸ß6‰¨òòò$É 0ˆ„—(ÙåÒ”œóëÏßíhOŸ?ß .KÝw»Í×ÊËÕP^®æº:³jð–?¡#›^ëÕñ��áÚÃÂEÿóIÒØ™w„SssÌà²ó{�æ{·l¨öì®ôùóÍŠËã�Ê~»Ë|­Ö땼^5œ>­ÿò?~-I‘–ð~£þ[ŸO­Z4¶êÇÍ÷†š‹¿b™$éBy…>Z½: ,ß·OU‡ëþ_¿©¸ädM[±\¿k;N¨÷×z½úÖçSf^®†ÆÇ+ÙåR­×+_M�|55íW}Ù¹ªÓÄ/”W|OøjjT^S£o5ëå—44>^£33Í`}'//OßûÞ÷äñx˜ @D ¼´¡Î­À’äHJV|J²b“’Z�Ëv½°áć’ÔVÑç\ú+ß·O£3§*mÞ<¥Í›§?oßÑ«UtU²zÐWS£ 噞B��7«Ëv½ò½ åå:îñhÚòå×=žoUWZÚVué\vþüo}>ݧ„;ÒÍãq))f»õq�'ìØÊv½kV@ú›0ホjÒmÛBÎû7��:á)ÔŒ§¦$—K éé!¯Sêv‡|õáÃæ÷Ðèöðòz´*tKrÕáÃ:¶}».œ.7«5Ñ÷²²²$) Àt:�L À–/mhj{{èµ”ízWǶm3¿ŽKI1«.+Ú۳é:ü±öv]ç¶vß6ù‚Ž�ÎÌ4ÿ\ÝŸÎìÿð†Â˪ǯy¿Cããå«­5çÓàœywÀ†Ûþ�á¥|~ëóu*ú¿–t§+dxù×ÒÐë|Þè:¦¾š%ef*!=]?Ú²Ye¿Ý¥¿?�† {Ñ·²²²”˜˜¨={ö(++K‡ƒIØᥠՕ†ß|ÆWS£ ª:üqЮÏqIIæŸ/\#Àò¾F¦§÷jxÙ|�Uz#ýªMÊÌT\JŠâ’“42=]#ÓÒCnä#ICbãžE8áÄØögÔÚÚjîö}-þ×ì�û7÷Ê9s¦¹Ù’´Öy½ª:|XuÞÒ›Þà =gâĉr:�ª®®f2¶ExiC¿{rÕMÆeŸ¯Ë×ûr³•®B¾›õMcc—;¯‡—’¢)99a7ýñÕÖê–¸¸ 55ý[Èo„QÉiìhÞÉwºtÜÝ7Ïé?~ú¸¦­X.çݦþAfCy¹þ¼mûu·¢£ç�Ÿ.TT¨öS¯šëêÔpú´ÊËõ£7¶(ɯ…]jk�÷önÔ…òŠ€–ÿ®|{� º'ùjjTüóõ/çÌ™�9Õ¬Æ4æîÞ-›U¼~öô£½{÷jÏž=æ×UUUL À–/ÿ�käé]VÇ%ûUý5[lóÿñ$w±áÌ�—’4õ±¼ŽMs¶o»Žcì褠c—ýÖèŒKI [UÚy­LC]i©’23õm“ÏÒÕ‹ß46ª|ß¾öà_SBzº&-~Ø\'uÚòå„—ýñ³Ñܬ‚‚�:uŠÉD„ALÁÀaì�-éš-É£ý^o8}ÚR÷áê�îâ>œ3gÞÐç•“U6¸rÝ˺O;Æ–ÜÅØ’î ýšv&efv¾Æ¥¤hÚòåÊÌË „ö¤¡ññÊXü°¦-_ò¾ÊËudÓkªúøãöñ%ó×Ǫªªôì³Ï\" áåcTÃ9ï¾;lè54>^“~HRÛÚŽVÛ„ÅWSc†d“~Hq))!ïaJNÎM]çÛ.ÖýœøðÃ!�7”—ëBy…$iJNNȲ«±�ñÛ~JnnØëOÍÍQÆ#‹»½óüÍú¦±QÓV¬PÆ#‹5aÞ}74gè=ÔË/¿¬––óØ’¢˜€í^0Ç=…fõåÜͯUÑÅ¥¤¬÷xlÛvkÞÇ[nImAàýoþJióæih||ÛZŒwß­ûß|ó†«ÿ|µµ’¤±aÞŒÅ+3/|°h¬U—’¬ßy[λïۃVª²Öë5w“7*;Ÿ›áמ]WÚ{;|•«Æõ+öï—$¥Í›§ŒÅ‡<ß—q.z—Ñ&¾sçNóØ­ÑÒŠƒ5k½ì�5/_M�þ¼}‡þaù¯{·lVCy¹¹û¸;y©ÛcÙu ÊËUòÚ/4ãéŸih|¼²Ö=tNÅþýf˜v=ŽmÛ®Y/¿¤¡ññú/ÿãת:|XÊËåHJV²Ë¥¸”dùjku¡¢BλïZû²Öë Û¬�&vòøIDATy¹Ë±]è>Z¿A÷nÞ¬‘éiÊxd±2Y¬ºöVùؤd3”½P^¡Cë7ôè¼ú�eÖË/I’Êv½«cÛ¶éÏÛw˜›óL[±B“^¬¦º¶ wH\œôújkõçí;øaëeUUU*((P}}½yìöaQZ9c°b†D©¼þ;& `{„—Pù¾}j8}ZÓV®PRffPuá…ò •ºÝ–ßpŸ�I‹ØñÚü7^V>¬cÛ·kjn®n‰‹“sæL³ ñ[ŸO¥n�N¾ûnÛñ»ïV\JrÐÆAåûöÉWS£I�,Ø}Üqcl—;íþMc£>Z½Zÿ°ü óœÎk”Vì߯?oß¡oz¸M»êðá Ð7áŽts\ÿñÓÇ5mÅró¾;W·Vì߯ãžÂÛ±mÛ52-MqÉÉšš—§©yy!Ï+yí–ß ýçzÛÄÃiûåˆ_�öDx [ú¦±Q{òe<²Xi÷ÍÓÈôŽõ-¿õùTuø°Îìÿ°_ÖdôÕÔ胟>®‰?¬¿›93äØŽ{ iGXì&@~†­•ývWŸWVvÇ7��:îvë¸ÛÍCB·Ýl›8‘†ð, §ÚĈ$„—ÐÏh 4~3€~äõz‚KÚÄè0ˆ)`E„—Ð�\.—rssͯ¿¾"ýæØíþËU&0à^@?ËÊÊÒúõë•��`;ôÅwúÅ¡Ëjha~á%X€ÓéÔ† ”™™iû²QúÅ¡Ë:QÛÊ$ÂK°‡Ã¡'žxB�>ú¨yŒ6rÀ@Fx 3gÎÚÈ�Í€õmän·[¥¥¥’:ÚÈ—º¢5%9ª[ŸsîR«Z.3ŸD‚s—˜ÀÀCx e´‘vìX­]»– Ømã`3ìF(/À†Œ6òÌÌLó˜ÑF~¢¶• DÂK°)£�üÑG5�mäûO}Çl�ðl.Tyy=Õ—û#¼€ª�»#¼€ª�;#¼€Ó¹�ü¶ÛncR¶Í@ä1ÚÈëëëåp8˜€-^@„ª¯¯WAA�êëë•››«¬¬,&`+„—�öîÝ«={ö˜_9r„ð`;„—Aªªªäv»U]]Ídl�ð"DçjKìŽðl®¾¾^;v쨶¼5Zš’Û!¼ðz½r»ÝÕ–#c¤ŸL�[m™à$é*“„3$ŠI,Žð,,\µå?¦FiþÄÁ]þÇVBŒô_§E«úoì8t–èˆ [À:/À¢ÂU[.u Vzâ n}Æ”ä(MIÌdl‰ð,¦¹¹Y{÷îÕÁƒŽw§Ú€HBx ròäIy<Õ××›Çn�––º¢5%™Ð0°^€„«¶üAR”–º¨¶ L„—ÐϪªªTPP@µ%�^@?:yò¤^ýõ€cT[ÐfSý§ºº:èØ¬ ƒ.á%ô«9sæhÆŒǶ•\Õî¿\UËåV&0 ^@?ËËËÓ²eËc;ôÅwú×?^QyýwL`À"¼ p¹\zå•W”™™ikh¡ 0°^€E8=ñÄ!«0·¹¢s—0 á%XŒQ…yÇw˜Ç¾l”þõ�WôáiÚÈG4SÖãp8´víZ-©£ sþÄÁºïŽ® è_ü¿GÅÄÄhìØ±L"ÐC‡²²²˜ —^€�8�NmذA{öìÑÞ½{%I__‘vÿåªNÔ~§»¢•ü¾±cÇjíÚµL ÀVhZ°`�Ö¯_PMiTa~RÅŽä€È@x 6eTa>ðÀ汯¯Hï”^ÕoŽ]QËå(& `k´�€Í-X°À\ ³¡¡A’t¢¶U'j¯09[³UxYŸ˜¨¦añ<5–Ô×oמ8q¢¹æïÿ{ "Ø*¼¼p["O Âp8Z²d‰\.W@&veù5/ï¼óNžÛq8ývm£ sÆŒæ±Ûn»�‡°�¨ÖÖÖV+°¹¹Y^¯Wõõõ<-¶�˜˜(—ËÕ¯¦áäÉ“ª®®–ËåRb"Õë{±|x ``İ"ÂK–Dx À’/Xá%K"¼`IÑLì ¹¹Y{÷îUUUUŸ^711QsæÌ‘Óéä!ô1ÂKØ‚×ëÕÁƒûíúyyy<€>FÛ8l¡¾¾¾ß®ýÕW_ñú•—°�¿›œ¬¿›œÒ«×øÛWM:Q|ŠÉèG„—°�˜a·ê6çH& ÂÑ6À’/Xá%K"¼`I„—,‰ð€%^°$ÂK–Dx À’/Xá%K"¼`I„—,‰ð€%^°$ÂK–Dx À’/Xá%K"¼`I„—,‰ð€%^°$ÂK–Dx À’/Xá%K"¼`I„—,‰ð€%^°$ÂK–Dx À’/Xá%KŠf Ð×êëëU__Ýïé/---:uêÔu½'11Q‰‰‰åõzUPP`«1WWW+??ÿºß÷ÀhÁ‚Îá£â4Á5V;ߨ­ó›/}­–Æo̯cbb´dÉ8ÀMˆjmmmeÐ—š››•ŸŸ`þý�2ôwßO±åýüí¼Oÿ{çÿÑ•o¯Hj .׬Y#§ÓÉø ´�£Ï9­Y³FcÇŽ5�ýŸß•éÿûK�íî…à ÷^¢_DB€Ip л/Ñoì`\ô>ÂKô+;˜—}ƒðýÎN&Á%@ß!¼„%Ø!À$¸è[„—° +˜—}�ð–bÅ“à ^Âr¬`\ôÂKX’L‚K€þEx ËêÏ“à ÿ^ÂÒú#À$¸°ÂKX^_˜—ÖAx [è‹“àÀZ/a½`\Xá%l¥7L‚Kk"¼„íôd€Ip `]„—°¥ž0 .¬�ð¶u3&Á%€õ^ÂÖn$À$¸°ÂKØÞõ˜—öAx‰ˆÐ�“àÀ^¢Z[[[™DŠææfåç竺ºÚ<ö÷?ÊÐðÑñ—6Cx‰ˆ*ÀŒ¾%šàÀf/‘B˜Á%€�°æ%"R¨50 .ì…ÊKD4£³¾¾žàÀf/Xmã,‰ð€%^°$ÂK–Dx À’/XRt¨ƒ/^Tii)³ OdffjĈÇ‚ÂËÊÊJ�?žÙЧ¾øâ ¥¦¦š_µ�WVV2Kú\çl2º«“cãš0)•YÐ+Î|^©¦Ææ�¯u^N˜”ª× 72ƒzÅÓ9/êÄÑÏB¾Ænã,‰ð€%^°$ÂK–Dx À’/XR4S¨¢¬Rÿy´L£ÇŒÒèÛoSZF*“ôƒ^¯¼|:çEÝŸñOzlîÊkžû«W=º?ãŸôâŠ_öȵëÎ�×ÛÛv_ñÐ:ÝŸñO:þÉgÇ_¶@+Z§_½êÑK+~©•­ëòü¾òöö]ò]j <¶m—îÏø'½þL�復Œg^äù€Ÿ0ܰ^/¿8yVR[�øÑîC]ž{¦¬íÜ©Ó'ßôuKÕʇÖÉ×úù.5éLY¥$i¤qçh߃9óµtùb=˜3?ìù}¡îÜy=6w¥Š<(nXlÀkŸ÷Ϙ®ç™O¹k2?a¸a½Ú6^wî¼|—šïPSc³Þÿ÷}ºwQvØóOm«lœÐ­ÚE…ûä»Ô¤ “‚?k“g£$‚ÇÛ¯=wa¶þù™\ó¸ïRSÈóûÂñO>Sݹóš"Ì]º|±æÜo¹ðÒxæ’h¹ÀMéÕðÒh³6ÄG?ÓñO>ÓÔyíÕ�’B¾~½Œê¿Îá^ܰØ�ŸoT}v® w~_8Ñ>¡®oÕ`ÐxæS¦Su €›Ó«áå™öÖæ¤1£4å®É:qô3~2Œ;a�¡+ +Ê*õ§ßSݹóŠ‹�UÒ˜Qš³ðƒª!Oý, ú¯Ù׬G?3Ã´Š²J5ûš5~â8Å ‹5«=�ÿ�ŠjûóèÛG)į óýù.5é`ÑåklRݹóJ3Jsf+į�÷êü)Ó'ÍGݹóúë—çu¦=€Uû˜Œ1¯;â!CÌ’ƒÇtæóJ�ùü¬&LgŽ+ܼúߟÿ<'�¥ÿïù×Uqj<ó ãÌû­ø¼2ì½óòÅɳaï§«û­;w^Š©éR³|�M×|°�Þ /Ë:ªgÌ™¦7ãúÓÁŽ`,àÜÏ�sSŽû.5éå•ù!7Ëy{û.­þ—ÿ¦s§›ÇžÎy1àœ§s^Ô„Iã´m÷k’¤—Wæ«îÜymòlÔ„Iã‚Î76ÀÙ°u�’ÆŒ 8ß?xûh÷!ýzSaÐF:ïîÓO×åµÇ¿ùj¡Š Co`3õ®ÉZ¿u�þêU�þtðXÇ}nÛ¥·%m}o“â†ÅêÀîCz{û.=˜3? Ì«(«4Çk(9pTR[ý&÷†  òÍW uâègÚúÞ&½³ýÝ 1¾_¸OÿÏGÿ½Û¦ñÌ%iåCëÆ"I�¯ËÑÂÜûŽØ}Hon*Ô”é“õZáÆ Ï,*ü@ïî ºß�vÒægƒ7,z{Û®�×€½ôê†=þkXÆ ‹ÕŒ9m!c¨ÀÏ„hóö]jÒº¼—tü“Ï4aÒ8mòlÔeÿKoØjn¦³ùçÿfdÆú”?œ3MRÛú•›<µú•eæëƹF¹É³QæÌ7¯½É³Q›<5å®É!Ï—:B³ÖÖV-]¾X[ßÛ¤­ïmÒÜ…Ùò]jÒ¯7„vooÛ¥¢ÂïÐêW–ië{›ôÖ�­Ú°u�&L§ãŸ|¦×ýB¸¥Ëëñu9’¤Ñ·�2Çdw�Ûñ¥¶àò™¼—Twî¼~8gšÞ:°U”ý/m}o“&L§3e•Z—÷RØg´ùçÿ¦#ŽjÃÖ5æýL˜ÔV=ù~á¾ë~æïîÓøIãÌ{]º|±$éÍM…AA´\‡kÏïÁ;î·îÜym~¶@±ñóûbçÿûsÞB]öÒkáe¨5,��ðO¿?T±hìèí¿æäÛÛwéLY¥*Ÿ“4f”þù™\ýpÎ4ù.5™a¨±>eÓ¥fIÒŒ9Ó4õ®Éfè×¹5=øüéšzW[ksܰØ�­ìuçÎë×› %IO½²LKW,VZFªÒ2RõÔ«ËÌÀÏÎêÎ�×ÛÛÛÆ·É³Q÷.ÊVZFª’ƌҌ¹Óõxûæ@ÿy´Ì¼†uᄌqæ˜ f(ì7®Í?ÿ7ù.5éÁœùÚ¸m­YÙš–‘ªMž�Š�wèLY¥Y‰))0ÜkmÕ¶Ý›4cîtó~Œ°¹»! ÿ3Ÿ»0[·­5ïuéŠÅfëú�N»ÎŸ¹ÆîäÆýúÏ�ñ sî7�Ç ‹ÕÂÜûÍï³’ƒGù °±^ /Ci©š2}rP5Ÿ8fwuçΛçlض6dÛòœûÍs®f×r£Â¯sHv&Ìæ>¡Î/*ü@¾KMš2}r@»ºáñgrµtù‶øMž�ÚúÞ¦.7ÙqÄ9B^;­S½ÆÆw¬ÿøÑîC:SV©Ñ·� Ø)Ýà_õj|®Ô‡›c_c[ÀÜÝ ‹ŒÏŽ�wèñgr‚^ŸÑ^ëÿ¼|—šÌq„ZïÔÿ~C­cYÒ¾>§¿¹ ³µaëš°ë|ÀzmÍËpkXÎ]”­G?Ó�¢CZº¢­•جºôÛ¡Ú¨üáœi×Ü|Å+¥Ž 4TتÝÚÿú�ÃÎPçkQ.l¯îë¬s•dÒ˜Qæ8ŒÍq¤¶¯îÜy3äí´N?œ3Í ��ÊQØ[ï…—aªï]”­··íRݹó*9pT3æNï¨2:Î=ÞÞu®< Å?ôì¨â ~ß!Æd”Æîâ]�_QViVùMéf5¢ïR“ÞÜT¨? n•—ÚBV)°ºÑ­ÍàjÐÊ óÃ�ãhl ûŒŒŠÈÎBµ§w癇«xl qÿf…nFèk¯w®þLÓ¶¨±áÐ�݇t`÷!Í]”Tù {êµ¶ñpÕŒRG…àGEmƒAgǹÆ:”]rF°;¬£å:Üæ/uçΛá¡U^Ç8Ç]óü¦Æ¶1…«ìÌØpèÀîCrÄ9ô`Î|­~e™6y6šêŸãŸՆギ㿃»q CWÕ†'Ú×Ô4ÂRÿ{5ÇþëWv·ŠÑø¼pç�´°Xa„±Ó»^ï2T€š–‘ª× Û7Xjÿ�݇Bîrûé•ÊKÿ5,C­—8wQ¶Þ޾ˬFìjÍî”ü¾­…ÛXÏQ ¿ùK¨Öt)üÚ’áΗ¶ª¯¢¬R_œ<«ñÇ)-#U%�™kQnÛ½)(ˆ4ZÇ;ßû‰0ííRðzžÝ éêÎ�7ïÇX§Ó¿â´«ví)Ó'_÷3Çh¹÷6_˜U·©!?³s¥«ïR“¾8yVޏŽ5?§Þ5YS 7êø'Ÿé¥¿”ïR“~õªG·­å'ÀÆz¥òÒßÂ_þȼùjÛÎÝþЄú¬ÎJÕ™²JÅÆ;ڞáÇ�†nO×rî|©£�¼³_o*ÔëϘ!�Ñâ}o˜5 ‹ ?� ÕˆA÷¢ÅÝΙo¶ïŽ>eúdó}×j×ÕžÞÿÍBµÇ´û�êÎ�Wl¼C÷.Ês¨àú�ö]ÚýççÌçgõt΋z&怒ó§Þ5YO½²LRGå.ì«WÂK3|Ë_IilÖó§öêÉÎU†F y èPPVQV©Í?ÿ·¶ÏY¾Ø ¶ü«ÿµ[û·'w¹¶dˆó§Þ5Y±ñù.5U~´û�Žò™bãÛÚÃ%).¾=p v~´û�¹›zÐZ›aªõ¸!±Rv¾Ž±ùÑS¯. zFáÖíÜž~-Çý6MòßIÞx^¿n›.óÎëϘ­ëþ÷ëß.ªÚÓÜñ|˜ƒŸn›ë•¶ñ/Âìí/iÌ(M™>Ù¬|ì\á7wQ¶Ò™ÏÏjåCë´0ç~Mȧ’ÇÌ@sîÂl-̽?äç��?“#)¸ÝZêzmÉPçKÒœûõöö]zye¾–._¬ÑcF©äàQØ}H±ñmòl4?ë‡s¦éíí»Trà¨^¶@3æLWSc“JSÉ�£š0iœÎ|~6(h3Õ÷ÿ}ŸNýL?¼gšÒ2Rö¸?þLŽÖ徨’GõÒÊ|͘3M±ñ±z¿ð3P}ü™Ü€�ÔxFáÖ wÿ×zæ«_Y¦ÍÏèÌɳš0iœš.5PtH¿zÕc¾6úöQZý/ÿ- 3>ÃD‹ ?Ø%\jkM÷ð­-î|©­bÔר¤÷ ÷ŒiÊôÉZºbq@wZFªV¿²Lo¾ê1wÃ6ƹɳQuçÎkó³A-ÎK—/6CÏ’GõÃ{ÚB½pëy¦e¤j“g£6?[`¾Ç\�?“0.ÿgª-¼«Ø»zæ&�Ó½‹²uæóJ½_¸ÏGl¼C�¯Ë 4oòlÔºÜu¦¬Ò¬,�»0[�?“£×Ÿ-y¿ÿüL®ââcõÑîC**üÀl¿w¿°§¨ÖÖÖVÿÅÅÅš={¶¤¶ èµÂ�ý>Hÿ�m­�鯢¬RM�ÍJÓ½ðíFø.5u´(_cL×sn¨{îþ÷.©Wï¿'Çï??×;f«Ü/nÌÓ9/šŽøÃ4kÖ,óµh;ÜÀõ†R}Qu7,¶Û¡âõœ{£÷Ü—÷ÞÓã¿‘ù±Úý ç b Xá%K"¼`I„—,‰ð€%u¹Ûx“¯ÙܦzZ“¯9ìk]†—gÊ*õtÎ‹Ì €>GÛ8KŠjmmmí|pÕªUòz½Ì€>ár¹´eË–€c!ÃKèo´�°$ÂK–Dx À’þè QZIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2493567 ceilometer-27.0.0.0rc1/doc/source/contributor/2-accessmodel.png0000664000175000017500000012576615250267452023115 0ustar00zuulzuul‰PNG  IHDR3‡­T‰zTXtRaw profile type exifxÚUŽË Ã@DïT‘øí°”Y¶”R~ ëÈÎ;Àh´z,íï×A�FXÉGL$À…§§>+L^³(Kïš‹s›TÒ«&Ó�3د‡~ö?†aâ�ÀÀ†MË®»‰�ž%j+÷7òvÍ¾Ê ÿ}�’»œ>B ,(.AJ iTXtXML:com.adobe.xmp ΓÖ[sBIT|dˆ IDATxÚìÝwXWÃð³ÀÒ‹ôލ¨X¢ X± ±¢&Ɔ%[,XbMvMÞ˜˜˜(–ÄX¢±%Ø %¨`7*b£I �¥|ÀîÇ‚K“]<¿çñgfïÌÜÖ�³· @DDDDDDD¤$TXDDDDDDD¤Lf‘Ra˜ADDDDDDDJ…a)5VUÕÞ½{qæÌVQ6|øpôêÕ‹AD �-3ˆˆ¨Ê¢££Y DDuÜõë×Y D¤0Ø2ƒˆˆª�0;F/^°"ˆˆê�ccˆ44XD¤PfQµfgÃòy<+‚ˆ¨IÕÕe˜AD §Ì0ƒýŸ‰ê>kkk,]º”ADDDDDJ¥Ì13dÕ}±±±¬"""""R:oífÒÐÂVV¬)¢:$2.�ãÙ€ˆˆˆˆˆ”Ó[à ++ôsqaMÕ!§ÂÂf‘ÒâÔ¬DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘Ra˜ADDDDDDDJ…a)†DDDDDDD¤Tf‘RQcÕéé騱cà³Ï>ƒŽŽ+…ˆˆˆˆê¶Ì "ªC¢££qóæMܼyßÿ=ÒÓÓY)DDDDTç0Ì "ª£bcchQ�Än&DDu˜8И?>»œP¹**°ïÙv]»Âر)4 êA 2SR�|ÿ>ž"&$¤Úö7öìÀ.·r-§êå¶jl»¸âyx8‚æ/�ûz•T�Ÿ�œÔT¼|ô�ñèôi  @îëMDDTYl™ADTGšZ` z;};; Úöº,^»îÝ cn5M ¨jh@×Òö=z ÇšÕèõýZ¨ëë³Â*ÀqÈ'eµEÓÐÖ;"_$‚e›6Ð17¯tYhÀ¢Mtþú+¸­Z �ª*/<Õ8…k™¡ëâRáפ……ñJ–¨?Ö up‚È»Wù߶Р2éÛØ ß†_ ¡§‡ô„Dü·o/b/_AFR**0tp€ã�OPßÍ –mÛ¢×ÿ¾Å©™³�Ÿ›[¥ý¾/ßÐ5i¢pÇÔ¨o_¨¨©âÖÎÝh5nõí‹[;wV꺩…Ð13C}77|0öSغºÂqÈÜ;p€¿\DDT£Þë–›öí«TxBD¤,º» ‡æí°…ÉÖeñ"hèé!þÚ5�0÷ÿöGjl,òrr�›•…¤;wpÞg%.øú¢ /ÆŽŽh6b8+NNÆMš*Ü15êß™/Rp{ç.d½z…†ýúVº¬|‘©±±¸³{7Â~Ùhا7/<Õ8…3ã]´,¸~ïï"ªóº» l¡A¥XwècGGd½|‰—¯€(#£ÌmŸÿGGÔkЩ±±2·1mÞÍFŒ€Ù@Ã@9©©Hºûî<ˆ„7¤¶­è –mÛÂqè˜4ou]]ˆÒÓñâÁD9‚è Km/ÔÑÁÈãÇð:* GÆ}††½{£Å¨Qе²BzBþÛ»‘'O @óáàke…Ì”D?ŽÛ»þ¨Ò9¶= m¦L)u¾ç}Vâé?ÿTªÎÔuuáqì(^?}Š£“&ÃeÆ Ø÷pƒš¦&þìï.W=š¶hƒúõqÿ¯¿�Ÿ›‹§ÁÁp:æNN¥öWQQçÏ£ã‚ùзµã/½¿aÆ»À0ƒˆh0ÐxŸÙuïˆ8r9oÞ¼uû° Ë\ר_?tZ¸@j¼MCCØvq…­kg\Yÿ3øûWê8[Œ‰6Ó¦J-Ó00€Uûö°jßwvïÆõ­¿J­ÏËÎ.ü £¡ û=àºÈ[²Î ¾:}õ%2’“aذ!ÚLŸ&Y§ka§I“�ý&‡×è9V´¼Ü¢sRÕÐ@ 8ù¤ÂuéàÞð8 ðèôi8 ‡þý«fˆÏ£ ?�¿\DDTãêD7“ôÌL8 ]l)£�¦ßÞ½Ðuq�óСX³u+t]\ðߣG…\\\ ë₃§OK½æÊíÛûÕWhÔ·/ ;vDƒ>}à1o·‡—*ÿUj*t]\à2brss1ÿÿCƒ>}`Þµ+àMZšd=œÇ@OOØöì SWWt;ûо!KzùÞ?þ—áÃaÖ¥ ;v„〘¼t)î=~\-u÷ïÕ«1oôé£N�Ðjð`|óë¯È*úÀTҙ˗1|î\Ø÷î ÃŽa߫Ϛ…£gÏÊܾ2ç}ùÖ-Œóò‚ã€0êÔ Önnè0r$|7oFÒË—2÷#¾†e)¾^|LÎC‡öž8�ö0uuE›¡C±ëÈÉë~÷÷‡Ëˆ0îÜÍ ·¿ý†£´Wô~‘çž!ª‰@ƒ]N¨8Ó- ï‡Ë—«TŽžµ:Ì› ¸³{7�ù{úô…ÿ¨Ñ¸¾õWäçåÁeÆ èÛÚV¸lÃF�àüùd  wöü‰ÃãÆaOß~84z ®oýùùh9z4L›7—z�xL5m-´ž4¡ßÿ€?ûõÃ߈ |0n,š�‰ «VaO_éu ûö©Ò9ÞÝó§T«“]n=°Ë­‡¤UFeêLrNZZhòñÇùn-þì×_îVjš¨ïæ†×OŸâŃ€”ˆ‡xùø1ìºwƒP[»J÷�­«+àeä#þrà yèhiá·•+!TSÃ’õëñ$&Fj}dT–ýò „jjضjԅ·–ùÇÑ£è=y2üƒƒ‘ðâD¹¹HJIÁñsçà>m6ïÛ'µ½–† 3+ ?îÚ…Íû÷#)%陙͢õYÙÙ8sù2>š1g¯\ÁË7o�™��ë÷îaÒ’%ðÄÄÇÃu̬ÿãÜòYYåæ"&!{Oœ@·±c~÷n•êmÞ=èé‰çÎ!)%9"ÇÄ`Õ¦Mè=yr©@ãÇ�;1hÆ œ<É/_B”›‹äW¯‚Q `ù† ¥öQÑó>Œ>ŸŽ¿“�€‘¯ÓÒp72k¶nEçÑ£›�P¥óÖ,v­þ Àä¥KñߣGÈÌÎFijg˜îム�ü´kfùúâþãÇÈÎÉAÔóçXé燭Vù~‘çž!b A5MËØð&*ªJå4ùx0TÕÕqcÛ︾õWɘiÏŸãÎîݸµcTÔTÑxàÀŠ—ýÑ TUyâ®oÙ‚7QÑÈËÎFj\îìÞ�‡µ8(ICOÏΜÅãG‘›•�ô„D„ûùÌ>ø‡ãIP0ò²¥×ØÕ¯Ñs¬TyEaºf½zˆ¾p‘Ç�#7+ ¹rþ¿QßÍ B�ÂéS‹y|ú4Ô45Q¿‡[Å?Hª©BÏÊ -<<ж¨…ËÃãÇøËEDD 3äÕ¦ys,ž6 é™™˜¶bòóóùùù˜¾b2²²°tút87k†ùãÇK�É‘†´°0 ë[8Öã˜|±f `Á„ ¸éï�ä‹qçða,óô„šª*¾úáD<}*)C¨VØc'=3¿<ˆŸ-BÂùóˆ?wNj}jZf­^�ñŸ|‚ûÇŽáUh(®ìÝ —¢oǶìßøæ×_—˜—-¼mâÏ�Cü¹süõW87k†Ììl™á�¼î=zïŸ~‚šª*~øê+< Bâ… 8µe šÚÛãú½{øßo¿I¶¿�e6@ `î¸q?xI.àö¡CXæé |¿};®Ü¾-µŸŠž÷J??äååaÞgŸáî‘#H AìÙ³Øÿð³´Äó¤$ø}Ь,ñ1¥edÀÇÏ?yy!ñÂÜ;v }‹¾Uúßo¿aÝÎ�ضj’J¬ûóøqé�•¸_ä¹gˆhPMSÓÔˆ2³ªTŽeçÂ÷À™ëŸÖæN­+\¶é= —]v`aw q+YJWÚóçÿÿú¢0½ä:uí=Ǫ–÷´ÄqË£Q¿þ(ÈÏÇ“À õ„üÜ<4ê×ï­eŒ={FêϘ  Þ³m¦OƒP[‘ÇOàñéþrQÍŽQÔ“w–‘â¡ÄÜqã‚óááØøçŸ˜9f ~Ù³!7o¢{»vøbìX¹Êܲ?²sr°ÌÓ 'N”,··¶ÆÂ‰‘_P€•~~øÝßkæ6UQ)Ì…’_¾Ä4LøDº«@ ¼xýýºvꝾ’¬kîà€¾ú ÝÆ�ÃíˆÉ²žŽ6¯X�¦öö’m;99aã’%è4z4®Þ¹SéúÝú×_ÈËËÃÂÉ“1eøÿ�JߥMüîë‹^“&!äæMÉòmÿ�¼¼<Œ<+gÏ–,o`cƒ…'">9›÷ïÇ®#GоèƒgeÎûIÑ r &L€¾®náJ¡îݺÁÔÈCfÏF|rr•î-ñ1½JMÅ”áÃ1©¨»‰¶…|çÌÁé‹qéÆ xO™‚Eìl‹­{ðäI•ïy4Ž¡ñ¾¥§AÃÀêº:È~ý¦ÒåèXX†Ø_þÿíVVÿ<`nxýLvë‘×E­JtÌÌÊ,##1Qêßù"Ñÿ¯K*c]Ñÿ5uŽU-ïu‰V¨o£gmóÖ­ð<ü2Jü?š•’‚¸«W`Ó©ômmñ&:Zþ‚ ��šŠ”ˆ‡ˆ8zQÿ2�'"¢÷<̨ l]±�F�ÆŠ�aoc??`늒‡Ç·ù·( =`€Ìõýúa¥Ÿ.\»&sýˆ·|³1}äÈRËš5lXøá$- pbÓ¦2_/Þ6­œQçßæBÑ8ŸôêUj]«¦M‘xá‚Ô²KEƒ‚}6x°ÌòF¹»cóþý½u«JçÝÄÞw>ĬիñÍܹ°45•lÛ®eKD¾:x¸K÷3n`m-ùyXŸ>2×¥–¨÷ªÞ/òÜ3µ!((ʃ†hPÝ�ž� 6r@|9ïQo#ÔÒ’k;õJŒÉ ¦© ev¥/o's›¬ìJ­«És¬jyò ØZ\£~ý�–.m%3«ÈÜ®?\ß²µÌõòÎ>CDDôÞ†•�šÕÆÂ?y{cÜ×_cäüù€ßV®„U9ߨ”hZÆÃ©Ø“2¾iô–ÎìJOY¦Uô!¬øà’ɯ^aËþýø÷êUÄ$$HÆ©ÈÍ«ú(áϊα���|uRÔì¶x+‘âš-�‰�¯ÒyoZ¶ ƒ<=ñW@üƒ‚àÒ¢º·k·víàêì 5µê½em‹¾ÓPW—º—d­+9hUïyî™Úpýúu¾C*9}cT®¤ÿî¨IcØuëZ¥0C”™ u]]ì89EátuefA]WjZZÉè%ÔÖ’CMªîs¬É:+E (5 iYö郿þ†‚¢îºDDD 3Þ!÷®]ajd„¤”˜¡OçÎ Räü@”ZFË#ƒr_§+Ç·6OccÑ{òdãËhRÚå|“WÞºê¼_ä¹gj“Žž!·ìÀwK%clf ½zÆrmË@ãýõ$8MF£~ýpïÀ¤ÆÆ•»½YË–h?wþÛ·_j¬‡ÔØX7m };[$ÿW½Óž§'ÄC]·êÙÛ#IÆÀ×õí ·+'L¯Õ}Ž5Yg%Yµk33Ä„„⌗W™Û¹­ZÛ.®°jß±¡—ù BDD 3Þµ7")%†úúHJIÁŠ�ñͼyr¿^WK ¯ÓÒsæ êééUxÿ‚ýl+cÉúõxž”;KK,Ÿ1[·†‘�Ô…B¨©ªB¿}ûª= ji!5=)¯_ÃÔÈèíÛkjâMz:Ò33%cYÈz ×•³ÙlyÌML°röl¬œ=‘QQ �p0.\»†™«V!''SЦz•GN±¾Ñ5¡ª÷KuÝ35v~úFhëêªÛh¼Ÿ’nßAüµk°hÓn+W!hád¾H‘¹­QãÆèº|´ML`Ü´©T˜~ ÆM›¢ùœ[¾¼ôÃtûöh7k&ž�ý7Š .-�„›·`ب¸Ë 3Šf1I¸u»F몪ç(PUEA±–•5YgeÕ‘x°Ô²< €mW4êןa)<•ºvB!7n`ßÂÒÔçwí‚™±16îÝ+óA ‹šü?|ö¬ÖÎC<ƒÿÏ?cD¿~°³´„®¶6Ô…BÄTqjR¨_4 Øƒ3l”Å®hû{�Ë\¿h¹]%w+÷˜�¦zxàÔ–-Xïí øeÏž2À’ÓÉ(5‹HuS„û…¨º Îròþ¹ôí·È|ñõ6À mÛÐbÔHèÛÙAUCúú0iÖ ífÍDߟ×CÛÄ/""p}ëé÷Ù#G�›•…únÝÑeñ"èÛØ@EM ZÆFhòñGè¶l)ômm!¬D8qø0òsóààîçÏ'CÏÚ ªз±�Ó¤IppwG~n"Ž©Ñzªì9æµX¬ïÖ*jjÐ44¬ñ:+NC_6�;C”žŽè‹ËÝ6æR²SSaëÚêúúüå ""†ïJFV¦ûø ???~ý5ì­­±vÁ©éYeÉÍÍ•úw�¢Vëwí’¹}à¥Kp2>7ÖØ¹dç䬊 €)¶fëVÉÃ{e[¸:N ·çXé¹àoGDÀÄÕ='L(µýöC‡d–·«èCdg'§*�÷ÄÅ‹áЯŸÌ™ZÄrÆÉèz£WÔ…åæƒ¥Ö­Û¹³Fï;E¸_ˆhPe¥'$âÔÌYxñà4 ÐfêT|¼sFŸ>…G£¿ßF8 5MMD_¸€€Ù_”43->—¾ùù¹¹hЫ>þcÆbØ_¡ÃܹêèàEDnn«x ƒ×Ïž!ܯðý³å˜1¼{7FŸ>…�ÿ؅ƶ( ߸¯k8¸®ì9¾¸_øÿR×%K0&(Ãýÿ®ñ:+®Aï^PUWdzÿE^vùƒ�æ‹Dˆ:{*B!öú�¿DDÄ0ã]YúóψŒŠÂÐ>}0 {wÀ�޽ѿkW<ŠŽÆÒŸ–Ú^<&‚p0rD"$¾x˜4t(´55ጉ‹#2* 9"â““±õàAŒóòBdT”dŽšÐ¸~}À² �üê²²³vçF/\ˆÔ´4É̇ÿùG|T(42***Øuô(¾Û¶ I/_"#+ çÃÃ1aÑ"deg£C«V’í'5UUì:rË7lÀã˜dfe!2* >7bÇáÃPSUÅä¢iN+«  ñÉɘ°hN]¸€W©©ÈÍÍų¸8ÉõkáàPêuâe^ëÖáÎÇÈÎÉAlBæ~û-®ÜºÃü†Iî"T¥õçÏqbê4œ[¾ÏΞEZ|�N�BzBòE"äffâŃ¸æ· §fÌDNZåî£ûý�À¹ó‚ìׯ‘Ÿ›‡¬—/}áN1÷ÿö'õT™s ýá{$Þº…ܬ,䤥K��Q“u&Ö¨h¶¬'�Armÿèta÷¡Fýû󃈈š  äÔ E¦N� èÓ¦ ú¹¸¼³ҭľÒÂÂp><îÓ¦ÁØÀW€iQ3N p† —#�ž™‰›6¡kÛ¶€þS§â|Ñ¥ÅË€¿1iɈJ´ÚsrtÄñM›`Plüñ±—5KEÖï;y“–,)µ��¹9‚·mÃÒ_~Á¾“'K÷ÛöQÜ�;wbñúõ2×96h€€_•˜Òoï^,\»Vö�$à» 0ÍãJçý,.nãÇ#)EvŸmM ø¯_/¹†bOŸÆøE‹Jm¯.â¯Ä4Ä&$ õêU‚r�©2ë*s¿TôzU§Saa(š¹`óæÍ2·ùî»ï sëF8ê ¾[¾‡þ=ñ"ÿ»°¶¶.w ñý¢ýæ š<ŒdåÕ!��¡¯,\¸�BD ¡N´ÌHÏÌÄtàÛùó¥‚  pŠÍå3f   Ó}|�^4Xå�^^puv†¶¦&ôutàÒ¢…ä5Cz÷ÆÅ?þÀ˜�akau¡:ZZpnÖ ¾_|�àmÛJ=˜V'�þý±váB8ØÙA¨¦ssŒýè#ýö¬ÍÍá=e œ¡.VzZÏ9ãÆÁýz|ر# õõ!TSCkkÌ7ÿüþ{©6¦�‰ã~~è×¥ LêÕƒšª*L�Œ0ÈÍ '7o–dTT}++\Ú½ó>û Ž @OGêB!ì,-1fà@œß¹³T�ÃúöÅ–+ðA“&ÐÔÐ@===tsqÁ‘ УCèµÂÉ|KÛʪíû…¨&°…)*…k™AD5�-3¨"äi¡Á–DDu[f‘"RaQyØBƒˆˆˆˆ à ""z+DDDD¤Hf‘\h‘¢`˜ADDr+hlß¾�•BDDDDïà ""ª´ŒŒ V½s 3ˆˆHn%g6ñôôd¥Ñ;Ç0ƒˆˆä"Ï­DDDDDïà ""z+DDDD¤HfQ¹d‘¢a˜ADDeb�ADDDDŠˆaIÙ´ot]\XDÄ ƒˆˆˆˆà ’rýÞ=V1È """"…Æ0ƒ¤0Ì "DDDD¤èfԲ˷naœ— €Q§N°vsC‡‘#á»y3’^¾$¿zãÎ�¡ëâ‚›÷ï—YÖƒ§O¡ëâãÎ�ñâÕ+¹Êû~ûv躸à¿G�º..ÐuqÁÁÓ§%Û\¹}c¿ú �úö…aÇŽhЧ<æÍÃùðp™Çó&- º..p:°÷Ä ´÷ð€©«+Ú Š]GŽH¶ýÝß.#FÀ¸sg44ßþö xƒ1È """"*E�UP{ã3ooäååI–åˆDx‰»‘‘Ø~èÎnßkss|ܳ'öŸ:…GŽàGG™åí9v ðI¯^8.wÙòøãèQÌXµJª¼¤”?w'ΟÇÚ 0ÕÃCê5š€Ì¬,ü€ÉK—JÖE<{†é>>°45ÅÝÈH,úé'ɺ¨çϱÒφúú˜2|8o"DDDDD 3ÅJ??äååaÞgŸaÒС°45Efv6.^»†ß}‡¨çÏáãç‡ÍË—câ�!ØêöŸ<‰Õ_|! Äòóó±÷äIÀ¤!C0³(x�§l˜?~<æ�/ü3-,LRöã˜|±f `Á„ ûÑG°63C|r2œ>�Õ[¶à«~@�ÐÄÞ^ò:¡Záí•–‘??üäå…QàÅ«W˜³f N_¼ˆÿýö"ž=öU«0ÈÍ ÉÅÖýyü8à �“�‰¸¨‡¬%£§o½zÆ 2ˆˆˆˆˆaU¯'±±’€@_W .½[7˜aÈìÙˆONtiÓŽ àþ“'8ræ Fôë'UÖ™+W›�€èääT¡²ßfËþýÈÎÉÁ2OO,œ8Q²ÜÞÚ 'ND~AVúùáw¬™;W²^ ^¥¦bÊðá˜TÔÝDÛ¾sæàôÅ‹¸tã¼§L‘œ�m±už<áM¢^&ÇáäþŸYJhÄäer 2ˆˆˆˆHÙpÌŒZ$nÅ0kõj†ôê…=Ç�cב#Xêé ø;(™ÙÙ˜8dˆ¤KIEË.OZf¦\Û•×’B[S³Rë¨vÙÙÙ±ê(DDDD¤ÌfÔ2s¬œ=+gÏFdT‚CBàŒ ×®aæªUÈÉÉÁ”#$ÛO2{ŽÇîãDZdút) µï IDATì>z@áÀŸU)»,ºZZx�–†˜3gPOO��HÉ1È """"eÇ13ˆƒ�¦zxàÔ–-Xïí øeÏ©m:¶n�ˆMHÀ…k×pÿÉ„ܼ —-кŒ)[å-», ‹ÆNxøì/‘’c�ADDDDuÃŒZ4qñb8ô뇫wî”Z'3®Äà�Àÿ·À8xú4v :yذj)[,77Wòs�öíëwí’¹mà¥Kp2>7ò¢)0DDDDTW0̨EˆONÆ„E‹pê¼JMEnn.žÅÅaéÏ…Sa¶pp(õ:wwhkjâPp0þó7^&Ç`�ADDDDu [f(¡ÜÜ\¬Ú¼ðùðá¬"’‰AÕUl™¡DrD"†DDDD¤°Þu�!Æ@ƒˆH±1Ì """"…T[A† ""ÅÅ0ƒˆ*E @ Ƚ^üïšøSVùªªª077ÇðáÃqýúõ Ÿ[É?B¡666øôÓOqïÞ=¹^#ëXKºzõ*F��¦M›BOO¨_¿>>ýôSܹs§RûyÛõ!"b�Á@ƒˆˆaÑ[”úSÞºŠl[Ö~^¿~��;wâÊ•+èØ±#.\¸P¥cNLLÄúõëñÏ?ÿÀÅÅW¯^•ë<Ë:V8vì:w/bݺuHLLDll,¾ùæœÿüs¼zõªF÷¥«« ???ÀáÇYùDT§Ô• CŒch½{ 3ˆˆädii‰ï¾û±±±˜={v•ËûòË/áêꊨ¨¨r·{óæ +ŸˆêŒºdˆ1Ð "z·ØÍ„ˆ¨&Mš„Ý»wc×®]:t(>þøãJ—•ššŠK—.aÆ øöÛoK­ß»w/€ê›……ˆ¨¶2 MçþÈÉÎB\ÔC¹^¯§o½zÆïô˜å=6hܲ^$Æàer»œÕ0†DD °uëV´jÕ S§NE—.]`l\¹Ö¾¾¾¸|ù2Ö®]‹üü|Ìœ9VVVHLLÄÁƒ±hÑ"˜ššbݺu¬x"RzQQQRA\>ë_¡2„êš7ûïì˜ï„�©ð1‹íÛ·cÆŒ¼ˆˆª»™U�ƒƒ–/_Ž„„xzzJ­åþ)ÎÈÈÿþû/V®\‰sçΡU«VÐÔÔD³fͰ}ûvÌž=wîÜA³fÍXéD¤ô´µµ¡©©Y¥2D9Yïô˜³³3«å¼‰ˆ¨ú±eUÊÛf ‘gV‘ŠÌ… 3ˆˆˆˆˆˆH)%''ãÅ‹’�Büo@±Ã‰ŠÊÊÊ’:yέqãÆ[[[hkkÃÆÆFòw]<f‘ÂÈÈÈ@tt4222[¥rMÕÕ%?««ªÂP(”ZoVÆtÒê**0ÒШ–sKÉÎFN~~©å¢ü|¼ÌÉ‘Z–�õÿÓS‹ ðJ$*·lqàQVðamm ØÚÚJZw(SÐÁ0ƒˆˆˆˆˆˆƒ-im�¬bño£­ª UU©pÂP]B•¹/,´´ê|Ë Elå ²óò$ÁGzn.ÒssüøñJ$‚¨  Ôëbcc‹›7oJ-wiiÚ´©$àPÄi¦ëd˜‘˜˜sss˜››#>>žï Xg¼FDDDDDï·¨¨(DDD ::HII‘ëu¦êê’°BGM :jj0TW‡†ªê{Y�ªªr…4âV âÀã¥H„ôÜÜR-<Ä]ZJ¶èhܸ1lmmakk‹&MšHÆì¨-5fÄÆÆÂÆÆFæ:###tïÞË–-CëÖ­«¼¯””œ;wƒܹsàääÄw9•Ug%붦÷Gdgg--­R2‘r{ðà"""ðàÁƒ·Žù PO(„¡º:t…BÔ ®U…²)¯HšH„´¢`ãeNÒD"$•èæR2à022’Mš4yç­7j,̸|ù2`À€8vì˜d¹H$Âõë×1iÒ$¸¸¸àرcèÛ·o¥÷“——‡îÝ»£W¯^’îž={¢@F3*›¬:“U·5¹?¢7n ::óæÍ{ë¶� Ô²’÷ÔÕ«W±nÝ:„‡‡#..999°°°@×®]ñõ×_£eË–2ËÎÍÍÅŽ;°wï^ܼy¯^½B½zõàììŒÑ£GãÓO?…j‰ä_|<§N�’ùž&^/ï}ŸŸŸ��7bÛ¶m¸ÿ>TUUáàà€ñãÇcÆŒPSc/A"""Rlâ–¥º2” .Ì45a(ÂLSºjjÐ-1~Õ,]¡º2£”ìl¼ÌÉAzn.²²¤Ž””¤¤¤H®­‘‘š4i‚¦M›¢uëÖ5>öF�‡:t�¾Q…B´oß›7o†««+¼¼¼ªflܸwîÜ�——ïÀjƺ¥wíèÑ£€�¾uÛâ¡€¬`ãØ±cøä“O`ee???ôèÑééé ÄÌ™3ñ÷ßãìÙ³hß¾½Ôë1hРܾ}‹/ÆÖ­[aee…¸¸8ìܹS§NŦM›pôèQ™Më¼½½Ñ§O™ÇT‹/Æš5k0nÜ8œ:u ªªªX¾|9æÌ™ƒ»wïbË–-¼aˆˆˆH!Œ��ܸq£Ìn#õ„B˜ihÀP]æšš .˜‘†F©)ÙÙH, 9²²�‘—' 7BCC  p€ÑÎ�;ÃÉÉ©Fº¤¨Ôt˜QòAALܽàîÝ»¥Ö�>Ÿ|ò ôõõaii‰5kÖâââ0lØ0ÀÆÆß~ûm©2ž>}ŠiÓ¦¡AƒPWW‡¡¡!ÜÝÝqéÒ%™û“÷œ‹×Yyu+ïþ  «« +++¤§§ãóÏ?‡‘‘œ��úQí‡õêÕC×®]«\–——rss±uëV¸»»CKK &&&5jÖ¯_�ÌÌL,]ºTê5ùùù>|8®^½ŠÃ‡ÃÛÛöööPWW‡½½=–.]Šƒ"44#FŒ(ÕÊB(âÖ­[8xð`•�çÎ�€ï¿ÿfff066ÆêÕ«»wïæÍBDDD #99ûö탗—|}}ñÏ?ÿHÚªªh¢«‹®&&jcw++¸£‘žƒ % 8õõÑÉămlð‘•:ÁZK Âb_èÅÆÆâÀ�X´h|||„ôôtÅ3òòò�@Pf˜‘˜˜XXFFRËwïÞ� ==W¯^ERRºté‚/¿ü‹-’lwíÚ5ÉT<ÆÆÆ(((@LL ž?ŽçÏŸÃÈÈ 6DGGãÍ›7°¶¶ÆòåËñÓO?áúõëPUUÅâÅ‹qáÂI¹ÄÀ�agg‡û÷ïãæÍ›ˆ‹‹ƒ§§'ÂÂÂ`bb‡ ׉ølll°|ùrüøã�¸ÿ>Œ��áíí�ÇÃ××ßÿ=îÝ»|ýõ×8{ö¬¤Œ+W® uëÖ ÅŸþ‰W¯^áÌ™3ˆŽŽ†››.^¼XjòœsÉ:+«n+²ÿ¨¨(¤§§ÃÁÁãÆ�CŸ>}‡óçÏ+ì5¢Ê‹��…@ ÀĉK­›5k–T8VÜçŸ�@€ØØXÄÇÇ#,, }ûö­–.âþ|]ºt)µnÈ�!Ø´i–,Y"µÜßßçÎ�ƒ‡‡z÷î-³Ü�bèС8sæŒT:PQQÁ¤I“°dÉä%Ô•õòåKI™bâß1[[[ÞtDDDTë.]º„µk×bÑ¢E¥ k--t02ÂGVVlcccØêè¼·ƒtÖeºB!éé¡»™†ÛÙ¡Ÿ…šèêB»ØµóæÍÃï¿ÿŽû÷ï+f˜q÷î]¤§§£qãÆ044”¹ÍÞ½{Ž©Qòa¢^½zظq# §§‡eË–víÚ%µíÕ«WmÛ¶•, /µL܇'66kÖ¬��� 6lˆAƒøÿV$™™™˜9s&ÌḬ̀}ûvÔ¯_æææØ¼y3þüóO M›6•ª“âÇàëë‹úõëÃÊÊ ü1ÀÓÓ+W®”,›¸ÅCff&†Ž‚‚œšššÐÕÕUØkD•gmm ggg–ZCCÃ2×9;;ÃÚÚÇŽCAA�\]LäaooR­Ä´´´0uêT¸ººJ-·x=zt¹e�3Fj{±œœ,Y²Ïž=Ãï¿ÿ^¥ão×®�ä÷S$aÆ 6l´µµ±aÃÞtDDDT+ÒÓÓqôèQ|ñÅرc‡Ô€�âc¨� º›™±åÅ{ÊHC.ÆÆlcƒ~h©¯�zÅîƒÐÐP¬[·^^^2{Ôj˜QV“‚‚DEEá»ï¾ÃŠ+àää$i6-vðàA¤¤¤ U«V’efff€ÔÔT©mÃÂÂ...¥–É 8>ûì3©pEܧ]<�Ÿ¿¿?0bÄhkkK¶300@ãÆ�K•[âc;v,Œ��¥Þ €ÂoŠ‹·RÉÌÌ,¼@EßÊîÚµ QQQ7n,--¥ÊŸ¿¬€âmç\V�•¬ÛÊî¿mÛ¶øè£�d^7E»FT5DLLŒTÊ…`Ú´i¸ÿ>¢££%ëçh«ª¢¥¾>>²²’l}AŃ�V††p·²B? 4ÐÑ‘tEIIIÁŽ;*jÔH˜qåÊÀü�@ ù£¢¢‚úõëãÌ™3X»v-._¾, *Ê’ŸŸ�§OŸ€¤KBya†ø¡XVÀѽ{w©×?yòдiS…³@·nÝJ‡ººz•”ÅÇУG©åâ$³ärñ˜âc;tè€Ò-Y@£h@–ììì ŸóÛêL¼¬¢û—9~üø2ƒE»FTõ0€T ŒÀÀ@…BÌ™3jjj¥Ö‰ï©¬¬,¡S§NRa_UŒ7GŽ�¥¥%¦L™SSS¸¸¸ÀÛÛ7nÜ�ùq÷·· P$~ßJHH�¹þ믿ƛ7o°qãÆJ{TT”dÌŒ[·naРAxúô)öîÝ dggKZ?Õ´   R!†©º:º�™ÐÊÐ�-0H®`£“‰ >²¶F##I7q¨áããS¡î'5Ú2ãêÕ«ÈÏÏG^^rssñÅ_(ì'?cÆ ÉÃgqÇŽÃÀ�aee¡P¡PGGG�j­QüXü-iYÊâe%gV¹~ý:H¥¼víÈœ®ñùóç¥Ê­ñ1tìØQæ1”ulâ.âckݺu©²Åã[ˆé¬È9¿-\×me÷/ëdE½FT5íÚµƒ¹¹y©À¢cÇŽ033Cûöí¥ÖH–###£Úº˜ˆ 4×®]ÃDZqãF4lØëׯ‡³³3†^ªµ—¸%ÔÛ¦OÍÏÏ€RÓ³ŠcþüùX³f Þ¼yS¡cމ‰�««+.\¸€+V@UU�?–'ÃßߣF�âMGDDD5*99k×®Å�¤BŒžffèmi Ûžz“ê& UU4ÒÓÃ`©P#66ë֭þ}ûj'ÌHKKÃÝ»w¡©©‰Ö­[KZd¨ªªbÞ¼yPSSêU«d¾vóæÍ4hâããqðàA¼~ýyyyøê«¯J=H?}úÉÉɰ°°�‹��/·œœœ ,ÑÔÈÎÎ#FŒÀµk×pâĉR¯]»v-€Â) ;wî,!$$¤T˜!kÉòÆ~(Ùõ@üúâƒEŠg(Ùb䯿þ’YFEꤼc�g¹��é®bÇ�P8îFEϹ¼ñ+*»YûQôkDÕcàÀ�HMMEhh(®_¿ŽäädôêÕKf$''ãúõë Ajjª¤ÛÒ±cÇРA´hÑ¢JaŠ}$aÅ’%Kн{wL›6  Äùóçñé§Ÿò†#""¢qñâE¬[·NÒ£‰®.ú3Ä ÔÊÐý--aZôœ oooÉ—êï$Ìw1)Ù]@lÁ‚€•+W–Z'î&РAɲ7nH¦åüàƒ$ËÅýÅe²º/Èó ,žÎóÑ£G’e’�¥ªƒV%Ì»]|ªV pJÓ}ûö¡eË–T�o@ººèÞ½;þùç {÷î’ß_¡Pˆnݺ!((gÏžE·nÝ §§'w###$''ãÀ�RËÅ-·zöì)Y–ššŠK—.•9…©¸ËF×®]¥–÷ïß½{÷ÆñãÇ%­}J:}ú4þúë/|òÉ'pss+÷˜544°lÙ2lÚ´Iî:�#>/044ÄñãÇñøñc<þ¼T÷"""¢ê�žž.é"+гhv¢wÉÅØXhÄÆÆbÿþýï6Ì(9-kqââ%[glÞ¼ýû÷DzeËФI„‡‡cÊ”)X½z5š5k†ž={ââÅ‹ ¿mÞ¼9vîÜ)™ ¤¼oýK>XËZîéé ___œ? 6Ä‚ àåå…gÏžÉ<§[·n•г2a†<Çfoo�‹/¢qãÆøðÃa``€#F k×®¸yó&š5kV©reÕ™¬º­Žý¿ëk$ïõ¡ê5pà@„‡‡#44´Ô°½zõÂÅ‹qõêUI“£G�BWW÷­ÁÀÊ•+aff†U«Vaß¾}HKKCHHfÏž CCC©÷___8;;cíÚµX¸p!ž={‘H„ØØXüôÓOX°`LMM±nÝ:©}ìÝ»®®®5j–-[†'Ož ''OŸ>…¯¯/Œ?ü;vì�«>Æ�;;;äääȵýªU« ¦¦†É“'#((iii¸ÿ>æÌ™‘HKKKLŸ>ñññ\”ˆˆˆ¨<ÁÁÁ’®%m Ù­„j5а.ºÿBCCe¶¸ô$Qä IDAT¼mØþ÷ܳgÏ`oo�6mÚH®Åüüü�››‹Y³f±¢ðñúÔŽÇ�£Q£F ¥’ÝÃÄÝ—""" ££ <ÿý÷[ËŽ��ÅêÕ«qêÔ)ÄÄÄ ^½zèÕ«|||$ûKMMÅÏ?ÿŒÃ‡ãþýûHKKƒŽŽ5j„þýûcΜ9eN �ŸŸ�;v`Ïž=¸qã^¿~ CCC´iÓãÆ�ÃÈ‘#K��!þ·¬·Ô}ûöaäÈ‘e®/éòåËðõõEHH^½zSSS¸¹¹aÁ‚’0# êêêxõêoºwhêÔ©§NýÐÖÕý�ì3öYNø@áÀ²â¾ˆÞ'GŽ‘ŒÑ5iÁúw¶ßð‹'p#¤pZøÍ›7óBP�çåå…””˜ª«£w9�½ Ùyyø+&УGÉçi15VQ¡ & gÏžEãÆ�%Ë·lÙ¥ÛËÌÌÄ/¿ü‚€€Vž^#^ŸÚÓ°aÃ2Ø[µjUj�xšSyX[[—Ùu¤$ñœÞÞÞ>L˜0&L�û5å…ððð�»¬:àÈ‘#e®?tèo4"""ªVéééHIIN¹J ACU¦êêHÊÉAtttéÏ쬢Bˆ‹‹ÃŒ3�””lÙ²?üð:wî OOO©í===Ñ«W/©©DIq®¯‘üŠ?,Ö“sàr¢šf®© ˆŒŒ,µŽ-3ŠøúúÂÄÄ;wîDóæÍ‘››‹† ÂËË _~ù%444¤¶ÿý÷ßYi |�x}ˆˆˆˆˆ*ç•HÄñ2H!$�á" ÃŒ"***˜?>æÏŸÏÊà5"""""zo=NM…£¾>+‚jUšH„¤rÑg7""""""’x•›‹ûœ5�jYè‹å®g˜ADDDDDDR®½|‰G©©¬ª!IIHÌÎ.wv3!"""""¢R.ÍnÒHO�•AïDv^®¥¤àIFÆ[·Uˆ–qqq066VÈ MLL„@ €……ﮨÖ/‘â°ŠŽ�Jn€Â@ã\b"²óòX1T£R²³/ 2432`’�Pæö f„‡‡Ú¶m«�•zç΀“““rÞ))8tèPµ½¾ªõQÝåý{÷—eõÿqüu³÷Þ (‚ŠÍ™åJMëgšmWe¦f¦e*V–_WnMMËUŽ4m¸3s¥™g‚‘½do~ÜÜ·Ürƒ àçùx|ß<\ç\ç:×�r½9ç\B!£Œt¼®^E¿hšDf&Û## IK“Á\v~>gÙCr^IÉx_E·ŒMÂŒrèÒ¥ ………ìÝ»·Æ}0òóóéܹ3‡~`õ«2º=ñä4h …‚%K–”z̲eËP( 4H£\¡P P(øâ‹/î{žÅ‹«�¿·¾B!Dme’™IÃÿ®`‘” @na!ÿÞ¾Íïjˆb\LNf{d$ÁÅögq �À3$½‚‚2ëW«0Ãßß_îè¶lÙ2.]ºDëÖ­Ký‡Ýžx²?Û 4à“O>áÂ… %¾ÄÇŒ——Ë–-ÓÚÆÊ•+ÉÍÍ-õ………,]ºT[!„O$½‚>žŽ;2aÂ4Ž�ŠŠ"::[[[êÖ­[áúaaa¤§§ãååÅ AƒèÑ£QQQlÚ´‰””ÜÜܘ:u* .$(([[[&OžÌï¿ÿÎôéÓ™7oW®\ÁÒÒ’‰'rèÐ!uÛÑÑÑDGGcccƒ§§§ò&„‡“’’‚««+S§NeÑ¢E¢««Ë”)S8zô¨ºþÖ­[éÓ§îîîqþüy¢¢¢9r$§OŸÆÎÎ//¯ �ïäÉ“yç�wðõõ%88˜[·nÑ A&Nœ¨ž6öìY"##°µµ¥°°�ˆˆˆr�miõµ�GUú£­½ãÇ�ãïïONNGŽ!22’6mÚ0iÒ$>ûì³ �ƒ¨Ý|}}Õß¿~ø¡ºü³Ï>ãìٳ̜9³Ô_øùùñÍ7ß”Úþ7ß|CóæÍ100�ÁB!ÄÏ61±D¨‘[XÈÍŒ öÆÄ°;2’3‰‰l�ãx| žb~áÂÌË/¿Œ‘‘‘zÆCdd$Ó§OÇÃÃ^|ñEFŽÉ´iÓÔå}ûöàŸþ)³çÏŸW·;sæLÜÜÜðôôT×?qâ™™™Œ=Ö®]‹‡‡ŽŽŽ¬X±‚M›6QXXˆŸŸ_…ÇW5í}úôéØÚÚbooϼyó°²²R÷àÔ©SU[mõµ�GUúso{¹¹¹¼õÖ[ØÚÚ²aÃ4h€�� ³fÍÂÜÜœãÇ�Wø¼¢v5jýúõãûï¿gëÖ­9r„9sæÐ³gOÆ�Wj½ÜÜ\ À±cÇ8wî\‰¯ß¼y“Ý»w3pà@233e …B!î 5<ƒƒ±N¸­.OÎË#85•½11üÁ™ÄDbåç¨'Jv~~‰ãfF†z†N^>v±±ø\¼TéCEïa\ÀÖ­[K”988�Zlc�â³Å{Z‘ú÷>¿ð %ÊUÇ*ééÊ{饗°±±Q—«Xttîf<§OŸ.õ|ðàÁX[[«ËUª¦¤ÿúë¯ÄÆÆòÁ`bb¢>ÎÒÒoooÎ�;W©MOÍÌÌHIIáøñãôìÙ€úõë“””¤qœªï•[mõµ�GUúso{7näÆ�`dd¤>ÎÅÅ…”””J�ƒ¨ýV­ZÅÙ³gy÷Ýw177ÇÞÞžuëÖ•¹Ig^^o¼ñ,]º”ï¾ûNãëª}6Þzë-¦L™"ƒ,„Bq‹´t,ÒÒq 'ÑÎŽD[²Šž{Òóó NM%85}…G## q31ÁL__¯‰ÍÌ$.;›ðôtõÛHJ|V’’±LNÆ61ñ��÷‘lZPP@hh(€z)�¶ ¢2õ‹·1dÈ­ÏÏ>û¬Fùµk×´–«fr4lذ̰EÕnçÎ�5êß¼yS£¾êíO?ýt‰>«¦­W&Ì?~<}ûöå­·ÞâÈ‘#Z�ÓTdlµÕ×6UéϽí©^ÙÚ©S§6¢ö³¶¶fÍš5$''ÎüùóÕ!]YÜÝÝyöÙgÙ¸q£F–™™ÉêÕ«éÒ¥ îîî2ÀB!„eÐ+(À!.ŽFW‚ð¹x —ðŒŠïgVXHDf&g““ÙÅÏaa‰‹ãbr²ÌܨaT3/.&'³?&†�·nñW\ïÜÑ2tòò±N¸MÝë7hxÏ��dÀCš™±sçN¾ýö[Ξ=K||<êß�6oÞ\kQü·"õ‹·ÑµkW­åmÛ¶Õ( à©§žÒZ^|釶þ©ÊJ«ß²eK@¹O@Ó¦MKô9::ú¾ACiÆ�‡‡‡_}õëׯgýúõ<óÌ3lÞ¼YãNsHEÆV[}mãQ•þÜÛžê˜F�=°qOÕ¬#Ö¬YÃk¯½V®×§2„°zõju@¶qãF:t¨ l-”žžNDDD�ì{DDD¥^ lll,ÁœBˆGÂ0'‡¸8ââÈÓÑ!ÍÜœ;VV¤™›‘kh¨nD 2¬ôôp46ÆÚÀS]]��e0«Ap‘œ“C\v6i¹¹Äeg“^ÆÛkŒ220KMÅ"9¹JËG[˜±bÅ FŒA«V­Øºu+¾¾¾˜˜˜ÀŒ3hÑ¢…úXÕæŸöööê²*R¿xÎÎθ¸¸h„ÑÑÑ888¨ßšQüxGGG\]]K”ÛÚÚâáá¡Ñ†��]‰²{ëGFF‹‹‹ NNNêuêÔÑès\\áááíVTÿþýéß¿?G�eìØ±:tˆaƱsçNõ¹prrR_EÆV[}mãQ•þhk/&&@=†UñdرcË–-ãí·ßÆÎÎŽÙ³g³xñb�MAËú �5ŠåË—3nÜ8 K—.ÅÒÒ’~ýúÉàÖBË—/WÏЫi~þùçJ× �@C!Ä#¥WP€Õ�;Xݹ£|860àŽ•™ÆÆá(÷ÛH¾gÙ»©®.ÖX`¥¯�©ž6ÅêˆZ¤ç瓞—GlVI99ê}.î^˜¥¦a–šŠ^AÁ£ý|=èçÎ� À?ü@ãÆ�Õåª�‹?0k[bR‘úÅÛ¸w#Mm{;”vÎÒʵµq¿v‹÷#¿(µº÷MªWAVf‰É½:vìÈöíÛquueÿþýllË;Ué�¶ö,,,H¬Äô£ÒÎ+j¿èèh† F½zõX°`ìܹ“‰'Ò£G|||ʬobbÂË/¿ÌêÕ«Ù»w/–––òÞ{ïa,¿¨•Ä}µup}dýuõh€¾�¹9Y•nÃÈÈHcï&!„âqPÍÚPQÍÜÈ41&ÕÌŒ �ãÓóóI¿g€¾B¡žÁa¦¯�•¾>::X`X´¡Ð”–›Kz^ž:°HËÍ%=?_ãí"eÑÏÎÆ8#ãÌ LSSÉÌ‹Gf¨–OÔ«WO]vîÜ9õëJ›5kVænEêW4œ¨hyYKLÊfxyyqéÒ%nܸ¡^6‘‘‘¡*f|úé§lÛ¶�;v¨Ò Š0GGGõqª7¼(*2¶Úêk�ªôG[{Íš5ãðáÃܸqC¸;vŒðâ‹/òí·ßVè¼¢v+,,dРA$&&òË/¿`nnÀºuëhÛ¶-o½õÇ�Gÿ>›L 2„Õ«W³yófõƒ®,1©½† ¼yóÔ¯‹¶¶sá©g_ªP�adlúHûÉõÿNªƒŒñãÇcgg'7_!DµR|æ†sQY¶�Ùú¤››“ilBŽ¡�zSQ•ÜÂÂ2ÂMuu1ÕÓÃ@Gë¢_,« V…ª�ИM›•U¢¬¼LRR0ÌÉÅ83£ŒtL22ù¬‹Çftîܙݻw³|ùrÞÿ}þþûoBCCÑÓÓ#77WkPüa¶"õK t˜q¿²Òú1bÄF�M@@+V¬ >>žyóæaggGHHˆÆuwïÞ�+W®Üwwaa!7nÜà£�>bíÚµèëëóé§ŸðÑG•8>66–´´4ÌÌÌ*<¶÷Ö×víUé�¶ö&NœÈáÇ™6mË–-#44”aÆ‘˜˜¨ñ¦šòž·¼ã*j¦¹sç²ÿ~&L˜ ±il«V­˜}:ëׯ'<< ¼½½>|8ƒ RwóæMúôéõk×hÑ¢§N�ªÐØj«¯íÚ«ÒŸÒÆr×®]Lœ8‘«W¯baaA›6møì³Ï46r-ïyË;®¢æ9{ö,íÚµ£Q£Fœ:uªÄr®ÜÜ\Ú´iÃÅ‹9zô¨ÆçGµ�bñ¿þ¦M›ÆçŸÀ×_Í'Ÿ|RêñÚꋚ'==]c††Wã6Õ>Ð(O�qô��dˆZgûöíìÚµ €·?^üÈÎ{æØnÎW¾�nÅŠr#D­Ä‚ ð ®ËÊ+ÃØ˜<]²LLÉ×Õ!ÇÀ�le qïÒ•'…IJŠòÿ33ÑÍÏÇ8#�ü¼XD;;[´7æ½?ð0C”íÖ­[Ô­[???õÌPnH———Ç| ƒôɸÖÞ‡P???BCC9uê”Ö·\¼xÜÝÝ9w¦¥†aaaÔ«W…BADD„Æ&´¥…¥‘¿V%Ð� C 3„�0£z€:ô4‚€=½ËZªK0¡bž–¦þoƒìôs”³9ªër�‡fèÉ·íÃ1tèPöíÛÇ¡C‡ðööV—¯\¹€7ß¼ûÃrff&ß|ó ûöí“�{€d\k/SSS‚ƒƒï{\³fÍÈÖ2UO[Øàîî®Þ´÷~ÇKXQ»>KÅ—œ¨–gÔ´@C‚ !„â>�@±å&• jRÌÊ¿_V®�!9†ÊYæ÷¼¡¥,5mÖÄã&aÆCâääDTT£F�bÕªU˜šš²uëVæÏŸOûöí9r¤úØ‘#GÒ­[7�W½Šª“qB”GM4$ÈB!¾Š é2`�€„ÉôéÓ±³³S¿5//OOO&MšÄ„ 0,¶QÍš5kdÀW!DyÕÔ@C‚ !„B<©$ÌxHttt?~<ãÇ�—ÁBˆ ¦d!„â‰~æ–!B!”T�†jyÚõÿNrx÷új×O 2„Bñ¤“0C!„(¦ºd!„BH˜!„B”P] 2„B!”$ÌB!´¨n�†B!„wI˜QAqqq( œœœªE¢¢¢P(ØÚÚÊ8 !ÄV] 2„B!4I˜QA—.]À××W£<11‘ß~ûí‘÷çÌ™3´jÕªFŒ“BÔ4�;Ð� C!„¢$ 3*¨K—.²wï^uY~~>�;wæðáÃ�¼?Õ5ÌÐ6NBQS=®@C‚ !„Bí$Ìx–-[Æ¥K—hݺõ#?·*Ìð÷÷—!„Ñ£4$ÈB!„(ÝC 3®\¹Âk¯½†““ÆÆÆ4jÔˆY³f‘——§qÜž={xî¹ç°¶¶ÆÈȈ&Mš0gÎrss5ŽËÌÌD¡P`mmMdd$/¿ü2–––XZZ2f̲²²Jô¡¼mW¤¿...( BBBðóóC¡P0fÌÞxã Q(4hРÄ9’’’°°°ÀÊÊŠäää_¯Ì5j›™Q‘ë¶´´ÄÊÊŠ˜˜ú÷ï�¥¥%ÎÎÎLŸ>½JǧÊÔß½{7mÛ¶ÅØØ'''Æ�Gnn.mÚ´A¡PpýúuùîBÔÚ@C‚ !„BˆÇf?~rrr8rä‘‘‘´iÓ†I“&ñÙgŸ©�Û°aÏ?ÿ<éééœ:uŠøøx:vìÈ„ ÐhóæÍ›êêf͚ŗ_~IXX/¿ü2K–,)ñ@\‘¶ËÛßèèh¢££±±±ÁÓÓ“³gÏ €­­-………„……¡««ËÍ›7K!K—.%55•±cÇbeeUbÜ*z�QQQDGGckkKݺu+|Ýááᤤ¤àêêÊÔ©SY´h���èêê2eÊŽ=Z©cï§ŠÖߺu+}úôÁÝÝ�   Ο?OTT#GŽäôéÓØÙÙáåå%ß½BˆZhH�!„BñÂŒÜÜ\Þzë-lmmÙ°a 4ÀÆÆ†Y³fannÎñãÇÕÇþúë¯XYY±lÙ2¼¼¼077ç‹/¾àÇÔh÷êÕ«èëë3{öl7nŒ¥¥¥ú}Ó¦MÇ—·íŠôWÛ,ˆS§Ni”âééI^^¡¡¡êã233Y¼x1–––Œ;VëØUôµõ§"czþüy"##™9s&nnnxzzÒ·o_Nœ8Q©cµõ«¼õ333=z4¬]»Y±b›6m¢°°???ùÎBÔÊ@C‚ !„BˆÇflܸ‘7n0hÐ ŒŒŒÔå...¤¤¤pèÐ!uÙÖ­[ILL¤yóæê2RSS5Ú½páï½÷&&&êr777ÂÂÂ4Ž/oÛéïéÓ§K<¤«ÊŠïYѨQ#®]»¦.[µjñññ|øá‡ZgeTæµí—Q‘1UÕ}pqqA__}}}u½â3 Š? 7iÒD£<<<¸;{¡¢mW¤¿e=¤“ɽaÆO?ýDhhh™³2*s�ÚúS™1}ê©§4ÊhÙ²e•Ž-Þ¯òÖ?{ö,M›6-1>ÑÑÑ%ÚBˆšhH�!„BQ ÂŒ˜˜œœœÊnÅŠôêÕ‹/¾ø‚ pæÌ†ÎŒ3ðññ¡K—.;vLã¡wÁ‚øûûãïïO½zõøï¿ÿغu+o¾ù¦F*Òvyû«-<2d�7æ‡~àÙgŸ-f€r˸qãî;n½FmAeÆ´x�p¿€¢"Çj›™Qžú#GŽdúôéüý÷ßxzzòñÇ3iÒ$nݺ@›6m4Ú¸páB‰�M«ú5!„xX�†B!„†¢°°°°:w0&&gggˆ��­1ÛµkW8À¬Y³ÔpÖ¶k|TnݺEݺuñóóS BQ¤§§3oÞ<"##5Ê%È¢b¶oßή]»hÙ¾×#;otØ5b"®Ê_ Q›±`Á<ƒƒ±HK—A�]´³±..ZÿÖ«î�/móÈê쯿þâÀ�4oÞ¼\³2jâ5> C‡eß¾}:tooouùÊ•+JÌNBˆêN5C£x !A†§ÚÔ ðŸ=2 B!ü2“­¬½*ª›ÜÜ\NŸ>Í�!C°¶¶fÛ¶mèëëתk|˜œœœˆŠŠbÔ¨Q„‡‡“˜˜ÈÊ•+™?>íÛ·gäÈ‘ò+„¨qT�†———BT’¯¯/FFF�íü÷.'BñøUû™_s…» IDAT¥í·PùøøAçÎ�Y¶lõëׯu×ø0MŸ>;;;~øá7nL^^žžžLš4‰ &`hh(ß±BˆÉÔÔ”O>ùDBˆJ²³³cÑ¢E2B!ÔªýžB!„B!.Ù3CTGe홡#Ã#„B!„BˆšD !„B!„BÔ(f!„B!„¢F‘0C!„B!„5Š„B!„B!„¨Q$ÌB!„B!D�¢'C „Bñà„……qõêU®^½JFF† ˆx¢øúúÒ­[7ˆìرc?~œ�âîî."f!„B<AAAìܹ“k×®É`ˆ'Öµk×$Ìx¶lÙBVVóæÍcüøñh�„B!ÊFff¦ „¨6løØÎ½cÇvîÜ©Qfîà!7E<1²Ó’Éɸ#ñ�dee©ÿ_ !”$ÌB¡ÕöíÛÙµk— „¨1\]]ùüóÏùyúé'<€®¾!N>ípöi‹¾‘‰ÜñÄ <@ä…C2�€B(É B!´ –A5Jddä#?g`` :È0±v¤yß‘¸·ì"A†â¡°¶wîaaa2(â‰%33„B”I?% ë Ó2¢ÚJw­Oº«çc9÷–-[匌Æ=†Jˆ!„x¨<¼šÓÄïŽþ±Qfhˆ'ž„B!ʤÈÍÆ89^BT[Y6Ž�å¼ÇŽ#11Qù€Ñº—BˆG¢a³¶hˆ'ž,3B!„¨„sçÎ``b‰£·Ÿ ˆâ‘iج-Ÿ{�%'âÉ%a†B!D%\½zk÷F2BˆGN ñ¤“e&B!„• zU¢ž�± F‘ä„XömYÉ•3G¹AFêL-¬ðhØ‚./ Á·C÷uÒR’¸zî_üžî%(DÉ’ñ$“™B!„¢Ê"n\á³AÏrêÀvúùˆ¯>É’=A ÿ|)1a×Y4á-þÜò�F�‚ü|f�êGpàq@!*Ifhˆ'•„B!„¢Ê6, íN"Ã&-À¯SOŒLL165£I›Î ÿb)ÖöÎDݺ¦QçÀ¯k‰ ¢ž�¯  U �†xÉ2!„BQe7.�À³qÉÍP½šú3ÿ·@õŸ§íέ«Õ^ñåH¶,›ÆüßIˆg×ú%\:q�¤ø �M¨ßÄŸ¾CÆâݬµºNaa!ïw¯�±©9³~:ÎÆEŸqæÐ.l�Üørí~.ÿ‹?·|GÈ•@r²³ppõ c¯WèþÊpôôô5úwáø~~_3ŸðkÿalfNÛî/ñòÈ)ÌÑ—›WÎ1kóqÝê�‘–ÂîõßpúÐNnÇFbhd‚wóÖô4–úMd#Xñx �%'B !„B!ÊÍØÌœÜÄ,BƒÏÓз]™ÇN]ó'Iñ1Œû?_Ì,mX²û?Bþ dîØ�Ø9»3bê·¸yùÂʯF3{ôK|ºdÞÍÛp;6‚ìÌ <4ç»iðT·yó£éäççsü�m|7m4^ÍZóù÷{±°¶góÒ/Ù²l©É·8êsu_NÜÁòφãÿlÞÿjúFl\8…çN$4è<æV¶ê #.òs>@NV&ƒ?�‹�_b®³âËQÌù"/ÜL£–íåÃP Ì›7Oýºc!�†÷#ËL„B!D•uù}:˜_VÎ""$¨Ìão)_m[·asr²3Y6å] 7o#õ›¶ÂÐÈw寮6æ+òórùmõ\uýˆëW¿ñz ¤u—Ð74ÂÈÄ”³Gö`bfÉ[ãgáèVcS3^:€þتn#';“õó'cnmÇÛ‹°sªƒ¥�=ƒ'ÌáÄþ_),,Ä£A3òóòX6å¢Ã5ý{ü:õÄØÔœz>-yuôTeÿ¾Ÿ#„J:wîœ0Ð�%'âI 33„B!D•=ÿÖvþ°ˆë²cÝBœ=¼éØûº½ü6†šo} :¯ 3µP† {·r;6‚®ý‡beç¨ql½¢cB.Ÿ½[?ø‚: iÙé9�ãGMÿ¾Dÿ,¬í”wiê²³‡÷�’O·ochd¢.71³ÀÑÍ“°k—ð( [NØÎ­«iä×�-Új´]¿i+n^9'„JÊÈÈPÿ·o»ž2 ¥hâ×¹\ÇÉ !a†B!„å P(è3èCº¼4”3‡wsúà.Ÿ<ÌÏËÿÇÁßÖ1yù¬í�J 3ÎÙ @óvÝJþÀj`@^nN±0CY¿cïWîÛ·‚‚b°wñP—_¢Z‘@CÔff!„Bˆ‡ÂÚÞ‰w1ö…æW—'D‡“š|K[l\H»£Ü/Aµ¤¸°k—€»Ë9’âcHNˆÅÊÎQc¶À¡ß~`Ýœ ÔmÔ‚‘ÿûwï&™°mÅLvþ°ˆ:^wà Õl W�6R’HŒ�ÄÜÊ;§:ýûv†Æ&rsE�!�†¨­dP!„BQiß~1‚û6#>Jûƒ†Æ¦ÀÝ¥"pw‰ˆjóOcS @s)‰Ê…öЪ³rù�jF…jsÎâölZÀ;Sãݬµz/Œë—NPÇûîÌ�‚|å²]}ÍWµž>´SÙ~±þééõ_!÷\Ô<²)¨¨�$ÌB!„•¦§o@Jb<ÿþù‹Ö¯Ÿý{M[ßݸ0¼èM$Å—”4öï@Pà?õoÇFrò¯í¸z6¯S/àîæŸ [”8ß�۱ػÜý­sصK\»p7Ou¹ƒ[]â#CÕeÙYìݨ DЇ-õ›´*jë²Æù.ÿ‹I¯u`ûÚòaÕš¢¶‘0C!D�4ùfªÖÿMºq‡�oñú†�4ð(üœU©S™6d„xÐ^:s+[¶¯]ÀÞM˹AnN6·c#Ù¿u5ëçMÂÖÑ��£>/Q7%1ž¬ŒtŽü#S~Z2•РóädgróÊ9M„‘‰)#§­DGW·(Ì8_f”œ™Ñз]GNv&—N"ä¿@tuK®®~öÿ°mÅ,R“oz•� ¦`ne£ 3Ý Kú½3=}6, üúdgfpöャüj4·c"qײׇhñðÈžB!j…ŽÆV6Ômß™ºí;Óð¹Ø:âu óóep„x\ëòåÚýìÞ°”ÃÛ7ðËÊÙäçalf�“{}z¿9š®ý‡alj®®Ó±÷+œ>´ƒc{&ìú|±j/vÎu˜¼|ÛVÎäë_&'+gš·ëFßÁc±´uP×/k™Éà sX;û~[5‡}›Wòl¿!ô4†œ¬Lo_Ï×c0tâ<¼›·¡ËKCÈLOaßæ•Lxù)´xŠ7>šÎô}ðlÜRÝný¦­øvÛ×ÌgöýÉJOÅÌÊßÝéùÚû¸Õ÷‘ƒ¨1�”ÜCC 3„BˆGhF=s�?ë`áì†Ïó/Ñaô'xwëMëÁ#8¹ziµì¯µ�µ½3oŒý_¹�·wqgúú#%Êëx5fì×?Þ·þ‚ßÏ—ú5ÆÍÛP¢¼Ç+ÃéñÊp�2Õëdû úP]–NJb< ›k¼=”35ÆÌ^'7\ÔxÚ�—^zIFÔ(²ÌD!D­’Ÿ“CÒ­þY6—ýÓ&Ьÿë20B «¦ÈG/ú¢Q~ø÷õ´{®¿ ’¨Õî]r²uëV e`D�!33„BÔZÁì ×ŒÅØzz—øšj¯‰ÒfJÜïëu;‚„kA|׫-Ý?›�Ïó/a`bœ&Nò!5–¥­É 1ü8oC'ÍÇÐØ„ÓwòÇO+ðjÖš.ý†È ‰'"Ðå �œ囄$Ðf!„�™BW9±àî—Q�—‡w·Þ øv#Š¢Íì6¦ËÄi¸úú³íý7+öÃdÏyé›uíéàÐȇFMhùêÖüß3¤ÆD•¨›Ÿ“�G»§yuí¯è{õ¥SS_^\´š¼œ‚÷þ@óoðü¬¥ç1µ³Ç»ûóxwëÍS?æÌ+5ÚÏËÊ@ߨ˜vÃÇâ?ø=ù`‰Zá¥á“0³´áŸ½?3åͧÉÏËÇÞÕ�ç}@ï×G¡o`(ƒ$ž¸@C¡PPXXH®ž<& 3„BˆÇ¦A÷>Ä]¹øÀÚ,,(๩s9·y'W/%éVF–V4}ñºNžNÞ/âõìs\?øG¹Û|æãÏPèêrüÛùœÝ°ŠÔØhô�Œ©Ó¦Ï}9KWw:ü9;?¡%ÌÈ¡÷Ì%œÿùŽ-�Kz|,6õ¼è=s n­ÚÒzÈ‚÷þŽ•{=zM_À?Ëær~Ë�¤ÄDbfïD“^æé±“éþÙln=Hbȵ»íçæ…¦ø½ù»'}Àåß·<зÄñ8èèèÐóµ÷éùÚû2â‰gëàŠ¾�¹9Y( òud7Qþ—!BQ«þaÓÓêN]žzw ]'O pÓšÖ¾®��'Ùð!·o\¥ /�ŒÛ œ\½”¿S†Mþï• µiU§Ç–ÎåND¹¹d§¦pý¯=ü:z0Yw’0³×¾¤ÃÀÌœðSÿ°wÊG¤FGR�—Gµ þš1›¢%6þƒ†£k`ÈáùÿãМ/IºB~v6w"nñϲ¹ü½x:zz´|m¨æ Ц›ØÚqußNÎý´–ÜÌ r‹^§)„¢fKˆ g÷æ%äædýµ_ˆQÑ’!ª3™™!„¢FSíAQšs›×qñ—Môœg~üNkù•�¿ÐnÄ8œ›µ¬P{·o\ÅÁ§)½g.aÿ´‰¤ÅŨ¿uî4ó}ÝˬjͲeñWÿÀÔÆ�ö�¸øËF­m\úm3�džÇSK=Ï¥í[ä'„µˆ¶ CQ�fÞ½úT‰²Âü|²SSI�Ž&æü®ýñ±/VËñUõ¹kù°=f!„¨] ÉJI&úâ97®&hÏoüqA—´–'†ÞÀÌ¡bcîüd¯oØAã>ýñéÝ�¨ó§ ýç0¡Çvò…÷Ùóãö�k%ÊrÒ”!�j +7e òÁñà2Û²ò¨Wê×’nÞ�Ï—BÔ÷Ý»wçÏ?ÿ¬öýVèêbde…‘•ö>>4{õ"OŸæÀÔ©¤ÅÄV¹ýf¯ ¤ã'ŸHQH˜!„¢F+ím#“*(¸WnfúFÆj/æòyVt÷§Í°Ñø<ß×–mpmÙ†£>!->–#ó¦qnóºRëçegÝ÷&fåê‹¡iéã™™œ(¸&9!–�^l��¡1Ëÿ¼ŽN±Í_k“”¤>ìÓ {íx<¿¡MŠ�aÜÿùbjaÍ7{®È‡OÔ¨ cРAØÚÚVÛ0£x° ÐÕÅÐÜ[//ê=Ó™F}ûâêïÏKkÖðË�¡¤ÅV-а÷ñ‘ˆ„B!Dͤ£¯_ö?žFÆZ÷ŒÐ76 §ûI¤ÇÇqpöçœý9ÖuëãùtW|z÷Ãý©Žôžõ º¥.o)�œŒ4 Í-™×Â�ì”;r“Ÿ¡Á¨ãÕø�i)I\=÷/~O÷z,×uïù#C‚p÷núØÆúVÑX×mØ\>x¢Æ:t ((¨Fô¿0?Ÿ¬äd"OŸ&òôiέ_O¯yó°kØ�î3¦óëÛïT-Ìh$a†„B!Dµþi¨ t ÉÏÎÖø’]ýeVµónDôù3%ÊU›m¦DGV©kI¡78zƒ3?¬¤åkCé5c1mÞ]¥0#)4§f-±õô&êÜi¹ÿOˆ[ÁçðhØì�µY�ŸÏ¬Qýhâÿôc 3´�ß§UGÖ‹y¬c­ Žê6’0CÔ¼ £&K‹‰eç˜y}ÛVœZ´À½}{ÂþùGýu#++ZŒG‡˜;;££§GFÂm¢Îžáìšµ$ݼ @Ë!ƒi;z´ºžj¿‹?'p}ß¾ µ¥�[›6ø ‚]Æèr'<‚àÝ»¹°qyšËI+zÇfÍhþúk85kމ­ yÙÙ¤ÆÄpóàA.nÞBVrr‰þ86mJ‹7ßÄÙ×#+K²SRˆ½x‰ó7uæL�¸÷ò6!„O¤ì¢¥"N�[”øZÛ÷>*³®ßëô–7é;€¨ó ^\¸Š1'®áâë_âk—wlÀÜÉ¥J×{óØAåµ½;Fë×=;wcÄ�@:�ÿL>µˆú»a‹Öæ�_×D=ßÇrM�ûü¥�õù¢0£…|ð„�XæíÛ\üi³òß³.]ÔåfŽŽ ܸß7ßĺ^=ôŒŒÐÑÓÃÌÉ‘½{3àDZoܸ\ç¨l[yùÔ}úiú,YŒkëÖZX khˆ�W}Ú�ù€î3fTé<ž]ºÐïûïðêÞ3'Gtôõ103ÃÖË ÿwßeà¦�˜:8hœ£aŸ>ô[õ=õ»uÅÄÎ==Œml¨Ûùi^üv9M¬÷]ff!„x"Å_ÆÍ¿Ý>›ÉžÉc¸r ;Ú�ü7¿6dÝIÂÈÒZË%yÔmß™®38¿ù’#najgOÃç^ Í°Q{ŠB�™ƒÿ·x û¾ø˜ð3ÿ’“ž†…³+íF(ƒ•ø ËUºÞ³Vá?x�z÷ãÅ…«8²pw"Ã0±²¡A�¾t™øfæš[ʇ£š»pü/þÜò!WÉÉÎÂÁÕƒŽ½^¡û+ÃÑÓÓ\"u+X¹„�£+ßN}Ÿ‹ÿ  ?Ÿö=ðÊè/00¼»¿Ë‘9ºû'¢o]''+;ç:tè5�^oŒB¡P0uhwn]½»ÅŠ/G²eÙ4æýz–÷»×ÇØÔœY?gã¢Ï8sh¶NnôoR¹ûz•íkæsåì12ÓR±ur£cïWèùÚûèêé•zþù¿òÑ‹-HNˆeö–8¸z(Ü¢ÃÙµ~ —N$)>Ccê7ñ§ï�±x7ÓÜØodoP(˜¹ñ(?Λħ�``dL×þÃè;xl¹îË--ÁQyîUDHŸ½õ Žu<™õÓ?m¦§$óq::ÌÝz sK2ÒRؽþNÚÉíØH �LðnÞš>ƒÆR¿‰Ÿºnaaa©÷å˵ûåI‚ŒZd¨Ü<|˜Vï¼�³ïÝ ÓÿÝw0up îÒeŽÎ›Çíë×°kЀŽ�ÇÞLJ¶£F±cÔ(×®#píºRß@R‘¶4ÐiÂ'\ùýwÎoÜHJD†æx÷êEûÇàÙ¥ ;pëè±J�ç©‘ï£ÐÕ%pí:.oÛFz|‚Iדõ÷eüÞx‡¼ì,þš9¥J×{'"Œ�ŸŒ  7—&/äýƒç˜x5‘1'¯Óó 003'æÒ9ÏûJ>ÕØñ?¶±ð“7ÉÎÊàóï÷²xçe¼›?Å–eÓøeÅLÍ{žOR|4z†œ>´“ÞoŒfÎÖS´éú"~YËŽµ ÕÇnývkf�ÃÝ» 37eζS8Öñäçåÿã·ïç0uÍŸÌÿíf–6¬9Ãüß¹Avf®õønÚ4mÓ™¿Ÿ£kÿaåîëõK§ùòíçÈËÍeÒÒߘÿ{ ž�[²õÛéüúýì2ÏŸœKrB,¦Öê #ä¿@>Ü…—Î0bê·,ý#˜O—l#1.’Ù£_âÚ…“êsߎ�$3=k{'~[=—×ÇNcêšýèèèòËÊY\=â¾÷%)>†ä„XÌ,m°s®S¡{åèV]]¢ÂÈÏËÓh÷¯_Ö�•‘F��Ã11·$.ò_ éÊß;72pÔç,Þy‰ñó7}ë:3G¾HPà?Å®Kû}™´ìwùF’ £Ö)ÛÚ¨Ë,ÜÜÈMO篩_{ñ"y™™äefsþ<¿š¦ülV¾½v*Û–Ž¾>1.pxÆL’CoQ�—OfR6näÜ�ëðîÙ«Òç±puàÌš5¤FGS�—GNZ:·þþ›}“&‘�’‚‰��úø¦/D×À€“ß®àÄÒe¤DD�Ÿ“CjTg׬áôwß¡£§Kã~ýªý=—0C!Äé¿ÛØ1~8±W.’—�EVJ2·Žaã[/zì9™ÊM<õŠÞL¢gh¤ 322¸qøO6¾Ù—›G’u'‰¼ì,â®\bß—Ÿ°ëÓQ• V=ßžãßÎ'áz9i©äçäp'2Œ Û6°ºo'ÂN­ò5_Ùõ+«útà¶ ¤D†“Ÿ“CnF:1ùkFëúw%;5E>ÕØÙ#{01³ä­ñ³pt«‡±©/À?lÕ8Vµ_†�¡ýß›„»wLÌ,è3èCNþu÷¡öÀ/kxiø$Ì,m°°¶ãÕ¦bbn©^>p3H&ßä2âºòÍá7þ£C¯�´îòú†F\ü÷@¹úš——Ëw_�ÆÌÒ†÷¾XŠ“{}Ì,¬0"#3®_:SæùCïÙx3';“eSÞ¥°°�qó6R¿i+ �Lp÷nÊkc¾"?/—ßVÏU׿þŸ:�ðÞdl\ppõÀ·C�¢`äì}ï‹¶Í?Ë{¯ô ±wñ ??�„˜puyNv&ûþc3 z |—ü¼<–My‡„èpFMÿ¿N=165§žOK^=Uy]EÁSY÷ÅÈÄT¾‘$Ȩ•ת~£˜ñÝgÛG¼Ï÷�Ÿ!9´ä ƒÄ�åñ&&åj¿*m]úy«Öòëû•o�qhìSéó$…†ðLÀd�Ð îÒeVwéÊÎѨËÜZ+—´ïÚ¥µOW÷ìÀ¥•_µ¿ç²ÌD!D�ô ^Ézñ—M¥. Yѵ•ÆŸsÒÓ4Îvâh¹†{ûYZ¿Óâb88û Îþâ���¶¯Ç_½ÂÎ�G<ò±ƨéß—(³°Vþðš•‘¦QZ´Ä¤]�þ˜YÜ]2eíà (g$¨›’™žÊ�K§iÖV¹ÞÜÁµ.K÷k¶Tr_ˆâaBËNÏU¸¯ÿîû…¸ÈPú‹~Qh`mïÄò?¯ßÿüê2e�ðÏÞ­ÜŽ� kÿ¡XÙ9jÔ¯WT/äòÙ¡O‡^1µ°º{°B€ŽÎýßswóÏ•ºWÎ^Ƈ‚£[=ŽìØDjòm^:sKþÝ÷ ·®^¤‘_´h«Q¿~SåßW7¯œ»ï}© ¢Â®É7»†FØ9Ö‘ £†ædßÑ|c—‘•%M_ˆ[›Ö˜9:blm�ŽžŠJ¼á©²mݾ¦ý3}'L`ÞBTä<¾ü’–-ëGêwëFÜåËDž:EÄÉSDR˜¯¹¹¨¹‹r®A»w•Ùg 77 3„B!ÄÃQPP þm¾½‹‡Æ×T³·~Z£­:Uª¯ª6�=¼Êfh;¿*̸uUùç:^MJÔOŠW>äXÛ;—¨ïÙDsJµj³Q�÷­í--33*r¯\ê*_®œB~â¯ßHˆWÏÊ(ºü´d*?-™ªýaÎÉí¾÷¥ºòõõe×®]òM~ÙÙ™d”³«rVYñ׊¶3S{{R££9±t)ÑçΓuç¹¹äçóþ©“ån¿*mé’—YòÞé-aÍ̪Òy2oßæß%KøwÉ,ëÔ¡N»vÔïÖ??ž™2]}.ýüsѹ2103cÕ3Ï’“–V£ï¹Ÿ° IDAT„B!„5À¡ß~`Ýœ ÔmÔ‚‘ÿûwï&™°mÅLvþ°ˆ:^w�S“o“X´ŒÄÜJsúrTèU­éþÏôÁÿ™>\=‚M‹?'èì?¬ž1–±s”Ô%D‡“š|K[u¢ÚøÒÊÎk{§JõõÎí8,mÊ~@Ór~ÕæŸæV¶Ø9)§ß§ÝIÐaT®]î.ËPÕ·°±×8’â£IIŒÇÊÎ KÛ²û¥s+[l‹Â„Š\¿2Èñ .R¹F~φ¥ê½2TT×õíþ �MÊÕ§{ïKuæîîN@@¥üûI·`Á‚r÷¤6õëÓ¨Oàîžpwˆ�|Pb 3§Š}oT¥-OOâ.—|+™•‡r–VZlÌëó�ðpsiË÷ëGç€É´xãuu˜q'<{¬êzwér�¾ïf!„BÔ{6-à�)‹q­×P]~ýÒiêxß�‰ š• ££¹ß»êMMÚtÖzž-žb̬uŒû?_.Ÿþ»X›%—R¨~ûo0R‘¾™š“ž’tßë×v~ÕL �beƦ¤§$‘—›ƒ®žæ�ºþQ¾’´UçÞõï]Tt] +7+£"ׯ 3”³RnÇ„sñßDܸ CÆiìá¡§o \£ Ü}*Ϭ’êhˆÊ{Rƒ +zÍ›‡Ž¾>7öïרŸB·hiYz\|‰z­‡¿ …… P £§GÁ=oRèêjì7Q•¶š¼ô’Ö0Ãë9åFÃÅ¿VÑótûß4\ýýÙóñÇ%‰kûöÑ9`²Æ’•ˆ'±÷ñÁ÷Í·Ø7qb‰sÔi׎NŸ|Ìõ?÷srùòj}ïåm&B!„5À�۱ػÜ}à »vIN¸yú{˜½¨þïðÿ©ÿ;7'›“ýŽ©¹­:?À–eÓøt`[õŒ €Â¢½,­ïþ^ôvŒâË7T¡‰GÕî«[}åÇE†j.c_hκ9ÊuþzÅ‚„Æþ4^S Ê OOþµWÏFøuê¥Q¿î=ýW_WƒæåYJ¾É¤"×`bf�•�#IñÑìÝ´cSszí•¡R¿I«¢v4V.ÿ‹I¯u`ûÚ÷½/B‚ŒÚBßÔǦMi?v,/¯ÿ 7Wsxú �ã’n)g6´= #+Kt phÒ„žs¾ÆÀÔŒ”Hå 6Ï®]ÐÑ×îîcQ¿[Wtôô0¶±©t[yù¸¶ö§ýرX׫‡ž‘!æÎδxãuZ¼öÁ»vW¡Ï Lììè>}:;``f†BWsggÚ} |‹Éíëw7S¾¼myYYÔïÖ•nÿ›†e�:èèéabgK“ýynÖL,ÝÝ103«öŸ™™!„BQ4ômÇ…ãqð×u<Ûo0WÏŸ !:]]=òór5ŽUýf¾M—øiñ¼7u9FÆl[1ƒ¤øh†MZp÷�……ÄE†²iñ¼°]==~^þ?€Ô)‰ñde¤cdbªž-qï †Šôõù7Gø;Ö.à­ñ³Hˆ gõÌ�HOI¦eÇå<ÿÝ aàÈϹøï~Z2K{\ê5 2$˜5³ÆcdbÊÈi+Ñ)z#€º~£æ÷ŒŸöëÒf”Ü”´"ׯâìáÍ•3Gùïôßôò‘æ›U€~ïL øÜq6,àíÉ qp­ËåÓGX=ã#r²2q×yÊß!AFMðþéSe~=òÔ)öMšLvjªFùùõèö¿i48�¦ªËÓbbùeØ0Ú~0 77ºOŸÀrÿÖÄ]þ—V~Ê2e1Ëý[W¸­mÛ…#™™5›^óçÓâÍ7Jô=hÇ�}>*zžõ}_À­Mk,\]é½pa‰öó³³9¾h±úÏ©Ñјú%Ýþ7 ïž=ñîÙ³D�ø  N._&a†B!„¨ºÁæ°vö'ü¶jû6¯äÙ~Cè3h 9Y™Þ¾ž¯Ç `èÄyx7oCÈ•@ú�àÜÑ?˜5êÿHIJÀɽ>ïM]NÛîýÔ퀡‰)ÇÿØÆ'Ú`lj†cOÞ™²˜½îþ ݱ÷+œ>´ƒc{&ìú|±jo©Ë*Ò×fm»0vÎz~^þ?ƾÐcSsêùøòNÀbõÞ÷;ñ Áι“—ï`ÛÊ™|ýáËädebãàLóvÝè;x¬Æ¥õ?´Ë4Ôm T*rý*.u•a†±©9Ͻò^‰óÔoÚŠ€ow°}Í|fП¬ôT̬lðíÐ�ž¯½¯žáRÖu=L9éɸººÊ7«]^V6·ˆ9ž«{ö~ü¸Öã®íÝ‹‘¥Í^ys2nqò§V¬$=.ŽS+Vb]¯¶õë«ß‚rxæ ž ÀÞLJ‚¼|’CC+Õ–nÑ£r33 ûçvŒI«·ßÆÞ§z††$‡…qå÷ß¹´åç*õ95:š-¯¿AóW_¥nç§1sp@×À€ô„¢Îœ!ð‡I Ñ8Ç�ýûIºyß·ÞÄÕß[[ òòH åúû¸°y3¹¹Õþs (,,,”¿„BÜkΜ9\¿~ƒÛ18Ÿ9 "ª­$Ϧ¤x)$W¬XñÈÎûÞ{ÊN׿ÏàÞ²‹ÜQ%_�À•3Gyùý)ô~st�ëÿ™­óÉIO¦yóæŒ5JnèCúûÆ·]OZuèýP‚Œ   õF£žÁÁX¤¥ËÀ‹Ç.ÚÙ‰X­ÿÆËžÿßÞ½‡GUßyÿr™K 3B’$$�B˜A,$´h$´*›¸ÖRm5éÚÖj­ÝÖô¶ ­»­O-Ö^žª­­5 ¶kh‹+ÖF-I´JRA !PÈ…‹$Èe&Âþf$d€0¹Í$ï×?2g~çÌ9ß3óñûû`í~û ½ÿÎߟ0WŸÈùRÀ�ÿ‰Cû=�v»�: ÆjGp.¦™# ««Sµ{ßÓ?¼WæˆHÝýÃ'P×ÐéjÓ�l–$ ÂŒa@�ô ÌFÀÝòq?vXÉöT}î¾ir쌀:ÿNW›ö•<'牞§·dggËl6sc‡Ðñcõzï�­€3€ñ`aYÀžû‰Cûuà›=AFJJ ?ª‡AMõ‡�]&ÈÀXG˜0§ŽPmÅ �1£ñànOˆ!õ¹¹¹fd„Ò|ô€š� s ƒ–/_®¬¬,Š1Œ2€„>ˆ��U}}=…À˜“�� ‡Ã¡ÔÔTÖÈ&éééÚ²e Ap ÀEuNˆÒ‘EË)üV—qd~L­]»–â999ÊÉÉ¡À93u&$L퓦Pø  €W«W¯Ö³Ï>K!0ÒÒÒ(caÀ+›Í¦üü| ¿D @ !Ì…0 P3@@ái&\¦ššŽØçgggËf³q#€A°}ûv•––jùòår8a—©¨¨H{÷î±ÏõÕW•——Ç�hûöíÚ°aƒ$iïÞ½ºí¶Û´téR €0€ËÔÖÖ&I 5hRtì°}îñcõêìp©¡¡�› йA†›û5�ø? |4):Vÿö™{‡íó^üÃÏu´~…èÜ #4Ì ´Y*ùÛ êhwh@€`PŒçYŸý²æØ+ë³_Vh˜ARO‡ÆöíÛ)ø1 Œ Þ‚Œè˜8IRtL� Œz 2Ü4 pf`TëO�áF ��0£Öånàÿ30*ùd¸h€#ÌÀ¨3� Ã�@üaF•Á2Ü4À?f`ÔÌ Ã�@üaF…¡2Ü4À¿f à e�áF þƒ0m8‚ 7 ð„XÃd¸hÀÈ#Ì@@‰ Ã�@FaÎHn0r‚)`¬Ú³g�Oû9�Î=o§Óéó¹'%%qãðü!Èps/<ý˜:Ú]žóZºt)7 †a`L*..ÖÆ�òÜëëëõðÃû´ïªU«”‘‘ÁËŸ‚ 7 ~L3ÀG–ɱ÷y‹…‡€å�A†SN`xÑ™“222d±XTPP —ËåÙnK˜¯¹Ž«/¹˜Á(ë”øa=çÔåŸÖìy‹Õîºô4—ÝåÛTS½Óó:**J¹¹¹JNNææ# �dHR¨Á¨í{þ‚ã#&NÒÒY2˜ÌƒvÇ×éÝ·ÞPóÉãj0ª£½çï” 6Èd2Éápp`�fÆ,‡Ã¡ÄÄDª¬¬L’TS½S.§–]w«""ý¯‹áRJó‰F½þò3:RWíÙ–žž®ÌÌL™Ífn:Vaaa¯×-'›Ôr²é‚ãk¿$iù�Ÿ´søÇëÕÁ½»/kŸââb „€1Íl6+//O©©©*((PSS“ŽÔUkÓ†µ0ízÍ[”0ײëí-ÚQ²Y�=ÿW˜n Œ&™™™*//ïרºº:¹\.�:Ñ8¨çàîŠ2 Š‹ëßô–´´4n  $%''kÍš5***Ò–-[ÔÙáÒ›[7é`õN¿íÒpk8Z«×7?£¦†Cžmtc`´ÉÈÈè÷âµëÖ­Suuõ��K\\œòóó¹)0‚38Ël6+''Gv»½W—Fáoï—#íz-L»ÞïÎyGÉf•—lö¼Ž��Uvv6Ý`T#Ìà<î.�W_}UùË_$Iå%›u êŸZvý­Ã¾ð§7Þº1V®\©¬¬,n õ3ðÂl6+++ËÓ¥Q__¯¦†Czþ©u#Ú¥ár¶jwùë}º1rsse³Ù¸q`L Ìà"l6›Ö®]«^x¡O—ÆâôO)vzâ°�KýÁ*ýý¯¿WË© I7‹3èo]/oü¥æ.¼ZŽÔëd0ÝB›.g«ÊK_ÖîÛ<Û”——'«ÕÊÍcaýäîÒ(..VQQ‘\.—vïØ¦šê�úØ'o’.�ó»1 ƒ233ûýT€Ñˆ0€Ë”‘‘!»Ý®'Ÿ|RÕÕÕj9u|л4\ÎV½¹e“ªw¿åÙF7@ |`µZ•ŸŸ?$]öþS¯o~F�.Itcœ�0€¸P—†-a¾>þÉ[.«KÃålÕý½jªwz¶¥¤¤(77Wf³™bœE˜À¹»4ÊËËUPP —Ë¥šê�*¬¹_Ë®¿U3f/¸ä1¼ucäææÊápP`€óf0H‡UPP wß}W�.½úüíÒh>Ѩ²­¦à2f0ˆÌf³î¾ûn¯]K®½Y‰ó{Æîz{‹v”lötcDEEiõêÕtc\aCÀ[—Æ/?£½»Þ”=õ:U”¾¬#uÕžñéééÊÌ̤ 3"î.�ÊÊJ¨©©IGêªõòÆ_zÆDEE)77WÉÉÉ  Ÿ‚(C+99YkÖ¬Qzzz¯íéééZ³f AÀe¢3€a`6›•““#»Ý®ŠŠ ÙívB f0Œ’““ 1ˆi&  f€€B˜ a(„0Æ™L&ÏŸ»Çóœø§±ç{Ûç= ãl6› ƒ$édd$�_h‰—$Y,–>ï¹¼*//Waa¡Ž?N1®½öZ­^½šB€�ìv»ÊÊÊÔdµ(úƒdr:) FÌᘩêî‰,ÒÒÒú¼OgÀ«ââb‚ ”×^{�"Àdffzþ\3ݦ® ~.bd´�::mš¤ž)&‡£Ï:35¾­EæCû)ü–+j²:,S) �ÕjÕªU«´qãF¹ÌfU'ÎVBÕ^wwS ›Sáf˜5K’d0”››ëuaà¢Æ;[µ…€ßjºba ’ŒŒ ÕÖÖª¬¬L.³Y»çÏÓŒ}û4¡¥•â`ȉ‰Ñ‘i1ž×ÙÙÙ²Ùl^Çf<òòòd4µeËukR’¬G�jê¡Ãti`H´�ª�‹Së„Ivdx›^âF˜è%''GIII*((�ËåRÔ):n±hòÇd=z”Pƒ¢=4TGb¦ªÉjõl‹��Unnî;2Ü3}8ÅÇÇëÉ'ŸTuuµºƒƒudZŒ>˜M¨�ñbHÒÊ•+•••Õ¯cf¼²Z­ÊÏÏWee¥ŠŠŠú„OœPôÇxŒ+ú¥qÒ$·X<ÓIÜ–,Y¢ÌÌLYÏ 7.†0pQÉÉÉJNNîj4Y­j²ZehmÕ¤Æãšxò¤Â::(<ÚŒF·ô„ÝÁ½#_B 7 @¿œj”––ª¬¬L’ä2›uÈlÖ!ÅlÀ`œŒŒTgXX¯÷ ƒRSS•‘‘áSˆáF˜¸,îP#;;[¥¥¥*..VSS“¤¾ÁFxK‹Â››ÞÜ£T{h¨ZÂÃÕ®“‘‘}:0$)%%Ev»]K—.”Ï$ÌøÄl6+##Cª©©QEE…***T__/©'Øp™Íj˜2¥gü©fE´´ÈÜÜ,c[áF€r‡N“Q-áár™Í^ǹ »Ý.óÆøŠ00`6›M6›MYYYjhhPEE…ÊËËU]]íÓ:!âìâ�1’$Ck«ŒN§ŒmN…·´°�¨ê ’Ód’ÓhTKD„Z"½v^H=SHìv»‡$À8a`PY­VOdž$UVVª¢¢BUUUž® éÃÎ�¦sŸ 8Â::ennVhGko “öÐP9�¹Lfµ�ršŒ}Ö¼8_BB‚‡e³Ù†í\ 3CʽƆ[ee¥ªªªT[[«ªª*¹\.Ï{GOGPW—ŒmN…v´+¬£S!íí íè`ªŠÜ�¡¡ê Ssx¸N�ºàT‘sEEE)))IñññÃ^œ�00¬Î7T[[«ÚÚZíÙ³G���žE%©;8¸gŠŠ"¼Ï|ªY’dt¶)øt·'ìúô˜›ºÒ.Iž°¢=4D¡aê ½d—Źbcc/‹Å¢ÄÄDÅÇÇé´‘ËE˜QV«UV«U‡£×öÊÊJÕÕÕyÂŽóC·žu8>ü¯7!íí mï™®âîðp377÷ëî 7w8áÖf4êôøñ’$§ÉxÁu,.%!!A&“É\X,–^A“¿"ÌcBkk«êêê(ÄàdA`Ô8¿ƒÃ­¦¦Fmmmª««S[[›öìÙ#Iª««ë5eå\�aaž0 o‡GL¿ÎçÜ@dÐþÎ@ÑQQQ²X,²Z­²X,2™LŠ‹‹óH�Š0Œ ………*++£0 ¸×j¸PACCƒ$IUUU’¤¶¶6ÕÖÖöNg¯…HûëÜ@d¤ÅÆÆÊh4J’’’’$ÉTH ø°âR3£Â¸  ÍÍü´’>™©i)WÊd±JãÆ©õØQªxG;7ýAÕ¯½<¨ŸùÝõ´¤>03â¢ÛàÜÿ¨ÅØqEò|ŠŒQçþ�ïÏ” w§‡[cc£½Žuw� ÷ôoâãã=a…ûµ?­Y1Ò3Ï2+I7?ö´¬³ûþcebÜtMŒ›®9ÿö)ؾU›¾r»œ'Ž�Ê:,ºýKúÄ÷"H¹Ô÷erŒÒVÜH!F¹ ‘“4!ÊB!ôËH>•¾!Ì´¨³tûŸ‹e˜©S‡êTöøÏ´oëßtêH½‚‚ÆkÊÜ-ʽSsn¸I3–^£œõ›´þÓêîìuµ˜:ßÁ¢BÂ Š›9›BÀ‚( �ýûÏ'ÄH(Ù¦Ç?q•Þ^ÿk5ܯÓííêt¶©î�2=wO®žÿÚèÌéÓŠIY¨ÅwÜ3*k3�0Œ tfÖ¬«W(&e¡ZŽiÓÝ·©£µå‚cwýQÓì‹�8WMö÷yÆÒkôѼ»4Í~• &ÊÕ|RGvUhÇ3O¨ê•|®ÓWiÉ÷(îª4™"'Éyò„•¿¥7÷ˆjÊÞ¸à~ÓS—食¿Ûs^§×içŸ~¯ÒǦÓíí’¤Ô/]é߼߳�{ݎ羚«ÝEòéúÂ&LÔ7þY§†½•úÍõK´b̓š³òS 5™´î#Sùò |‘¼ò&IÒŽg~Û¯u0Šÿ÷Û^·/ù⽺ö;?èµÍ4ɪ+–eèŠe*yô!m]w¿Ïç™òé[µòG�hÜÙgÁK’Ù­Ù+VjvÆ úë÷ïÓ;ï³ßUŸ¿K+þûGÒ¸qžmQӯв¯ÿ·W¬ÔúU+<�ÆÅør}]®žG[†�Jýâ×´èö/ñ…~ƒi&€€ë¸J’´oë+>còœy=] gΨôW?Õ¯3®Ô�“£õè²ùÚºî~�éîVÚ—¿¡ig?ërEÚfêúþ\’TòèCzìšz0ÙªG>>O[×ݯî®.­Xó &]Ñ{ ëìde|÷uwuéå5_×OÓõã9“õÔêëÔ¸o�¦Îwèc_ù¦$©ô±‡{-úùÀÌ=03B»‹þäóõ�>»¦HˆÑ¬…Ÿ½C/}ç­›;…® @˜À@DL‰‘$5î«òù où¼Æ�¯Šg×k˃kÕ¸¯J]í.�¨= ’GÒ;OýF7N V}Χã/ºí‹¦mÿ@[×ÝïYÏãdÝA•<ú�ÞøÅ�,Çgòzíwåg¿ qãǫ䱟hÇÓ¿‘óÄqu¹œª}k»žûjž:�mŠ¿*mè®ïÌI’ÉbUÕ+/ªâÿ ÔélSg[+_<@˜€¯BŒ=ÏZïÀì¸E=�@ų뽾¿sÓzÆ]¹Ä§ãOO»ºç8þ½×÷w=÷lϸÅëµÝvöõû/mê³ÏÑÝ;µnî=�sý°\ß® ù²¿Âš€€ÕÞrJÆÈI2Lˆ”³©Ñ§cDÆõ×h¨¯`$ЙXï½°QRÏ£O#m3/9>vábÝñR©æÝ”ãÙv²®F’={Ž×}¬ É’¤gÇ]®¦û%I–óžVr)'jöì7+i@5êë „€€U÷v©”lSˆÑ¤U�ÿŸÂ£§\p씹)ºùѧ4yί”›oé³Ïä9óôÍÊcºýϯz=æ¸ñã‡íúFa  ½˜§Z>8¢è¤¹úÂ+o)õÎÿ”eV’‚à 2LŒRÌ‚+µbí�uÛÿ¦ð)1:²«B¯=¸Ö³ÿާ«î®.-Xõ9]“ÿ=EÚf*8Ì ¨³tõ7ÖȾúvuwuiÇ3¿õéüv<ó„:�mJ¾á&Ýø³'5c–‚BB=E o½CŸúåMš™ °ˆ‰½ö+ÿÃït¦»[ V}NiwÝ'Ó$«‚ FÙLÿþ‹'fPÝ;oöÚ§ÓÙ&IšsÃM ‘Ù=ä×0X3ÐNªÓú›3tó£Oiê|‡Ò¿õ?JÿÖÿx[õJ‘žÿÚêr9=Ûª+UüÃïèß[§´»îSÚ]÷õÞéÌÿàÛjØ[éÓù�¬«Ñ‹ùwêÆŸ>¡�ܘ­�ܘÝgÌ‘]Úö“Þç|lÏnmyp­®ýÎtMþ÷tMþ÷z½ßP]©í¿\×kÛáwwx·fF éõŒ @À;YwP¿ËZ¦änÒœnÒ4û•2[¢5.(HÍG©æ%z{ý¯udg¹×ýß.ø•ŽíÙ­ÅwÜ£iöE=�z=Ѥúezó‰GT{vª†¯ÞÿË&5ì­Ôâ/Þ«K–É=EÝ]�jÜW¥÷Šþ¨·×ÿJ§;:úìWöøÏõÁž÷ôÑÏEÓ,T¨)\§×éý—žÓöGÖõYÐsóÝ«ø…¦Îw¨»«ËóØÕ¡¾> |TùÒ&U¾´É§}–¾®ƒ¥¯_Ö>ÌŒè×6I:Võ¾^¼ïÎË>¯ýÛŠµ[q¿Æ6î«ÒS«¯Òë𬙠a(L3cJG»Kõª)|úîüaSŽpX/<ý… €1ÍŒ V«•"`P˜L&Š#ŒÎ 0&äåå)55•B`Àâãã)Œ0šáNÚÈIDAT 0f$''SF¦™€€B˜ a(„  f€€B˜ a(Á”p1§Má:1k>…€ßj�šLc3uÚ®“„ð#L3x•––FPRRR(cĸ3gΜ¡ PЙ a(„  f€€òÿñGÅ8ýž–IEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2493567 ceilometer-27.0.0.0rc1/doc/source/contributor/3-Pipeline.png0000664000175000017500000006146115250267452022370 0ustar00zuulzuul‰PNG  IHDR©­ôÑ2ŒbKGDÿÿÿ ½§“ pHYs  šœtIMEâ (’7¼$ IDATxÚìÝ}XÔu¾ÿñ—¡ ƒ3‚Œ ƒ &(– ìŠm(®TgEK¬=ii¶Én›•î^'ovm;µvw¶:»éžë¬•¶»G¯³vcvÎnâzÓ¯Mó†ÎIO€ZAh"È� ‚3‰Ð’¿?p¾ÌÀp§€3ð|\WWÃðýÎ|¿ïï0Îk>wý.^¼xQø€k(€� !à«ú·õËÊÊJUVVR%@—°Ùl²Ùlt>¤8p@7n¤B€.µpáBMž<™B¯ZíîK * ;ðï hKÿŽl4 ®N�õõT pYêõµÉD!@ׄ԰Ê*ÙËʨಔÙíªˆ´SÐ.f÷R ¤©R„T©B*„T!B*€� !@H€� ¤@Hà’þÝùàq3gjTútIRñþýúô­-Úoúo×H’ŽïÜ¥ÂíÛ»ü¸LV«ê�ûÂââ”ü“%’¤ƒ¿[«ªÂÂû|{ñbÅÍœáqÿ¶?Ðæ~W“·óLþÉO«ªÂc:ø»ßñà',v»œee„Ô+®ð¤$IRxR’*>ÎéPˆsíSž“ÛåÇtÝœ;5aáBmΘíq Åb8,.NáI‰Fwæâ}ûÛÜÞõ<îçä~}ª>/Ôé¼¼vÏ·£×=ltœqlÎò Uäævhb!‹Ý®ðÄDY"Â=ös¿Žmu­µØíŠž2ÙxîÖ®Eókâz�u´.®ý\×sÀÀ�^kÜÙ¿€�zÉéœ}ºÅ¢ëæÜyÅÝ~£SS•¼d‰,öˆ¿s–•+û…<ºäŽ�Õ÷.Íì2ýß~k„ç¼ <¶y÷‘¥*ÏÉѨôôcX]ûI�Ý|½í×\ò’%5#½E7Ü:‡Cùo¿­Ü ½†›äŸ,i1Y“$%d6îû/½ì1ûqòObIŠ›9Ø�ØÕ%ÙµMEn®×®¿IIJY¹²Em23å,+×ÁµkU¼o_‹ý\uy÷‘¥’¤ï>ó´×óm~Ìe²Z5öÎTü�w¶Ú�¹"'G{žø¥× l²Z½N~%Iy6¨"'׸ŽÞºo·v=2Õf]\õÎݰQ§sr¼Ö¶#×2,.ΣƮ×YtjªRVþÜkMÚ:. Ï‡TI:¼q£¢S§È¡ÄÌ…*Ù·¯Ó­=Í'?rµ„-dö}ï·k”ý¿úë�NUäæj`x¸,Æ~’t¾¢¢Õç:_ÑØÒ8Ðb´¦vf“ÕªX³ÚèÞì,/WÅ¥pž”$KD„23¡ì^ðŠî¡Úý]Çb²Z•²òçªs8ŒRõyc-]áÆY^®¯Ú8¿æâfÎTÊÊŸ·YÛ´gŸñ¨msÃ’’Œëãzþ¶Ž¹£š×ñ«Š ãØÇƳ,O{æiíº”],v»1™”kÿ³Ç.ÍÌœ˜¨„ÌÌv[•ÿaÍjcW]­Ö×%lt\§ê⺖®s«w:�®æõN§PÓž}Ƹïì±c*ÿ8GßJò8®Ý�?AP!Õ›:‡CÙÏ¿`°Îvû�p @g �i÷Oxtñ ‹‹ÓäGÕà¸X¥¬ü¹ª>ÿ\U……ª*,Ô®zÄ#àvd¡ÂíÛU¸}»GhìÌÄC6�¿=¸v­Ç:±&«U—,VìŒÆÖÎ/vî4ZÇRV®4‚Ç»�,m ÜÃäusçÄ5ëðv¿/I:¶cg‡[«Þjm-v»ÒžyƨmkA31s¡ê�Níyü �Ve÷@•�¹°S¡)næL£ŽŸny»Åú®îÇž”¤°¸8�šMvkil$Ãââ4íÙgZ'íú¢Ádµz=/“Õª”GW*zÊ¥¬ü¹œee^[Ó£§Li·.ÉK–´¸–Óÿí· OLÔÙcÇZ¼ö’—,1BóžÇŸ0Z�ó64Öä¶W×+Ðbñx\À×õøÄIå99útËÛF@HÈÌì𾲤)¼-]Úb bUa¡v?ñ„ÑÒ”ðÃÌ«VX“ÕªëæÜy),îð¨®À~híKÆ±Žš‘n„EWwÐCk_òÚÂW¸}»Ñ¢ëÞ%ôJŒ�;Ǩmó€*Iβ2½»t©q¼®só¦y“¤â}ûtlÇãºwFü�ÿØx åå-ªëØrÝÂxØèÑ!ÔÕE÷àÚµ-Z:« õîÒemR¥Æn¿ÎòrIŸí×b·Ûóò¡Üýù«Õ(zÊ”«öá<:5Õ¸ýÙ¥PÞ\�áÏ¿ ƒk×Û”çäèÝG–¶ÛE³ü㜮=Þ)SŒÐÔÚDu‡ŽíØÙŽ“’d±Û[ls¶ð˜×–ÄÆ�Yáâ;jϿԻ�,՞ǟhu÷Y—ƒÝ&È5c†qûø¥c÷öšqÅæbÓ¿<¨ÈÍmõ¼êã‹oc^]»#u‰heÿöB¼·znß®ÿž7_»þé‘.™° èµ!ÕÕúä’òèÊv÷Û4ÃnI;]Oçæyݯ'¹¥¶Æ;ïÛ§OßÚâ±MyNŽŠ÷ík,L—Æ@&f.4ÂSWpFîµóƽöÞj[ÿ•³Õ}ÝÇÿv溸ºÐz«cX\œbgÌЄûîóº¯kVäŠÜÜ6ƒš·ó6Y­Fke½Ã¡ðÄÄVÿë§‹mͶƟι¼/Ü[¦ïÚúŽÒž}F×͹Óë—€¿èµžØÕí·£³ývkmuu­l+Ô\m®å@:3ÑRó€5eŠ¢S§“õ„öjç^ûÁqq�ëx¥×%")IQS¦(ltœ†Gt¨{ìÀaá—}lîA::5Õ£uÜZû’ÂâF“z¹Ž1ù'?‘³¬\Åûöé³ÿú/Ÿø{|>¤J—?Ûogf¬õGq3gêÛ‹öÚ…³"7WåçÈ®X·®¬=¥þ«¯zü9ÛZ>Æ5còéÜ<�™‰]:;γ+ ¸´†jw«s8”õã+næLE¥N1ºl»Îûº¹stÝÜ9úô­-:¸v-ïv ¤väCvGgûýÚ­éàØØVÇ÷ùšŽ¶ä¹„ÅÅa«ÞéÔ±;U²oŸœ—–\qéÌ„S]ÉâÖ�ùl'—º\îµ"7WÇwìl1‹nk]\+rs;4¹T{Á²ùì̾Ä5 µt©µ95UIIF ëusç¨úøñËZŸèS!UòÞí×›³Ÿz’6Bê0··öºwÛy}œc¬Ui²Z[7s¦¢¦LÖÙÂBånب±n³æz[~Ƴ£ãºìX�n-ÓÃ’’ÚüÀ= ~ݵuŸ�·xÿ~íþçÇÛ ÏçVV¦ðÄD…ŽÕæóxk¥u¯ÃÕÛܞ毭òœã¸Ý—*ŠJ�BH€_¸Æ¢ùl¿­…Yc¹–ôém>žkR!gyy‡ºw÷VƶÆ2F¥NQtjª1 ­+ž-<Öæ±KHèº�ZVfÔ?:uJ›Ûºj_ïtöHkv [ çgm´dkeÌnñ¾ýF˜‹›9³Õ ×ÚkªxãþQíÌ=ùÑ•úÁî÷u×ÖwzdFéèÔTãùZkE.ܾ]βòu©íh>ÛokÜ—?i-p\7wŽ1ñX+Kޏ‚Iw*Þ·Ï~î»ÏëóE$%ã]Çê âƒãb[=Æ”•+;tü� &®ç‹‹Óu—ÖLm.næL£Å±­Úv—¨VÂ~X\œÆ^ZKÕÛu8[ظ<Í·?Übæ]“Õª”•?oµž%n!÷Û‹·úü®ñÁÕÇ�÷Èr/îKî´:³q\œñ·Põy¡ÐßWĽÛokoÜhŒµKYùs MHÐñ;ôÕéÓ8l˜®›;Çhµ<[x¬ÅlÁîK L{æiUää¨"'·ÛZ]ãm-öݺ~½ò6nÔÙÂB 8PßJRü�wÁ4ÿí·�Pä ®ÓžyZ·ÖhQ�ž2E±3g(:5UõNg«!ôlá1 Ž‹ml¼xQ_åÔgoÿW›á)oÃ�HMÕà¸X%/Y¢ðÄD}úÖ£¶£fÌPÜÌÆ æ,/×á�{ìuá:×QéÓu¶°P%û÷«Îá�ÅnWlútÅßy§LVk«59ðüóúÞo×Èdµê{¿]s)¸*8fÀ©@Ïr–•éÓ·¶PÀ‡1q€� !@H€� ¤@HR ¤©R„T©B*„T!B*„T!B*€� !@H€� ¤@HR ¤©R„T©B*„T©B*„T!B*€� !@H€� ¤@HR ¤©R ¤©R„T©B*„T!B*€� !@H€� ¤@HR ¤@HR ¤©R„T©B*„T!B*€� !@H€� !@H€� ¤@HR ¤©R„T©B*„T!B*€� !B*€� !@H€� ¤@HR ¤z¡þ”:Ïi±H’¾d¥𕥩NŸþ¹²²²( üÚ˜1cŒÛf³YÑÑÑ©ÐýjÍf9­�7ëkS jTÃ5tB¹ùùùÊÏϧèul6›l6›¢££­1cÆÈf³Q ¤Àå«7™tÖ&‡ÅâHC‚M²G Vt¤MÁf“ÆÄEJ’âc#)Ðå+5n×ÖÖ«øÔUžuêL•CöïSí…:#¸ºkJJŠ‚ƒƒ)R ý`Z2H•6›.\úi¢â£7\ÑÇÈ6ØB¡šA•4~¤ÇÏÅ¥•*>U©‚c¥Ê?vL999Ú¼y³’’’”””¤ÄÄD+RÀ“ÓbQy¤]N«Õ¦S'�SJòh›MÀe‹Ž´):ҦɓÇ®VžuêÿíÉUÎÑÆÀj6›•””¤ŒŒ º@3ý.^¼xÑÛ/¶nÝjLl^Z&{YÕÐ+Ô„„èËðarZ­ 6iê”qš|ÃXZKôˆâÒJ½÷Áaå)Rí…:¥¤¤è–[naò%¸„–T}†ÓbÑÉ‘#Uo T¨Õ¬…·~Çh倞iSæ¼4�¯­Ó{Ö{{ÿOÙÙÙ3fŒ233iYÐçÑ’  ×«7™t*j¸jBCb Ò·ÝH8à3šÂêQÕ^¨SFF†n¾ùfƬè³hIЫ}9l¨Ê‡WS f¥%(#=™¢ð)Áf“2Ò“uËw'hó_>ÔÖ­[uàÀ-\¸Pñññ@ŸÃbz¥†€Ž­SÑÑ92B«~~€Ï‡ÕÌyÓ´ìáYºØP§Õ«WkóæÍ@ŸCK*€^Çi±è‹Ñqêo ÔÂL¥k/¿©ç¿Goþy¿Þ{ï=åççkñâÅŒUÐgÐ’  W©ˆ´«0~Œ"ì6ýì§·Pø­»ï˜¢‡3§«òÌi=óÌ3ÊÉÉ¡(©àOŠbbTf·+%yŒVüd¶¢#iuàß’Æ�ÔËçȬ—_~Y (©àë”?îz�bÓ»Ӕ9/MÁf…Ð+Ø[ôÄò;•’ Ž�WmP�Þ�F÷^½Væ¼4IRvvvãÏ™™!|1 ^dѲûg(>6’¢èõA5zømþË‚*B*øj@]ñ“Û  Ï¸eêx™ƒµñÍÝU„Tð'ÆŒVmP�–Ý?ƒ€  Ï™™I’ôy™óÒt¾¶N7n”ÍfS||b”$ixL\‹ßEŽŒõ@/\ЙòS÷9jªt®ºJŽê*9ÏU·ùœN«Õ£56èüyYNYB+º]½É¤ÒÑŠ1ÌX† ›MZ¾x–û—×µaÃè‰'~IQRÑ15!!ª UMhH‡B©mX¤"cFiÐà!²…Ûe 2kHøð+:†kãÛn}ª»P«3§Œ@{ª¨P•¥ª¯»ÐbÛ ÁÁº¬/Ç¡ÕVY©�šs ¬«ã‚£K�ºVýƒuÿþ�b€— š9/M/oØ¥­[·*##ƒ¢ ¤Â»Z³Y§‡ k7˜Ú†EjHD¤†DDÉn÷ÚBÚLAf㹯�¯IJ—$9jÎêLù©6ƒë…à`� Ö©èÆ®Áa•U ©®¦…WìËðarëá{n–m°…‚€IãGêæÔqÊÊÊRRRÝ~RѤÞdÒ—C‡ª&4Tõ^fÙ•¤@S�®�¯È˜8]?^¦ ³OŸ“5d°¬!ƒ=‚뙊Sú,çUZt\•§K=¶oê£�êj…T×(¬²’:­ÖlÖ©¨(%Ž‹QÒø‘Ú�‘ž¬œ#EÚ°á?´|ù S„Ô¾¬ÊfÓéaCu¡•lÃ"umüx];vüwÛõC‡+5ýû’» ‘D¥E…ú"ÿˆG+kMh¨jBCU5\!Õ5²——Óv*:JA�ý‡ l6)s~šV¿œ¥÷Þ{�n¿©}QC@€Î„Se˜Ík«©eP¨®�¯ÄÓd Ükë` 2klâ$�MœdÖ/òëDÁQc›oú÷×Ù!6�bÓà3•²UVÊâeíVÀÅiiœ\lÖ´›M: >6R£GEè½÷þŸn¹åZSRûZ8­:ÔëXÓø„d]?¡Ý‰Šz{`uÔœÕùGôYÎÿzt v…U‹Ã¡ˆÒ2Â*¼*u­‡+#=™b@'üpþÍzì_^ÓæÍ›•™™IAR{³z“Ie^'B² Uâ�iŠOHöù1¦=Å2X 7LU Suª¨PíÙ©²“Ç�ß;­VÆ[XW¯ˆ²2Æ­ÂPe³©nÀ-¹çfŠ�dlÑÍ©ãô·}ÙÊÈÈ�Íf£(©½MC@€J¢¢tvHË7yË PMš6Cc'Q¨6 �‰Óðûâä¨9«�öìP~ÞA·ð¨“#cTn·kĉ´¬ò÷¦SÑQŠ1Lñ±‘.CFz²²jÆ Z¾|9@HíM¾6Tev{‹–SûˆQ›xá´“¬!ƒuóìùš4m†r?Ü­ü¼ƒÆDKõ¦@Æ�‘ÅáЈ¢“L°ÔGU ±©! @óçL¥p™‚Í&e¤OÔæ¿Pqq1KÒð×P‚Ö9-}vÝX�ŠŽö¨ö£4ûëŽû–P¯0¬¦¦_÷þôq%O�®@SPSí­V}2~œ*"íj X}LeX˜ìCCI÷4¸)É£%I ü-©^¸Æ�6ïÚkª›oŸ¯á1q© ™‚Ìš4-] ßù®>Ú³C‡?Úgü®ÌnWÅСŠ*9ÅxÕ>¢ÖlÖ…à`M�<ŽbÀ 6›”8.FÙÙÙºûî»)Bª?ªˆ´·˜±7Ф„¾«IÓÒ)P7‡ÕÔôïklÒ Ú·óÏÆKßôﯓ#cTe Óðâ™kk)V/Vyir×·ÿ€+3yR¼r�)''GIII!Õ_Ô›L:>êZ]h¶–X|B²&M›Ñ«×8õ5C‡ëŽû–è‹ü#Ú·ó¿å=qRC@€ŠbbôEì(�€:eúºkÑrªp�W½÷§�Ë6,Ò#Ì?NN‹…"õ"•¶0…X‚©ÐÅþaZ¢$);;›b ¤úªZ³YŸ�í19’eP¨æ>°L 7°ì…¯±† Ö]‹–k¤Tã¾oú÷WaüUDÚ)P/ñ•ÕªëÇŽ ÐÅlƒ- lU~~>Åàóúdw_oëžÆ'$kÊô;d 2óªða©éß×µc'hÇæÿ0ÖV-³Ûu6$D£ŽÁºª~¬ÖlVC@€ÆÄFR èñ±vå- ¯¢� IDAT|^ŸkI-ЉñX÷4Ф›2æéæÙó ¨~bxLœîýéã²�eÜw!8XŸ��W­™kè·!õÒ©èá¬� Ý!zøÕÖÖª’%ÝR}CC@€¾ˆåѽ×6,R·ß·Xc'ñJð3¦ ³î¸o‰¦L¿Ã¸ï›þýõù˜Ñª²rüÑùK_0DGrý ;DE†I’Š‹‹)Bª/ÔÏÇŒVMh¨q_|B²îZ´\C‡ó*ðc 7LÕÜ–)ÐdÕÆ5U :þ¦6جѣ"(t“øKÃ)©©Wûƒï¥ ’Ü×?Mž:]7ÏžÏÕï%†„×]‹VxÌþ{rdŒNEEQ?ò•Õj|€t�Ñ£"˜< !Õ×êMó4iZ:W¾—±† Öí÷-öª_†SQL Åñ®¥„¢‡¡Ð�¢#m*))¡©WCMh¨>3Úc‚¤sÈøÓ^ÌdÖí÷-V|B²qßÙ!6}vÝX5P ?`6RèFÁf“jkk)BjO«²ÙôEì(�€zû}‹umüx®xª7ÏžïT/ëó1£ ª>¬vख़}™4 º•-l�$Æ¥ ¤öì‡]³Y%QM“!¹fðe‚¤¾åæÙó=fþuUø¦¿_ÓøB°ÙD1 [CjãðŠóçÏS „Ôž ¨î]| ¨}[ SuSÆ<� ÊU€�zUj )H3ïþ‘LAf®r66q’GP=;ÄFP©Ý«ÞdjPo¿o±¬!ƒ¹ÂÐØÄI-&S"¨ú§Õ©Ð\K}P „ÔîҠ㣮mPéâ wÍ'S:;Ħ*“ô¾¦¿¿T÷uP ¨h/¨JR~ÞAIÒÉ‘�­©a••€nôòË/+''‡BÝhÖ¬YÊÈÈèçâ×-©%QQF@•¤)Óï  ¢Ý j1ÊøùäÈÕš· @w" Ýï½÷Þë5çâ·!õËða:;¤©»æMó46q¯N´kæ]?’mX¤ñ󱱬¡ ¿V[[ÛkÎÅ/»ûÖšÍ:eüŸ�L@E‡™‚Ìšy÷�´yýoT_wAõ¦@�£k�§8t³Õ‹Q  -[¿¾×�“ß…Ô†€}küliŒ5:Ê2X7Ïž¯oý‡$©&4Tev»ìeeݦ¸´R¹G‹(ÐMlƒ-J£`³‰b!µçœ£zS ¤¦µP�Ëqmüx%O�®ƒ{wI’*"í²:²8�]î|m�ž]ý6…ºÙÍ©ãt÷S(ø1¿“Zf·«&4´é¢ÙóY WdÒ´t�‰”ŽÇŽb|*ºEq)³Hü­:ÂoZR�‹*"íÆÏ&¥êÚøñ\A\±™wýHú÷gU_wAßôï¯/bG)®às ƒn¨ÉI¬Ó t•ÊêzeçN€�ÚÃNŽiܶ�¥ÔôïsõÐ%LAf͸ë‡zç?_–$9­V}>LC+NSt [H 2Ò")ÐEòO8©Ð‹øEwß2»Ýsê]ŒCE×§ä©Ó›^stû©-Õ›LÝ|'M›!S�™+‡.7iZº,ƒÇ<Ó¿¿NEGQ€�êédÌã¶}Ä(%Ü0•«†nsóíMËUÙlrZ, ¤º…«Õø95ý®ºÕð˜8Å'$?»�…ЇCjC@€J¢†?O˜”ª!áùbèvS¦ß¡@S�$©Þ¨2»�¢}=¤–Ùíú¦ãäÖA¡š4mW =Âdöx½}9l¨êM& ôÕ�Zo2éLø0ãçÔôï3YzT SeÖ¸DÈ7ýû{Œ�ÐÇBj¹=¸=rÌ8]?ž+…wóíóŒÛN«•I”€¾RëM&UÙlÆÏ“¦¥s•pU î1‰RÕEúZHuoEµ�ÅdI¸ªÜǦVÙlŒMúRH¥¾Æ2Ø£5ÕýK½<¤6oEÇÂUŸ8ɸMk*ÐGB*­¨ðUÃcâd1Êø™ÖT „TZQáËÜ¿4¡5èå!µ! €VTø4ZS;¯&$Äãïèˆþ¾pîK{؆EÒŠ Ÿ4iZºÞùÏ—%IÕ!!AIZ¨¡¡ú"¶1Ô;,ÅQ@·ùã®]:râ…@Ÿ÷àƒvÛcÏš5K=r>Ñ’ZfÜNøÎwyuÁ' �‰“eP¨$é›þýi%lÃy³Ù¸}vˆME11Ðm¨@÷{ï½÷zì¹®zH­5›u!8ØøùÚøñ¼à³Æ&Þ`Ü® ¡ DP€î•¬T/¾¼UYïþÅÐ=¹­¶¶Çžëªw÷­tk�ŠOH–)ÈÌ+¾R“nÐÁ½»$IçBCÕ €† ÓÁ *‰®¿Ð 6ÿù€JʪTp¬Lg*Ï)s^ZŸ­ÅêE‹xA]hÙúõ=þœW=¤ž 5n_?�W|š5d°ì#F©ìäqI�㩇Vœ¦0m0›T[×@PíÅåç•›_C!ÐçTV×·«ª�ÝÖšhlQ⸛}o†÷’²*ãvöÁIêÓA€»ª!µ&4Tõ¦@I’eP(]}áÆ&Þ`„Ôʰ0Bj;n¹q˜*«ë•�[IPíFç/ü]Ï®û”B€ÀZåÔÖ�»íñoN§»ï˜âóu ¨ðgWuLj�Û˜>*ü…ûkõBp0k¦v@æ#•’ØÔµŸ1ª]¯¸¼–"=ñ·VZé7Çš}°@ÞØÍEàw®ZKjóµQoLãjÀ/˜‚ÌŠOHV~^ã7õ§‡UTI …é@P•D‹j Ôä$fŸºŠ{o�’h3Ž—U„ÔNpZ­ÆmÛ°HYCs5à7®�Ÿ`„Ôs¡¡!õŠ‚*º–-$Pi‘è"ù'~R›¿×Tø›«ÖÝ×a±¸}à§«/ü-¤ŽW )H’To ¤Ëo'?<5ïú{�ú@·¾×Òõ€?¹j-©î³úFŽŒåJÀïDÆÄêDÁQI’ÓbQX]Eéć'©é[þ†À@•Ÿ®¦0Ð�ïµ®UðuW¥%µÞd2fõ•¤á1q\ ø�á#G·�V ¹ŒOîßò;œø–ºù½6û`�.^¼HaR›sºuõµ�ÅU€_ŠŒåöš¶R�.úðDP€î}¯íׯŸrrr( BªGHuku56�«¿4$|8ãR ªà—ïµ%%%Ú°a…@H5Bª[«“{kào"cšÆS»¯û ‚*øü{mv6A!Uò�h Ò�ðá\ø-÷q©î3Vƒ  U¸<=>»¯{k“{+”/úñ]3uèÃ}Z±êy-¸‰×m^Yó+­[óœ^}ó¯JN™ê×/†o�°jâ�©úýæí>qnþP[Ïq©© ª²…©Æm–ëÞ¤>ðòº>³¶ôLPõx¯ÍÎn¼?3“âè›!µn@Ó¬¾þÒŠºî·Ïë¦é³Ã+\ãRëë.è›þýUo2)°KÑT ±éTT$¨Ašéñî¾µÁfã¶¿¬�ꨩ֪÷¹ÇCKÓÇ'~ßBÜÝlá‘Æíú:´Ï߯ è;õ í|«*] g‚*]ø¢oIýÚ­ )ÈìEJ›~›vïÚ¦MXÛj·ßÖúpŸçjd¢1×�—5ij{giq‘ÊNkâ�©Ê?š§²SÅsÝxEFÇ(ÿhž$)~\‚J‹‹Tðé‘�ÓÚý͹Ûq®Z‘Q1šxcj»Çî:¶æ�뺿´¤¨ÝÇrŸ$ã¼Zã:Fë �Ÿ¯¦²“Ç%Iµƒeq:;µJ¢M)I¶^ù èˆàËþð$Ñ¢ À·+Óƒ+Ö÷ª êñ^K‹*€¾R]“&IþÓÝ÷é_Ñ­SÆkÝoŸWò�S?®ýesÞß™¥ß<ý¨J‹‹Œû¬!¡ZñËç4{î½Æ}ïlÙ¤ukžÓêW_ײæK’ìQ#ô×Gõë§VJ’ÆŽKÔ¦?¬õxœWߨ¦­[^ó¸?2:F¯¾±Í#æÍÓ²E÷x‡ë1V¯­ÍVR×±¹� ýÍS�z<§ëyW¯Í£.Žšj-[t�fïõØ69eªV¯Í#ôzÛ6~\‚&~Ç?‚ª5$̸íÞ�½£l¡�ŠÉ:«U ¨€ÔÃÝ}Ý'–± ‹ô›"YCBõô‹¯t¸Ûïû;³´ì�ù²Xiõ«¯ëã“­~õuÙ‡GkÕò‡õþάû¬Zñ°îùÑb=¸ôzhé/Œû}¸OyëOZýêëÚ¶ÿˆV¬zÞuÛ¹ÕãþÒâ"#غÂßónÓ¹šj½úæ_õñI‡>8\l8\¬7¶ï—Å:H¯ýñ%¿x}¸w[wïÎŽ®ûðD×_è÷÷Ë+z¯¥ë/€«¨G[Rú7=]`P�_ê¦ôYF·ßWÖüJ-}¬Õm×ýö9IÒšW_7Z5oJŸ¥øë'èî™Sô›§ÕMé³<öIûÞmúÙ“/x}¼§_|ÅØ~ÁýK´é/©´¸È£…ÓuÙ©bc¿üOË>¦Òâ“Úºe“ò�æ)~\‚ò�æÛººRLJ„jÍ«¯ëÖ)ãåK>ò¨Ró?i ¯Ü}k»�騩i£Þ!~óÚ° •ó\cmkÍf™kkywé¦O-ªÐ£ïµ´¨ê�»véȉ¼8Ðç=øàƒÝöسfÍRFFFχԺ@÷�:Ø//Lón¿-‚Õ¥°é8W}Õ�µ´¸HónM•£¦ZoLÕ=?zXñ×'(þú úåò‡:ÝÝWjì²»àG‹õþ®,ÌÞ§÷weé�·þ¤÷weé�¿îóh=~êÅ—[ ë®Ö` ¢m~�f„Ô†€¾³¼ Aª}è~ï½÷ÞÕ ©½…1Ûïšç”6ÝsÂ × ·­u§mjiÑíÇùÊšç䨩ÖS/¾ì1£ð•„^×r9³çÞk<æ+k~¥ukžÓ;[6é¡¥�)þúúpŸ¬ƒB[tIvÔTKÒx†ú–-ªmµ²úšá1qÆ24«µÓËЀ  þTKJJôøã�S]®Ö­'b�†Ô¯¬V�õþʽÛïî]ÛZüÞÕÒz0{¯GHË?š§CîÓ˜ë'´¹^hWqµæ6oÍtÇå„Þ­[6é�íû=–›q�Uu¹)}–^ûãKzí�/µO»jEãìÆ«/M*uÓôYúÍS�¶ØÖQS­­o¿Æ_+ªàïµrΨ_¿~*..Öã�?®gŸ}¶ÏÖdõ¢E¼0€.´l}˵§iI½LîÝ~›{héc:øá>-[t�üh±’S¦*ÿ“<½²¦qÖß§_|¥ÇŽq÷®múÍÓ�êg«gv‡ePˆœçjŒYv;bÁý‹µuË&-[t�Zú EFŨ´¤Èx<×DGÉ)S•1g�¶nÙ¤î¾U î_"ë múÃZ½¿3Kc®Ÿ`ÒÈècœ«k[ǹjmúÃKW4~Ut½âòóÊͯ¡ès*«ë�ÛUçꕵ§¬[žÇ¨ÄøùÖdzÌ;Fï³’ôå—_öù   {R¯€«ÛoóeRâÇ%è÷oþU¿~jeã¸Õ5�÷O¼1U?[õB‡Cᕚ=÷^•–œÔº5ÏéÇwÍ”$Ù£FhÅ/Ãòªå+ÿ“Ž‡Ôøq ZýêëúõS+õËe÷O¼1U«×¿æÑ:üôêW=B›þð’–þxžqÆœúÙªç[„zIÛ¦M¿M¿“ê7k¥‚ ÚÛ�¿ðw=»îS ku½¶î.í¶Ç¿ù†¡º{柯A!õ*ùýæímþÞª½GJZ t®ý›wûmÐZ[sµµçïÌý®Ç/-.’uPˆ±¾©+ĺûø¤£Ýc»)}–Ñ ÚÖy5nImvqvm›4O‘Q#ŒãlmíX€ Ú³ŠË™±ào�  €�Úk´äzJwŒ�íèyuæ¹{ª•¹« ‰hZ+µ6ØÌ‹ž Ú«……jr’�B]¤²ºÞ£;­¯KI´ÇKPà×!Õi±·í#FQyô*�AAÆm– !¨öv¶�@e¤ER ‹äŸpøUHmþ^KPÐÕ®¡z[PMIljåË>X  oì¦0Ð�ïµ®  „ThåÃSb|ˆGPÍ?VJa€  €� =¯¸ü¼ N8�ŸÃB-ŠŽd %TRà*Ô7¨¶®A’d Ôâ¥+Øl¢8ÐCAõ™gž¡0|?¤~ýµqÛYs–Êè‘€º|q­¨ÐÃAµ¤¤D6l 0|<¤ÖÕ·„Tô2•eÆmóyÖ“$ A5;;›  ರN*ÐêjÏ·(úlP•Ü–ËΦ(:�1©~æ•5¿Ò·FXu0{ïe?FþÑ<•QLPÝT›·¨^¼x‘ ¤¢õ€:oæ•–œ¤  z$¨öë×�  À7CêÆ¥^uŽs5TA€ÏêÑ1©�õõúÚÔ¸ ĹêJYCsÚ‘4OŸVdTŒ&Þ˜Úæ¶¥ÅE*;U¬Ò’"EFÅÈ>RÍ_5­%y®š�Úš÷w5vñ½)}V‹ßÍž»Àè²ëòûÍÛ[l—4OŸiüB �É’âÇ%èÍ<îsÔT«àÓ#*;UÌi‡ûxÔ�! ªü%¤ºwƒtï O®Péjõt5Âë>[·lÒ;omRiÉIc\©ePH‡Ÿ³´¸H¯ýñeå’§üOËQSÍ…èèõªnš©: ¡�‚PU~RݺAÖ×]PÝ…Z™‚Ì\…+´ì�ùzg–Æ\?Aiß»M‘Ñ#”|ãT9ÎÕè�»oíP@�wkª5ÕšxcªîùÑÊ¿>Añ×OÐ/—?DwßvCjS¯óyfö% ªü&¤J�Ý!¿²Z%5v“ÇUhÆÕZzðý-fÕ-m65ÿhžÞß™¥´é·iÍïßðøÝ¦?¬íÐómúÃKrÔTkŪç=Ƶ¢cN·/gù™¬=eÊÚSÖ+knÖâyq²…PíÕÒª*-ÿǤ8@wMO?¡{wH÷n’hrÓôY² ÑÖ-¯yt»uÔTkë–×<¶5º�kÙ5¸ù¶ža·¨åc4ë^œ4�VÔ¨¿Ð4iËÏx*©¨Õ�œ3T@«A5%Ѧ‹/J’N�9£yã R{–{wÈ3å%\/¬!¡úÙªçUZ\¤æÝ¦Ý»¶i÷®mÆ21î"£FÈ2(D¯ýñemÝÒ8ïî]Û4oæ�j¶æ©»×þø²Ö­yN¥ÅEJNI•$ýæéGµ{×6úpŸ^ûãKz`ÞmƸVok°¢Qåé¦ñÕ§³Cû„VWk³Pʼc¤úõë×ôïê¹sU �ëñî¾îÝ!K‹ŽsZ1{î½’¤WÖ<§¥?ž'Išxcªž~ñ-{`~SH�ŽÑÓ/¾¢_?µR¿\ö�qÆœzõ�mºuÊxíÞµM-}LRãÒ4iÓƒoþÑK7¥Ï’£¦Z¥%'=ºþî=Ò²µºùØUIzhéczhécÆÌÀ‘Ñ1-3Zúâ³ÃÆm–Ÿ¹2TA€tºû6ÿ0_ZtŒ«ÐŽÈè# ¶ÅêuljgŸË= ¢mìê * u)‰¶A!µÛYMæ¿È?ÌU€_ª»Pë9•–T*àŠ¹&S"¨„Ôe¥%½€ûkw Ãá1s5¨‚*? ©î3 :jÎÊQÃR4ð?§N|ÞôšvÐÕ÷Jê5×ô# ª®NH•¤An(�:QÈ•€ßñ�JWß+ ¨ý.^TTd@PpõBªû$3¥E„TøGÍÙËZ-j@Cƒ‚Ο—)pÅt(¨þfË R»žå\SËÓùG¸ð+î¯Y÷^è|@�Ë/Ð5ß|Cqª¥UUz}÷n R»–¹¶Vêê$IõuôYîG\ ø�Ïrþ׸Z]CA®  škk) ÓAõ£‚‚*@HízþüÒ¸ÍR4ðg*N]}BK*@PÐ;Bjˆ[ Ô‰‚£Ìò ¿àÞŠR]ÍÒ3í(©¨% º5¨^¼x‘„ԮXWç1ž�±©ðùyÝB*]}Û“óY5ЭAµ_¿~U€�ÚuÜÇóåýÏ®|ÚùGT_wA’4 ®Ž®¾�@@TtDÿ«}a••:¥†€9jÎêLÅ) Ε�Oú,×½«/­¨Tè[w—ê½O=SÐT%);·Òª¯ïÞ­ùiiðc×øÂA¸·FåýÏ\ø¤º µ:QpÔøÙ}â/xêÿMºµí ÊdJ!µË ­8mÜþ"ÿˆê.ða¾Çý óùZ^ZB -…�©Ô@‡C*t�¾PÝÃ&Aè»úûÂA¸ÖLýÚdR}ÝåýÏš4-�«ŸQw¡VyÿÛR‡ž>MQÚÐРÑŸSèëVM¤­U©©ëïG’D×_À]ã+b/+7nçýﴦ§äýÏ&…UVR|0¨Ò¢ R»LXe¥\ê>éjM|&¤ºµ¢º¡ªziHmþáŸÖTøŠÏr?¢‚*€¾RiM…/:øÁNã6­¨Tt¯þ¾v@ö²r�#©±55á;ß•)ÈÌ•ÂUñYîGrÔœ•D+*þT%&SjÏÞC‡´÷СoŸ¯ £G+&2²WÖà±E‹xQR=…UVªÌÁL¿ð ´¢zÒùº¿« ÈA!ª=Ð~µ~}§÷[rÏ=zaÙ²^WB*!Õ+ZSá >Ú³ÓhE hhPHu5Et«’òZ½¸¡€BT¯š cÆ(Ôjmõ÷E¥¥:YV&IZûÚkªq8ôʪU½?¤6oMÝ¿ëϺyö|®zŒ£æ¬ÇŒ¾ee hh 0T{µ]¾\S'¶½ïÞC‡4oùrÕ8�úÓÖ­úÅø}×ß³fµ{Þè9×øê�Åœ(2nççÔ©¢B®zÌßÞyݘÑ×|¾VC+NS@·3jô¨ˆÿ¡ë‚*“)]¹©'zt‰Ý”•å÷ç©©'T}D_=0‹Ó©AÕÕ:*Izÿ�7tïO犡Û}‘D¥EÇŒŸGœ8AQ€^ªßð;´]Úä1eÓíéIºcFR·Ǫe³ôäòŒNíûä‹[õÔêÆˆO­3îß} _7Í]Ýøoè[Ë”69þª':&z¸M+Ïnqÿƒ+ÖSœ. ª¶œ¡ñ IDAT-ªWjVZšV®n|�ioÒ%÷.®�Ûî�?Ânïp«m^~¾jœNIRˆÅ¢„øø«Z³Ë=�æ5ì Aº¿/\̉"}2Áª†€9jÎê£=;™D ݪîB­öïú³ñó�ŠÓ2ײ^/Ð×í>Ðø!vÃæl¥M£ÿþýà ¦0AµÏ icܪ{(ûÕúõ^Cì½z~Ù²6Ç¿þjýzmÊÊRQi©Çý®–ÜÖÂÚ¯Ö¯×K¯¿®j‡ç$d1‘‘z~Ù2ex¹Î¿Z¿Þ˜8Éyð $iÞòåÊÚ³G¡V«JÞ¿Í9nvã—K/,[¦%÷Ücü®ÚáУ«WëO[·¶Ø¯­ó˜ùàƒÚ{è�[´H#ìv=ôÔSç±Ó¦6kGHíF Š(+Ó©¨(IÒÁ½»46éYCó΀nñÑž“%ÙݾõÐ{ÅDÙ”yWJ«¿?Q\©?ïÌQ͹Zí>P GVmÖ†ßfR8€ Úg½ôúëa«¹?mÝê¬\-‡®Á?mݪ¼‚½þë_·hQ¬v8tëC)/?¿1_j­v8t¸ @{ÒÌÔë¿ù�Gàl¾Ÿû±í=tHE¥¥š¿b…îÍÈèÐdO 22”µg�ªmݽÛk¸•¤,·.ã³Ü¶ÉËÏ×üŸýÌ#dO�8±Åy¼²j•îÍÈh5»×Z’Y,½: ú|H•¤¡§UfSmpãì¾{çuÝþƒÅ¼3 Ë�©8¥Ãíkz3=QÄdI@12ÚÖn÷Õœ£ÅJ›ó¢jÎÕjã[Ùzä�[”4.ÚgÏ)mr¼G÷_Õ® ¨îKÖ,˜5Ëã÷yùùF@a·kÝ“OzÙ­»wë¡'Ÿ4BÜ�M›<ö_ùâ‹FÐ|lÑ"�ñ¯E¥¥š·b…èá§žÒÔ‰�ÀöèêÕÆ~‹çÏ×c‹¿«v8´òŵ)+KÚºU©ßþv«ÁÐ%#-M!‹jœNeµR7mÛÖP§M3wµÃ¡‡ž~ZE¥¥ ±XôØ¢E-¬îçñèêÕJ3Ækwd×x_W‹k^~~‡Z±ýÝ5þp�îcK‹Žé³Ü�xw@—ª»P«¿ýå ãç�KΠÓÎ×ÖQ„^,i\´~ûÔ]ÆÏÞ‘CQ€^T›O¦´ãRwϾhSV–ÑõÕý¿ù+Vhæƒ*ꦛôó_4¶eÕª-¡®±ª!‹¶¯[×¢¥5#-M¯<ù¤hÝ»ÂæåçÁÌ4ÝÅDFjÝ¥VÐj‡C›.íëþ8 fÍÒ¿._îÑÚjµz„åç^}µCõXp)ÈnÊÊjÑ}Øõ¼®`ìÞŠšµ{·GÐv¨®óؾn�B,U;ZÛ¬µÔÝ Ë–!uÉ=÷´®{ƒþþp�æÚZ ©8­3áÃ$Iïo}CC""5$|8ï¬èûwýY•§»b44(¦è$EA§Õ^øZÁf…èÅFF5}�=Q\évûŒŠJª$IÓRÆxÝ·ºæ¼r?)iüp¦‘ÑCZ}žÝòõ—]¹Ê9Z¬¤qÑJ­Û§'vj¬ûó%^åuß¿ìÌÑîìå-Vè àKÏ¥ÛÓ;69Ô‰â3ÚøÖ‡Ê9Zlù…solóÜ\Çæ:¿œ£Åek<ÇôÄV÷Ý“]àq.ÿöû÷ôç�9JK‰×íé‰>ݪ ßwË�ÃŒÖTI*l6²/ñ6vÒ›v»^X¾¼EëbQi©1uAFF«“e¤¥i˜1ÿ¿½ûŠú¾÷=þJHÑ¥ ÈÚ$à"$êÒ«6l¯Mç@nª “œË&æÌ ÖØ¹ÁÓ6DóG£ä̹Ó;g¢s{r§µö�c*iìiƒ™)vÉTíÅÀñ ½QOÁ'ÙMˆÐÅE=,,.aYî0Þ?`¿îÂ."°Øçc†™ðãûÝï÷½ë†ŸÏçýÑ9—Ko×ÖÁ«¶¡Áø™Ñ5ä�ü|m*)Qnv¶–Œœÿ·a†VQóº·nÜhLý=qæÌM›}¿¤Ä˜n[[_?& †7=55â{û~|ÃjPÃó&»]ûÞ}Wo×Öêg£‚uH¬ã ©3@Vg§¾HK3¦ý©~KÏü¨Bóæ›xgÅ”|ÒrJγ7þbºØÝ¡ä #b¸uæ…©!AUU7Eí°®ù‚Ûè¶;^wÜÍÛªTUÝX‡ÃY^ŽY¿{cË„ÃXøã�îîÛ|Á­§ðZDЖnŒOä±¶í¨ÖÞ×ëÆ¿c·C;*ìze{IŒZ5jÛŽCòõö�úN“^z¥Z/ý¨X¿ØQ:渵»Û¸—‡šŒÕ7ºô‹×ÿ·Úþô*ͬ0)îËýÚ]å2>ŸŸœ¬§z(aëñ «uLP o|´©¤D/nܳSnøÏÚo2m:7+Kç\.�s¹Æí:Âí‰ �ãfÇ…‡Ò‰„Ôòó�0-¤~0ªKFÝkhõ«uÜó?¼zµ‚Ϲ\c®'Q·Ä™5!5ihH÷}þ¹œÿéëF·ßõø3›ywŤu]¹ÑÍ7ÓëU¦×KaDÕÖqãý!#w¿|ã¸|½ýʵ˜µþñåY©ù‚[5©ÍíÕwŸÙ£mŸÒcûzûõÝgöÈ×Û¯ô&••*cAŠ2¤Œy¬‹Mÿ5ø…®3üø6·W Ç»*Xa³]OUu£6o; IƱy–Eò]ëWÍ‘fµü{‡ö¾^'_o¿ÞúEYÌ�žj½þ1S ¨�à�P_´ÛµØlNØšü¼¢bL0j÷xôÂÎ�:qæŒÞ®­UÆ‚1CjøV3O”Ol›¯hÓhoµ1P(Üžs¹”ú­oMkM^ܸQ/ìܩچµ{<Æè°£¾ÞhŠôâÆ�cª4_ÄúÔ“Çjd¾'K‹s—ñlâ–|è8±ÝÌ’¶6ºù ª÷ZÀXó8Z[‡Wõ�NÕm1ÂÝ�øÈM×]Nêÿq Lc›4Ü©wÇv»¶í¨Vó·ê�ÓwoEøßh#²ë·é¹g†·ãÉH7żÎÑ5t¾§+Ðá£-:|´%â{;v×Þ8öÍèûÌVí-S}“Kí^ýò�º¨!U’P%Ååyce¤¥éà®]*zöYõöõé·‡þëš5ㆪŽ?¼åÑÜììˆÑØ[U²f�†5vš®{ßTR2<*úÁzñÙgåóû�©¾á£¨ÒðúÔ�Ñ݉11wÎÆ‹¶ttÈÔ0>?Rý–‚žMLØÙ�Nè¢ó¼ñù’¶v™¼†€DÚ^&ÚGÙKÃkDCõ©Ç ´c{|:+Ž7m5|×ú�zB¿l-¸qþÍÛª¢¬ ‹U{ËTVZõk ­1¯3ÖTäÃÇZŒcÇ –¡û¬otE½¶\‹™` ê—$7;ÛèÊ+I[vî3òù�°5˜©ŒæfAÕQ_¯W++�©´¡)ʹ|9.÷ZszÖéT»Ç£Úúzczî¦QëTÃGUÛ¸ WÂ…TIZær£^ƒÁþÍ>‚*&ä“–SëP]¹Êv3Æ•¾À¤§+ÐïÞØ¢š7·ÆmýãxkM3ÒS”;Ò]x*!µ¬´Pé †GH«ª›´pÅ6}sÝOµsO­Ñ¥w*×ë�¡ÀÙëhçžÚ˜¡ŽÄ¡ãFËË!H€€úe²¯]«’5k$ ¯¡üï#ÛÍ-,¤†¦ÃƲòÉ'U´i“ÊÂoèÜíϸï½þº^­¬4º‡Âq(DÆrâÌY¾û]=Q^.ÇM®oô}/ÉÊ2î+to%kÖD-…æÆ]kúje¥V>ù¤ž(/'ÐÎ…�š44¤û>k5>÷^õèðoöñŒb\]W.éCÇ�ýPMýY:X‡ $º5…V]¿´?æ‡ï㽪yskÌé§ÓR-ã~:ZFzŠê߯0o( îØíÐ7×ýT WlÓæmUÓºÖ3|D´¾Ñ5Ü8ÆÇÍöŸ ß  ~9~VQaLiuÔ×G„½Üìl#h¾][s4õŸßyGí�Î:�ÛÔ„wÉ€Ca{�†+|ÊíÆ¿ÿû¨ÁÐç÷ëöì‘ÏïŸPgßÑB�}ßþàc«œM1ö,ÝTR3ȇœu:µïÝwÕîñÈç÷ÇÜ®‡�:ˤöõiI[{DP=þûwyV3 †ÿ!ÃÔÐ2—‹ÂH8¶•9jû¿¯êwolÑsÏÜY ʪê&Ý÷WÿCUÕ�ÓþØ+,ZSh�ÐG´kFRA@ýòåfgG¬³Ü²sgD(üÉóÏ!ö‰òr½ZYi„JŸß¯W++õ#ÁmIV–¶†Ìðs;êëµñå—#Ž}»¶V[Fš-ÉÊ2Bbnv¶qž³N§Ú´Iµa£˜'ΜÑ_¿ð‚q®­7ÞòzÙPð �#=55æšÜïÛíFþ­Ã¡'ÊË#ûÛµµúë^0®�u«‘îší7Ú.ä/y¹’dìwùÈ“yv1& †:ù& EL€™ ùBǸ ‘ÚG¶ÀÉX0={„¯üFÝá†L.UU7Sn·í8s]êT3Ö±uöxñÙgUÛРgÎÁóç’†×dþaÿ~=Q^®Þ¾>½ZY©W++Çœ#=5Uwíòüój÷xôvm협ÚñŽýyE…zG‚l»Ç£ï�\O´°ùóß»Y8xõj#lÆE ïˆ|âÌ™˜[òüê•Wèê;Ê�sá&Fïmé<{šUü½=cª“€ àöò]»yß„¶ŽØSl}½ýFgÞ©î“ÚÐä3�×¶2G/ý¨XÍüG=õX�ñ³õ�Î)ß{xðï.µ×PãgIV–^½Z¯^Ñ™öf~¶}»qÜ9—+bMåùùºàpè'Ï?o¬å ¼­7ê‚Ãs¿Õý;vèû÷�™’»$+K?yþù˜Ç†Ž+Y³f̽”¬Y£wwíÒþ°5°Ñj0ž­7?÷ý‘‘ÕX2ÒÒÔôÎ;úÕ+¯Œ9ozjª6•”èû÷ÓˆÃ}ÃjÕëWG4¢J$wÍ™\#Ó~»GÞtœgOkѽöPMpÁ�€þðÞ›c*�|Ä-ŒööG�¦z³µ–’tøh‹öîÜõ{{]gü÷TÖÆÚÖýTí^­)´ªþýè# e¥Ec¶�™ªÐÖ45iGEI̽›·P}ãðRŒžÿEÜšT�€Š¾o·G J71/¤Me –‡W¯÷üã7™õ¦©�}íÚ[õœL}'3Ò;—Ü9—nfI[{ĈêÉc5:ûÑ Þy8 þÍ>y¯z¨â*|�ä¶Õc¾_Uݨ‡šnzž6·W›·UE ¸¿|㸤á&OSI]ÿøð(iC“+fpþåÃ�8}�iJ�î¥ÿýô~u¤´ªºÑ¨Ï=SH@HPw͵ZÒÖ®`r²¾™Ÿ~òX�º.w°F5Át]¹¤#ÕoÉßÛc|m±»ƒ€ .Ö?fÓK ªÕ{- ªê&µuxµ¶pxZ}“Sõ�.¬îܾÅÊh¹³q|hÏÔúF§ªª›ŒÐXõ‹ç¦t­;¶ÛUUݤÞk=ýƒ×TVZ¨¼œE²­´¨ùB‡ªª�)¹/ý°xÚ‚âÚ¢|ýø‡�è—¿>®æ n}ó±RYi¡l+säëíWÍÑf# ¬°hïÎR^X  „Ô¹ãþÖÏõ™Õª@Êpc‰P3¥‡Ö­×¼ù&žõ¨ákPC¼e€éÚÖeíßîVïµ€ê]Fà …®š7·¨lÛ�qϳwg©öþºnÌñ¡[óæ–˜ÓdoõZ×ÿÝkjïðx´W¶—L{ƒ£½;7(ϲH;ö8äëí×Þ×ëÆüLÁ ‹ê߯`T€�:·$ )ÿã�õ—¼Üˆ5ª]—=zê¿m%¨ÎaŸ´œÒÉc5kPïû¬U©}}3F÷µAÕ6tNë9»|A ; ¯lnz1ÛšØVæ¨íO¯ªæh³jŽ4Ëw- <‹Yk‹¬F‡Ü²ÒB­-´jm¡5êuØV´ªêƘçmm¡UÚ>¶yG^Ž9æý…¶ ©ªnTó·š/ �îf,0ima¾Ö?^5 ‡Î7úú'r=!/ý¨Xe¥…ªªnR}“Óh(e[iylÛ¸ÏÕx��(\íþ9{o–{LJ™?¹_O ¨ÀÜqÇõëׯGû†ÃáPmm­$éO§²:;gå †UI2ß�­'6ü�ÒÒòìÏÁ€ú¡ã`Ä+Xƒ:;|j]®ì‚åzyë“sö�­íy­ö¶<Öò%©zysþô^›_{¸âv~ ‘EüûºÿÞ¨ï…å/W&D-ÌÉzõÇߘqu{Øö){ØÏ˜Váÿ¾ö�4ʺs®ßô’¶v£ó¯$y¯zT]¹K]W.ñŠ˜CN«‰¨¦þ€ò?þ„€ŠcQæŠ`Ò23Râ>½¾ÁPÜ~w%ÂM†Ö"þ%/W’4ÐáßìÓCëÖëëò*˜Å‚}è8¨‹Îóu™‹}P1³˜¦jKÙ:¹/ÅgïGo·_Mg>¥ÐÀµa}‘O9Õ˜úÔ~Wk§Ìf³Ì3(Ä}úéäÞ¿FTIT€�:»‚ª©¿_Ÿå[5””¤Áà€>t”§ý3*ÍR—Ú?Ó‡¿?ÑÁ7ÓëÕbw3’mUžl«òârng«‡� ðþ1!å/Wª¨¨Hv»}ÆÜ_yyù´TIT€�:»˜-sºô—¼¼ˆÎ¿]—=zä©ïiÑ=‹yȨOÓ©†£_[tåª,€9.V@07Ü™h7l O ߎÄ{Õ£C¯ïÑÙ�NðŠ˜áü½=:ü/û"jÒÐ�îkýœ€ @T“‰Ùp!uH2*…O =y¬FG½¥àÍvf¢‹Îóª®Ü%O{«ñµ¯úýZqî¼Ò}> @ÔŠŠ Ì1w%òÍgz½JíëÓÅûï7¦ÿ^tž×¥¶Ÿê¿<ö´òø¯� 8ÐÉc5rž=ñõÙ¼5˜ž€š““CqBêÜ’ *ÿã�Õa±¨ëž»% wÿ=þûwõIËGzhÝS¬Uý�ýè„N5Ñ`pÀøÚW‚AÝßú9ÛË@@¥8!uî²tt(ÃçÓÅeK5””$Iò´·êÐë{ôÀ·Ö·¾ó€o£KíŸéäÑÃò^õD|}�ϧÜQÓ´!uNJíëÓŠsçuõî»u%;ËøúÙ�Nè“–SL¾ ü½=:y¬&bßSixô4·­]©}} *Å©‰#ihHY��2ww«=w‰¾HK“9ø[ßY§Å¹Ë(Ö4 tîÔ µü©!bjoÒÐ�]¹ÊÚS¨T€�šØ’ƒA-w}ªÞŒ uXëÿÍ›'ix ðïÿå5eç.ÕßþŽîË_E±âN¥áÆV‹ÝL퀀J@©I÷ù”ê÷ëêÝw«ëž»#Ö«zÚ[•–¾P®yœiÀ·ÈßÛ£S GÆtì•$S@‹Ýn¦ö@@% „TD>ørÖ½ê6›#ÂÖñß¿«S GôàšÇ•g]Iƒ¥I†Ó¯ƒÊ꼬L¯—B� ¨©· 9Ô’¶v-vwŒY …Õäyóõõ‚•_ð [׌Ôæº OZ>’§½uÌ÷¿ê÷ëî«ÿ¡tŸ�b�਩“Y½ûêÕ1au08 ³�ÐÙ�NÈ|w¶¾nû¶îË_¥´ô… W§‹Îóºè<§‹ÎócÖ›†Âi–§“i½  ¤bŠa5«³SÝf³:³î5,I’÷ªG'�Õèä±eç.Õ× ¾=ç§w]¹$gË)}Òr*j0•†"}íÊU™^D€€ €�™^¯2½^õfd¨7#]½Æèªt£Ñ’$Ý—¿Jٹ˔�{ÿ¬ŸŒÜÛgºè¥û|JêPoF†|麖‘ñ3ÃÓ`ÏK’’çÍ×â¼e³&´†‡ÒKm­ò^õÄüÙ¯ƒJ÷õÊìõ2j bÚ°aƒrrrd³Ù¦P·WVRP€�Šh’††ŒÑÕ¡¤$u/2«;Ó¬@JäTßÁà@ÔкèÞÅÊ^²TÉóçiÁ58�÷ŠGþÞu]î¸i( Ý÷pP菱³Ù,»ÝN!Rog`ýÚ•«úÚ•«œ7O½éò§¦ê‹´´ˆ)ÁÑBkHZúB¥edjÑ=ÙšgJQö’¥Ã_ÏÈœRS¦Ðdo�ü½Ýêº|É-�(S@_õû•Ö×G0·]II‰êêê`æ@HÅ­K�À*I“I} Òb†Ö�áÙ3¡ð˜Šqäíî‹xßB*Da±X$I]Ý×(Ä5¤¿ÏæääP „T©f¼üü|I’ë3Å€8r¶z´|ùr €� 7c±,–Ûã¥GŸ~~™©¾f¼»(€™ 'g‰šÿ|†B@œ„þš½‚ÉÙ^YI€8c$ÀŒ`µZd4âÄÕÚ)‰¦If¦ÌÌLB*€™%ôK“û!âÁ}©K&“If³™bÜ¢’’™L& Ä1 Úívãs¦û˜1!Õd2ÉÕêQуV ÓÌÕÚ)«•÷×ɰÛí¿@ˆ/B*€Ãb±Èíé¦3\÷µAÕ6tR`štõ Äý1¼=}òöøUøS}R`ÂŠŠŠtàÀ¹=^åd3m¦òúå¨g» `6i>ßf¼ÏÀLÇšT3†Íf“ÉdRÝ¿ž£3̢̘ÅÿÖêþõœ–/_ÎzT³#©fŒ””Ùl65ÿùŒúA¥˜æQ”¼0U[ÊÖÉ}©‹bq{œ/Ûª¼i?¯³Õ#o�_%Oþ E@H€[UXX¨¦¦&5Ÿo§�Ò c[•—_ ÄWÓ)—L&S}ÌL÷0£äçç+33“)¿0 úA5�vÉf³Q „T˜¬G}T�^¹=ì™ SÑtúSIRqq1Å@H€É ýÅŸÑT˜šP䜶ž@H€I3›Í*))QÓi£©0Ù€zâ¼¼=~ÙívŠ€� SU\\¬ÌÌLUn¢p‹úA9ŽžQAA�òóó)B*LUJJŠìv»\­5žrQ¸Õ‡›jÆ !¦KQQ‘–/_¦êÃ�ê)L€³Õ£¦Ó.•””Èl6S„T˜Nvû“ Êqô4Å€ ¨®i’Éd¢£/B*ÄC~~¾yäÿ?älõP‡ãèiutzUVV¦”” €� ñ`·Ûe±,Ö�ƒ Lû€Ü¯jÿøo*,,4¶òB*ÄAJJŠÊÊ6ËÛã×�÷(ŒÒêµ·Ž)33S¥¥¥!â-''G%%%j>ßF·_åÀ{ òöø™æ `N¸‹˜-ìv»¼^¯¼W/I*zÐJQ$¼ªƒõj>ߦçž{Ž=QÌ Œ¤˜UJKKe±,VõáF¹=^  ¡5žr©é´K�<òˆŠŠŠ(B*Ün)))ª¨xYæE_Óî}‚*€„¨Þ«Waa¡6lØ@ARà˪ºãNíÞç�·§�¢HÈ€º|ù2•••Q„T˜IAõî~ŸU P-–ÅÚºõE €� 3ENNNĈ*A@"ÔŠŠ—éä `Nºãúõë×)€Ù¬¿¿_»wï’·ë?Tö½µ²­Ê£(æœêÃ�ª;q^………Lñ@H€ÙT;:.©ô©"?¼Š¢˜ïo� ¼× æómT„T˜mAµªªJ---²­ÊÓsÖ(Å4�˜µÜ¯^{똼=~•––ª¸¸˜¢ ¤ÀlSWW§sILšIDATêêj™¦iËæuÊÉ6S³Nã)—ª7IwÜ©ŠŠ åääP„T˜­Ün·öíÛ§îîn?¼J%ëV3ª `Vðöô©ê`½\­-_¾\[·n¥AB*Ìýýýr8:~ü¸Ì™ Túä_ÑT ÀŒVûÇ“ãèi™L&Ùív¦÷ ¤À\ät:U]]­ŽŽY—f«dÝVþÒl `Æh:í’ãèy{ü*((PYY£§©0×ÕÕÕÉáp(V̸pš™™©²²2åççS„TÊ Qô÷÷«®®N���êîî–uéb=¸\+sY³ à¶ðöô©ù|›êNœ—·ûš,‹Š‹‹UTTDq€� ‘566Êáp¨»»[¦ùód[•+Ûª<Ö­˜vý� Z.´«ù|»šÏ_”$-_¾\v»�‘S ¤@$§Ó©¦¦&577+È4žò—eɺ4[Ö¥Yla`R\­�r}Þ)çg¹Z=’¤ÌÌLÙl6=úè£2›yoB*ÜDss³š››åt:ÕÝÝ-I2ÍŸ§œÅ™ÊÉ6+%e¾¬÷gÉdJ&¼0Âh ¨ŽÎnuy¯ÉÛÓg„RI²X,²Z­***b¯S ¤Àäy½^9�N¹\.uuu©££C�@ âg¬K³(� úû‡ƒèx233e6›•““£üü|Y­Vºô!âËét*ÈívËétR A¤¤¤D QFI€� ˜cî¤B*„T!B*`¶¸‹˜fwLöÀ]»v)ðE�þqûßÌ­Š¤­¦‹:À1’ `Æ(**R‡§Kn�—b$¨ÿ^ã+'ªNIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2503567 ceilometer-27.0.0.0rc1/doc/source/contributor/5-multi-publish.png0000664000175000017500000012337715250267452023430 0ustar00zuulzuul‰PNG  IHDRH – \Û‰zTXtRaw profile type exifxÚUŽë Ã0„ÿ3EFÀæ1N%R7èøÁq*·Ÿ,8�ÌŸ÷IÛ 1H»‡¥ššx•žs·Ñ«Nž.­–M‚),ÃY×G}ü/],ìtu·n»í¨t‘ª5C#•Çùwd­ý÷q¿]B,!õajê iTXtXML:com.adobe.xmp :ÈyÿsBIT|dˆ IDATxÚìÝX”÷�ÿû8÷ ƒ`Ô­@M¢Žf7¶F”´Ù¯~µ'öÈw›ž5=iµéµMÓä=Íi“~¿i´Ùƽšö¼ökÎÆk·5—¸î¶!!1«1»1m6r”Æøc€(̨ œ?ƹ™�`aîç㺸2?îûžûþ çÅûóþ¤ôôôôÀÂR`u$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –gcâÃëõÊëõö{¼¡¡¡ßcEEEýs:�²Ûí $7É477Ëï÷«¡¡A>ŸOÍÍÍQƒ‘á %ÅÅÅÊËËS^^á –ÒÓÓÓÃ0ôçóùÔÐР††Õ×׫¥¥å†ž�a*..VQQ‘ŠŠŠär¹x“ˆ’ëFˆ8®v*ïjG¿Ç]WÛeï¹öX½­ ßv�§éõLˆK$>ŸO|ð�<�<Ï ÛC�`èQÔuVöž.¹®¶Å휼2u>%S-i¹:Ÿ’©æ 9jIsÈŸ’6à~†a¨´´T%%%„% ‘%’` rèС·sv·©¨ë¬Š»Ïª¨ëŒì=]7ô¼ëmjIËU½­@ iS£†&yyyfX’——Çw9ƒ°L@âõzµwï^y<ùýþˆÛ8»Ûä¾Òb†"c] ()�g¢S-¶ÜÈ×ätê®»îÒ¢E‹hô @I�477«¶¶6jµˆ³»M¥WNÈ}¥EyW;ÇíuúRÒÔ�6Už‰NJŸÓïyÃ0t×]wiåÊ•%ô‘´I}}½jjjÔÐÐÐï9ÇÕN¹¯´¨ôò‰¸ö+|)i:”1W¯¥«uBfØs†aÈív«¢¢‚é7\—tÉ@ÁHá•3ªðÿa\LŸ‰ÛxØ t(cnĪ’’’‚”D‰×ëUuuuÄ`¤äò Uø��ë)4#åKISmF±j�/ôkîJP°ºq�ø|>½þúëÚ»wo¿çF"ŒW” $Ø£dõêÕ ÀrÆu@R__¯�;wÊëõ†=N02¸hAI^^ž*++U\\Ì ,c\$^¯W»wï–Çã {¼ðÊ­÷½Ÿ”�WÅ—’¦½Æ½n„"n·[ëÖ­cÚ ÀÆ]@rðàAíÞ½[~¿ß|ÌèéR…ÿ˜Êýõ¼£ÃTo+ÐîÌ%j±åöŽ«a¨¢¢Bååå  ©�«€d÷îݪ­­ {¬äò ­ë|Oöž.ÞÍ8ØkÜÚoÚ�ÛíVee¥ìv;HJã" ñz½zá…ÔÒÒb>æìnÓºÎ#–Z²wÔÆ{B¦^²/UãÄ©æcyyyÚ¸q£\.H:c> ñx<ª®®›R³èJ‹ª:Þ¡j$Áj�bí¶/ {lݺuL¹$�1�DšR³Îw„^#£¨yB®^È^¦Ö ™æcL¹$›1�ø|>íØ±C æcŽ«�zèâV¨¹ïGJš^ÈZÆ”@Òs‰ÏçÓ³Ï>Öo„)5cÃ^ãVÕØ˜÷ ÃÐ#�ÖŒµ²ã•^>Á»4Æ4OÈÕ³“ËÖ®¬¬Tii)ƒ—ÆD@B82þ4OÈUuÖRµØr{ß3BÀ8uÃ’¾áˆÑÓ¥G>¯¥ë8àKIÓ³“ÊÃB’ŠŠ ­^½šÁH2�'l*<Œu%%%ÊËË‹yû�ŽŒ‘B*I’‹×ëÕc�=Æ@WœN§žx≘·O½Q'J8’ì=]zäB­]éýkÂÎ�;uðàA Iœ?žA0î µêÍv#NÒçóiÇŽ„#IÂÞÓ¥ªŽwÂ*IvîÜ©¼¼ßÉ@³†�Œê›`8Zæ²ñÂ[„#I"’8®~aúý~=ûì³jnnfpcÚ¨$»wï G*;ÞQq÷YÞ…$bïéÒCÈèé’Iª««åóùÀ˜5jÉÞ½{uèÐ!ó~eÇ;*½|‚w ¹®¶é‘ÏkÍ�¤¥¥E;vì``cÖ¨$õõõª©©1ï¯ô׎$9×Õ6m¼ð–y¿¡¡A{÷îe`cRÂ’àŠ5A…WÎh½ï#oÅÝgµ.ä½®©©¡ `LJx@Òw9߇:0êRî¯×¢+-aßô#Œ5 HöîÝ«††óþÆ oÉ~½/¬£ªãse¯×«�;w2(€1Å–¨÷í;²ÚwŒk,*¸²ÍS9wK’<�jkkU^^Îà )477›•râÏétÊn·3 ¡�Dê;Ráÿ£ma®«mZç;¢Ýö%’K>»Ýnååå18×jkkµ{÷nH°çž{Ž�$TB¦ØìÞ½›¾#è§o?’êêjãÞûï¿Ï £€&ß Ñâ^AR__¯C‡™÷é;‚PUïè±Ü{åOISCCƒ<�Ün7ƒ¤�’“/ÙÒ Nz>?'u]f À¨ˆ{@Zj¾èJ }GÆÞÓ¥ ÿ±°©6EEE”M#9þ‡ê^©Ô‚™ 'W^ÿÕs¾…�£"®SljkkÕÒø‡ŒÑÓ¥õ¾#Œ0ú)÷׫ðÊI�UmjkkÀ ·€ÄçóiïÞ½!‚ÿ¨¼ëK»}­÷õöm¨©©‘×ëeP7LÜ’ÐÆ¬Ž«�¬Zƒ¹®¶i¥¿>ìû€%.I߯¬Uï0²T…ÿ˜Œë |=�êëëÀ —€$´‡�Y+{O—Öu¾gÞ¯©©aP7Ĉ¯×«>øÀ¼OcV Eéår\ïUÓÐÐ@/À 1â€$´1ë¢+-4fÅ�Uø�Eü~`´Œ( ñz½a½GÊýô�ÀÐ…V‘:tˆ*À¨Q@ú×þÂ+gè=‚a+½|<â÷£a؉Ïç «aY_ŒDù¥zsE›C‡Éçó1(€Q3ì€$tåªG0Röž.•ûÿñû €Dv@Z=Rzå#‰+¿Tñû €DV@ÒÜÜl6Ò4zºTz™€#gïéÒ¢+-’ €ëëiú à HB§?¸¯ â!4l£Š0Z†�x<óvÉ¥ãŒ"âÆ}¥ÅlÖú}@" 9 ñx<òûý’$ÇÕNš³"î‚UI~¿Ÿ�0*†�ôý ÄSè÷ `4Œ( ¡9+Á}¥EŽ«�’}H|>ƒH¨!$õõõaÓk\WÛA$U$€Ñ4¤€¤¡¡!âX ÞB{Û„~ß�C® ‰ôˆ·¢®3æm@¢ ) illŒøˆ7{O—œÝ�)\^¯W^¯—A$LÌIhõˆãj§ì=]Œª¨ëlÄï?$¿½{÷ê±ÇSMM ƒ°¯×«ýèGzì±ÇÔÜÜÌ€À(Š9 �æÀôŒú�XWmm­¼^¯öîÝ«êêj`�G---òz½zöÙg I` «‚„é5 ô!±®àjYR`©gB€Uø|¾°ß‡„$0zbHBû�PA‚Ñ@’¬Š�FOLIèS£§KyW;9Œ ×Õvóöùóç #$X! ŒŽ˜’ЦήVF £&ïj‡y›i6Ö´ä¶ÛÌÛ„$+™óÍÊÈÈ�DH£!¦€¤¥¥Å¼=¥ÇǨaÔ„V�„ÎÉ…u|uý×I–4mú =ø× I`”Ä�„~0 ý‹>�hƵ+æmþ1`]„$«š6}:! Œ’˜’ÐlBÿ¢$ZhCàÖV¦wY!ÉØfØRµ ?S«æ:´j®Cn.Ъ¹•¹rä0Ò ÎìtÍË5äÌNÖ8ÏË54/×�aKe0‹ $€Ñaò?ÎBþ¢Œ&V±ÁW×M’tä½÷$BIªªªbpnà‡ýå®ÉZ˜ŸõûÚ¢)jlóë_N´ª±Íoù1[[8Eór }Üæ×Ï�œòx?¼d†$éù#§OÀB‚!É‹·C—.]2C’GyD.—‹€8ˆéÏO,ñ‹©ðÊó6)•$cÇÒé“ôð’Z:m’Žœê¸¬�Ûüæ—ùsœk¶�>‰�€a¢’kÐ ’qÝó–/J·,ë½ßÙ.ýû>éìÉä{'g/”2'IMǤÎÏ“êÒìê2o{½^þJ*IÆ€²™9Z[8%ðôîkª;Ù®ºævù»¯…mgØRUæÊQÙ̶Tm˜_ V•Ãä½Ô­ýÇ[ÍÛ¬‡JHœA+HBéпä�i·¯–^øƒ´õŸ¥u?èýzà™ë�ï“ f&×;ùÀÓ�ë�½0é¾I]Ým¿amT’Ü8#M«æ8$‘ç�œÒþ­ý‘àóûO´êù#§Ìç7Ü\À S«¿KûO´jÿ‰Vµú»À¢¨$€ÄH¾o+6Hór 9×,íÛ!íþIàëß÷¶¹e™ôßÿMÊœÌw0Ž’Üæ˜Sj~uô´Z.^tŸ–‹—Uw2ÐäÛ‘‘¦Â\ƒ�€ $€ø³%ÝU=øoÝËÒßþuÿç fJÿã` yà™ÈÛ7˜n3ºFo¸qì\ç�¦ÊÔ5·«lfNXo’ µES4#+]ï~vQ‡?½ 2WŽæåfÈ™�.GFšZ/uéð鋪;Ù±R%¨0×Ðr×d92ÒäÌNWë¥.µ\¼¬�Û.©®yàUØ [ª–N›¤ù™æ5¶\¼¬S—µÿD[ÔŠ�áî´0?K·OË »Ö£g;#Vå8³Óußõ©M¿m<S8 ùC¦ÛI·,ë­ ‰|œ=xîo^–Êî' I0 ó3ÍÛ‡O_Ò¾þîkúþ›Ç#>çÌJ7WvÙps�–N oæêÈLëY:-[ÏnŽ’DÛÏ‘‘v=„ÈÖ¯Ž}1°pf§ë o’##­ßãÎìt-�6I»>:«ÃŸ^ˆË~Ak §¨lfN¿s.›™£¥Ó'éù#§ÂBÖj†0,ó €�HFîÃÒ‡oKçN*Jú6m-˜)•m4y :{2°_ÝËý�·î�c½±+Òܳ±7¨ycWï>3¥?»§7Èy·&ð\ߦª¡Ç+»?0mH lÜg¨ÊîôfÉœ8NÓ1iß ã¢¡kÞµNóö¸n B’dH¦ô$�mñÿy 6sý¸Í¯ºæÏåï¾*Ã6Ae®Éš—kÈ‘‘¦µES´ë?ÃWT[[4Å G‚û¶t\V^†Mór ­šë�3;]߿ݥ-ÿÖ°¶T=¼d†8ì?Þª�Ûýj¹xY…¹v­š›«YéÚ0¿@þ®k:z®cÈûEjL;/×0C¡àµJR™+G ò3eØRu_á=?Ä¥€’’@‚’––ó¶ëjûØ¾š¦£½·¿µCª~4rÐù¹ôäW"ãž��©7}Ý¢@P±ú!é{_ìh|ø¶tó{à s¿eÒœE�°cë¾ð¾'·, lÿä=áçéx皥|W äX±Aúéý±…3Õ2}›·Þ¾Zºç¡Àq><0¶’«½ÉHæÕ¾òÊ+zýõ×ù©'$Aø»¯ 8Õe¸ [ªêšÛõ›†óa�=ס->[Ž ›ægi—z’Â\Ce®@ÆáÓ“`0qªãо±ð&¶T­šë;þÚ¢)fÈñÓw›Ã*6ŽžëPc›Oß_:SŽ ›–NË6’XöÛòç³Í•|"MG:v®S/=öXc›_ß^2Cór æ2l© ëñÎãñhÇŽ @Hq3h}nè_ìí=WÆöÕt~hʪëaÆ %e÷ǶjÍ-ËzÑêG¥¯Nêýª~4ðøì…ýC)Pm²bC l¥+ð<—{6‘¦c�på«“¡ˆïÂàÇÛ·#°ýÆ[ûžk?ÏÁÑß–º5p¬JWà<3'÷6´µ€à‡eŒÁÆsC IhÜš8ó®Oï85Hï gvºY!í+šà2¶}§©ô�Zº¢Nß`%4°8|:°™+G£wJL°òäðé {zø»¯éð§äï¾öÚ±ìì™m:Ì?5F>ßß] KôwðàAˆ’и†'ù¦Ø¼ô}©³=P…‘992ˆ³'{§¨„V›·Û·Cªy!ü¹šACÙýR~”@!¸ZNè¹Ü¾:Pùá» ýôë­úøð€Tó‹ÀyF[š·îåÀ1‚šŽz¦lÝ8�Ý?î?=(Ôê‡Çn:^1Óùyïy®ûA ±íOïOúov¿ßÏOü8’““£/Þ¹|XûRI’xƒô¿X[8eÀD’¾]ûq¿Ç>nóG­–ð_í}¼0×0+2†Í *ª´x÷ôE3ÔÈ˰©Õß¶šÎ»ôT .­úúÃÙ/Të¥î¨ \YÂwpL¹D²›óÍúÓ?û³a‡$T’IoP±ï…@àq˲@ß)P)±ú¡ÀׇúOSùÛ¿L…‰žHÒÙO®gV”}O¾KzãõŸÓt,ðߨ�Ë�û?öá�À~³—¾AN¨?[øï¾(Ûì{!�ܾÚrßø?þéç§?É’$ÆÇm~ÍË5nHUC´ª•`ƒT×ÀÓP¼—ºÍÛórËŒa^ÇŒ8\?!Hü|ãÿükÍý“?a €ëI`x’·Ikçç�ð  ܲìú´•¯‹[–*1úö öãÈœ!òg‘[¾Øg @#Z°2Ð9$Zuȇo_?·Yïl2{ó£ŸsÐì…C?`Œ#$I¬�zcü ­—{÷Ï3ÒbIB§û8³Ó‡®Àh!$€¡4 )**2o×Û T1–¯æo^|ÐéWC„ö¹eYo@²bC ©«˜ÆònM`¹Ýà¿ë~ø ù14N=w2¶cýbãÀ½J¤1]=âO�hÞ¶ÛíüÔbÈIâçè¹N­-œ"IZîœ|Ã’¡LS‰4-(tc€¾*†-U3²Óuêâeù»¯…M÷Ê~0ÚI v©Iu5™9½Kç&ØO$´r¢êéÀ«•¾÷ç�j“7v…L»É½khe™àTŸà5DsîzÇò`À3ÐW,Kß ÍzÇ�_ä.V·‰_ \ Æ™�n® s#�ê„ ò3Ün^Nïtœcçˇ÷íKMa®]ß^2CÏ,Ÿ+gvú°÷€�Õm0}j>ƒÄ ¹’`�Qvô•a‚Ê6„ïS0³7,y·&ò>Áf¯£%RÐS0³··H¤¦°¡‚U!÷lŒüü-ˤ¼øbŠ ,€�$>~ÓpÞ¬†X5×aV” ݥƴ²Íp=;œÙéZ:}RÔ×/›[[/u›SbZý]úøz‘²™9Q«AnŸ–¶ïp÷€…�ÄZ–ݱDÏ<¾I¿{åïthßßëŸþçÏthßßëо¿×ÎçŸÒú{ÿBÙY™ ”¤¢¹³ô‹§Ó/ž~LEsg yÿMßüº~ñôcÚôͯ3˜$c̾½B2'÷.…Û÷ƒÿ-ˤÿñv hð]è�^:%Òª.ßÚ1pUG"ü—>KgNî�ôáÛƒO‹ .|ûêþaKædé�ëKýþû¾1]A’Œ-þîkúÕÑÓfHR63G[þ|–VÍu„ †-U ò3µáæmùóÙf@!Iû�·Æí|öŸhUëõŠŽµ…Sú…$#M/™a†}—Ý Þ7l©úÆÂiýÂŽ2WŽæ‚ŽÐ)ECÙ¯®¹�o„$•û¿{åïôÓ'6ëÎ’Û"† EsgiÓ7¿®úõ³Zý¥;-?fYYv-Y0_KÌWV–}Xc¾dÁüa…+{’«IkççÒûŠôÿs ø¿þ®÷ñsŸ„‡ ¾ �mCƒ�};ÕUOK³z|Ø'†‚™� áÏî‘2'�ÒOkN è N�¹eYà<Î5K¿øëÁ÷?{2Ðä[;_÷l”þðv`ln_øoÓ±ÀòÆ€ÅB‰ž$#ÑØæ×3ï6ëÁ…7iFVºiZ5Ç¡UsÞ¯õR·~uôtÜ«)~uô´‚l˜_ Usråõw˰¥†Mm9|úB¿¾)�m~í?ÞªUs*Ì5ôÌò¹æÊ4y†Í\FøØ¹Î°pe(ûÕ�$ 0¶Bz’$ŸÕ_ºS?Üô yÿýcÔ[ï¼§†ã½Óò§OÍ×�wܦew,QvV¦¹}Íïßb�X’)SzK§[l¹cÿŠšŽJo‘îy¨wIßÌÉRæÂÞ`äÝš@uEߪ‰—¾X!fõ·Â+.>|;44•v6‚–‚™ƒ7>©ÿöé�§Ã+Zê^Ž|îѼ±+pžë~˜š õX7ˆwB–y›&­ $;Zý]zæp³–NŸ¤…S2ìrì\§êšÛ¶$nËÅËÚòoMúúÍSµ ?SŽŒ43 3¿i8¯£ç:"î¿ÿD«>n÷kÃÍSåȰ…-ìᆭº“íý*OF²’ ž–,œo†�>}ÿ©m:rô£~ÛÑGªùý[Z²p¾žy|“²2íúá¦uäèGúôÌ9rzôÿa¬�äååõþc/d5‘1­óó@ƒÕÝ×§�Ì^x½Zâèàa@p¿‚™Rþ¬Þ%槪Õ#_ ¢äɯDîÃïÛt4°ÁLÉž3ð”šÁ^çÉë×qË²ØÆa 9ŸÒŠ8�N~jAH2Æþ4P•á0Ò”—a“a› YuªãŠüÝWc E~~äÔ Û4¶ùõíÚ�£>ïᆭ�ž–$ærijõwÉß}-¦Š•Æ6¿¶ü[“yÁý;ÿ¡î7’kl €�Äz²³2õxHåÈ·ýqXÕH$GŽ~¤ï?µM¿øÉc’¤ÿzÿ}zê¹_2˜ ‰e#§Ó©––IRó„\¹®¶�¯«Î¶gO&¾Bd(ç¢8�KßÀg­\ ­hIÆ–V—¹lîÑüG¨Æ6¿4Ìjó:†¸ÿp÷€…�$9,/¹MÓ®¯Róë—;h8täèGzÿصxÁtçKžËÎÊÔ¼9�ï�÷�ý1ì±% æK’¼s$¦× îW4w–²2í:rì#�9ë�¹beúÔ|-^ðÝT0E�>5ÿDŸhÖÅŽÎ!ï¼–XÏ9–k-š;K™™†:;ý1�=Æy@b½%þ”4F £*´r)´¢ $`dIÆ¿ÿò—_6o¿òê¿iß_½üeeÚû…sgšÕ%%÷üïú΃ôµ5w‡mó� kuäØGÚþË]Qƒ�õ÷þ…þêþû"6‹}ëÐ{zjÛ‹QƒŽéSóõ�7èÎ’Ûú=wúÌ9m{q—Þ:ô^Üö =ç¾+Ò|cÃZ�>sN�>µ½ßµnúæ×µxÁôþ±?2Ý& Ä´ŠMèÿÒ 5Œšæ ½Õ#L¯A"±º ÀªXÝfüš>5ß\=åÀ;G­ªèëÈÑ�ôÖ¡÷ÜïñMêkkîVG§Oïû£Þ?öGutú$IKÌ×Ó�'â~Ï<¾I›¾ùuegešûî{í€�*ãï,¹Mÿôëg#®þR4w–ªþ#3äh<~Rû^; ïQG§OÓ¦æë™Ç7õ[…g¸û}çÁ f8¼ÖÓgÏ~N¦æëoò–GNr1U�„6Æô¥LdÔí÷óû­·b)´’ HTH"QI°nHB%ÉørÓÔÞé牚ÞqÏ—îÔ¾×hû‹»Ì %;+SOlþ¦–ݱDÓ¦ækõ—î [ çÎ’ÛÌ�âÀ;Gô£ç~¬þÒ�ú΃+él~P•?öš?Üü ¬lqWر³³2õ‹Ÿ<¦Â¹3õÃMêÍ�€g¸ûÍ�ñ|ßüMÝs×2egeêž»–éöü ß|I*¦ ’¢¢"óvó„F-ÑžüÊÀMW-$´b‰_έ�¤o%ÉÁƒ€eB’¾•$^¯—Á£²2{ÿ�¬êˆ·Æã'õTŸÀàbG§~ÒÔuÞœ™aAD°iìé³çõ7?z®_Qóû·´ýÅ]f(:}çÎ’Û̪’W^ý×~K_ìèÔ£Om3ïßsײí*Úùnq—9¾wÞqßxIlÈ$­!K®‰Z±^Ïh¼ IDATÄ¿-sÿdžYE"I *--e`IoÚôéšû'¢ÿüðCI��Äçó10cTèô”�*HŠæÎÒw¾¹aÀcí¯}»_¨ Iÿ\y‘‡‹�êèô)+Óv…sgšÓP¶ýòE}½šß¿¥ÿzÿ}šV0EwÞq›Y• ::}Q{ª|z朶¿¸KŸž9§ÏΜÑ~a×úZôkm<~R‹|�oº$S@ú—{ï„LùRÒdïébô�p¡K¡•L@¢ù�ÿÐ?î~żït:µnÝ:` ÿøÊ?˜áˆ$•””Èår©¾¾žÁƒBC‘% æëÈÑ�"n—•e7Wd‰&Ú //‘BƒÐ°d°Uc>>~RÓ ¦˜+æH�¾*ÁcÔ¥ï4—áîëµÂl±nºÔoCÚT¹¯´0zH¸Æ‰SÍÛL±A¢E Gy䪗–ð�¯üCXeII }¸Æ¸X§Õ|væ¼~ýòoû=~SÁ”ˆSMF⦂޾(ƒ5�m8þ‰–ݱ$¬ñihX2ÁýF2Õ(QÓ”0~Ä�™I½­€€ Wo+û Ê‡T$áÀÊGƧÐi"ËîX¢_íúMÄí>=s.âsKÎ�{@¬äªì¬L]ìèTJJŠyÎCÝ_’. q% Tj¬›·Òò9$\hƒV¦× ‘GVF82~}z朼s$ðïå¹³†NÄõßðC˜¦Úd6Xm\x¨×œÎ3ÆãWÌIèÔ›ƒ‘CÂ…V�„t@<ެŒpdü{ë�Þ÷ï;n¸áçóÙÙÞª–Ð~$?c^>tjKðvf¦1à¾ßذV¿xú1­þÒ�#Ús@b·Ûåt:#~x�þ#H4Â‘È [ªæå1}¶Ô„�‡ÃH3_g8‚û:Œ´¸7Þç 7 áHr¨ùý[fÕÅ�%·é¯î¿/æ}ßÿUYþ¿Þ ’u÷þEÔíŠæÎ2¼¾y¨÷ûðȱ�Ìç—,ŒÞXvÝ_~YKÌ—ûú5 w? ”m(‡ö!iH+Pq÷YF À9ååå1(ˆ+‘èœÙézxÉŒ˜·o¹xYo6®Ã§/Äõ<–Þ”­Us‹ß®ýxÈûûú5ì?Þªý'ZãvÜxŸ'Ü„#Éåѧ¶içóO)+Ó®olX«% çkû/wE�î²ìŽ%úÚš»ÃV¶‰WƒÒ†ãŸhßktÏ]ËtÏ]Ëôϵú­®“�•©n~мº”ð¾×è¯î¿OY™výÕý÷E\•æÖš=G‚ûw?`ØIqq±^ýuI’gâ UøÿÀ"!Bû�0½„#c›3;]n.Ð¼Ü íúO‚sëG’ϧgÎé[�þXO?±IÓ ¦hÉ‚ùÚùüS:}朎ýH§¯O{ Vm„®ÓÑéÓ¯_þí€ËßÕöwiyÉmÊÊ´ë?yLÿ°ç_Ì©@Ó§æë¯î¿OÓ®÷ yåÕ P.vtjû‹»ôÃMjÉ‚ùúÛŸü@»_ýW³ië=åËtÏõé1û^ë _†»0ì€$tšC‹Í!ï„Lå]¥K0âÏ3±÷¯×4háÈ�Ó·ú"Èa¤)/æy9†ÊfæÈ°¥jé´I:ÕqEu'ÛÇô5}Üî×þã­¼¹GG’JÃñOTùðãú΃Ì•i¦MÍ×=_ŠÜ´´£Ó§7½§ÿùòo‡¼bÌ`.vtê[�þX�oþ¦ çÎÔ×ÖÜ­¯­¹»ßv¿~ù·Wשùý[f%HÑÜYz|ó7ûmsà�#Úþ⮸ì ) ÉËËÓ¢E‹ôÁ\ÿëT¹¿žQD\5OÈ kìv»ÄáHü´ú»ÔêïRc›_ÇÎwêonèe®Éc> iló«±ÍÏ›ÀrG’ßÅŽN=õÜ/Í Ž¯”‚’¹3¿�ŸÔ§gÎÉó‡?êÍCïõ›‚ôÙ™óúõË¿5oGóϵtäØGa�YƒŽ¢ÿãáêkkîÖ¼935}j¾ çÎTãñ“j8þIXuG$ÿ°ç_ôÖ¡÷ô•»–iÉ‚ùºiê}væ¼>=s.â´�áî7Òkh �äIðÃj0 98q6 âî`úœ°_Ü|xáÈØÖrñ²Nu\ÖŒ¬t92Òä0ÒÔêïb`€p70(©ùý[ªùý[ÃÚÿÓ3ç"VvôËñG2u'ÖóÉ~#½ÖáŽ1Ʀa$;wî ü£˜i6H€&:þ߀‘"I¼£g;5#+]’”—a3’`³×w?»¨ÃŸFnâºtú$Ý~S¶$éù#§¢¾†3;]wÏÉ•3;Ä´^êÒáÓUw²]þîk1Ÿë`¯WæÊÑ‚üL†¬JÓrñ²Þ=}Q‡O_ôµFšVÍÉÕŒ¬t9³ÓåᆭÆ6ŸÞlþ|ÐÊ•àkç692ÒÔØæ×©‹—U×òyÄÐÉ™�®û §˜×²tú$­-œ"Öª£ç:bzM„#€a$v»=lšÍkéÅZï;ÂH".š'äÊ;!Ð4Ê0 ŽŒÎìô°0!(2|<À‡tGº-,Œˆjl˜¾¼¼##M«æ8TæÊÑóGN…½î€F”×3l©zxÉŒ°k ½>gvºVÍu øZ ó³´áæ‚°å� [ªægia~–v}t6bPäÌN×ýó ú½va®¡Â\CK§OÒþã­ªknïwÎÁké;F ó³ôqû%ÀâG v¶áìTZZj$Lt� nj�ÞkG0R„#£Ã°¥jÞõéþîkCªæˆÕ†ùòw_ÓoÏ«±Í/»-U ¦djÕ\‡llù·¦½öª¹3 Ø¼UÇÎwªåâe9³ÓÃ^ë¾Â)Q+]¾±ð&ó+þîkzæÝæ°çÛüò_½f…¹†‡<¿¶hŠ9~}«SŽžëÐÑszpá4-ÈÏÔª9¹:z¶#bdØRõÓw›c®¤I&GþãßÕtâ8?�H*7M›®›o¹…pÆz@ I:$I:”1WÅ$™ƒésäO�øÐåp¨¸¸8a¯õÚï~Ç€�ÿñïïêÛ›¿K8rÍË5´*ÅþA|Bªf\¯´�ªÒz©{DÅ@êN¶GüàøÓ ZzS¶æå–ÉëCŠÐé1a¯õÙEµ^îV«¿Kþ®«·Ù¢5bpRw²Ý H齿z�Ùéf0³ÿxkÔpãŸÏkA~¦iZ:}RÄ êðé – G$…}’ÉW×­×’?ýÓ!ïG8Ã3쀤¢¢¢7 IŸ£ ÿ1šµbDjŒæíÒÒÒ¸ßétª¥¥…�G>;}zè”Gâ*Øc0‡O_Ðo·¼Ý±óÑ¿4¶ù5/×�aK•3;}Ø!ÁÇ×� ,Žžë4§«H�¥�²:Ï©^ÛHë `i½¯ß½WH«¿K­—ºåȰiFÖÄÈçß~ÉRß›.—K���ü�"©µ¶=ô%€áv@’——§ÂÂBó'{�ªêx‡ŰLŸÖœµ¼¼<î¯ñÐCéàÁƒ ö8PSS3¬ýGð�óKÝQ—ì 6ÿ öêH¤�Ž,D«þˆÅþ­z870Íoé´IZ:m’¹ͱs>5¶û]¾Ø{©{Ð×qfõ6b ;îžãÐÝs¢ï¼¶¼�P%ì½²ØÒÊš2eŠ|>?¨H*õõõÃÿG`dl#Ù¹¢¢B?ûÙÏ$QE‚‘©Í(2o———'äm^^ž***ìq`8 áHbþôB¦Í$‚a›0ì}ÛüzþÈ)­šã0›Î†®@Üæ·�ç£6C )Fï¯áX*u$™Ó›¬În·'$LÆ‚á$„#0r# HŠ‹‹©"ÁˆÕÛ Ôb ô8HTõ’á‚üÝWGö¡¤Í¯Æ¶SriZ˜Ÿ©ÂC ò3Íç s =¼dÆ�–ø|{›­F[C8ñaéB«HÏV—y®e3s"Vs”¹rÌ0çðgã2>þîkæòÈŽŒ4}cá´~,…¹†ÖN‰ËuH„#�¶xhýúõzê©§$I iSå™è”ûJ #Œˆ|)iÚ›ÑÛ{¤¼¼œêÄ„pd|©kn7›œ®-𢲙“åõw˰¥Ê™�Ï›@4§:.›Kï6¶ùÍýÍ�¡ñü°§×ý¦ñ¼^2C†-U/™¡–‹—Ícæ69®O«9ÕqYGÏvÄmŒŽžëP]s»Ê\�`fKé,óµû^çóGN�ø:Œo„#�8©ñ:�ËåÒÊ•+Íû»íKäKIc„ÑîÌÛäO ”‰;ªG‘ñ§±Í¯_ýÌœFâÈHSa®!gvº>¾¾¬î©«þü½S:|ú‚$™ûÊç�œÒáO/Œø\[.ެ$qf§«0×Pa®a†#uÍíúù{ñ)~Óp^¿:ú™yÜàk¯38VñjB `|"€Ä²Åó`:tè�ü~¿¼2U›Q¬ ÿe„©·èPúó>¿Ø Â‘Ñ 5¾]ûqÜŽwô\‡Žžë�3;]3²ÓåﺦÆ6_XÈéõöŸhÕþ½}EvýçYý¦á¼ sïyKÇåW}‰v }�ªåâeýüÈ)9³ÓeØRÍé.­þ.5¶ù‡|¼XÎ'lœÞì�ÃHS^†M3²ÓuêâåW·‰÷{`ì"€Ä‹k@b·ÛµnÝ:íܹSR aké•Ê»ÚÉHô3ëóö¢E‹T\ÌÊGáHrh¹xyÄþîk:z®cTÎU’%I¤V×€� ë!€Ñ‘ïömØúBÖe˜ö·š«†¡õë×3(áÀÊG`ô¤&â ¡z[l½b_ÂHCõ¶Õؘ÷+**hÌŠÁÿaH8°¨ƒo €Q”�€Äåriݺuæý×�by&:m ó¥¤iGö2ó~aa¡ÊËËÄŒp`5—.]2oŽ@â¥&êÀåååZ´h‘y¿:s©9µÖ³#{™¹j�az衇ÄŒp`e„#0:Ryðªª*9I’?u"ýH,j¯q«Ò¦š÷7nÜÈ]ÄŒp`e„#0z�Øíö°Jú‘XOß¾#«W¯fÕ Êétšÿ%XIQQ‘y›pF—-Ñ/ìG²{÷nI�~$®«m*½|‚ÑOr‘úŽTTT00Ô#�<¢C‡©¤¤„p`)ÅÅÅúîw¿+¯×«ÒÒRF‘m4^¤¼¼\õõõúàƒ$I»íKäên—ëjï@’ò¥¤éÙI+é;‚a±Ûí4ñXÕ¶pc¤ŽÖ UUU™eóþÔ‰zvÒJ5OÈåHBÁp¤Åæ0cš`,µ€$Ø�Ä0 I½! +Û$Ÿ�Yw„…#•••r¹\ `ÌJÍËËËÓ#�<’¼�õEùRÒx'’DuæRy&:Íû•••ÌŸŒy©£ý‚.—+,$i±9ô줕„$I :s©eÌ5ï¯^½šp0.¤Þˆ ®lDH2þÕf……#%%%¬X7RoÔ —––ª²²Ò¼OH2~Ug.ÕîÌÛÌû%%%ªªªb`ãFê�|qB’ñ¯ï´šÂÂBÂÀ¸“z£O RHòÔäU,<ÆùRÒú…#%%%úÞ÷¾ÇàÆ�Ô±p}Cï„L=;i%!ÉåKIÓ³“Vö G¨ŒW©cåDJKKõÝï~7l `B’±'Ž´Øæc+W®$Œk©cédŠ‹‹Ã–ö§NÔS9w«6£ˆwj ¨·豜¿ G*++µ~ýz0®¥Žµr¹\a!‰$íμM;²—ѼõªÍ(ÒÏ&—ËŸ:Ñ|¬²²R¥¥¥ `ÜK‹'år¹ôÄOÈétš�y&:iÞzøRÒ´#{YØ2¾†aè»ßý.á i¤ŽÕËËËÓO<¡•+Wš�y'd2åf5OÈÕS“WÉ3±7¨*,,Ô�üc3@€¤aë'¸~ýz«ººZ~¿_R`ÊMó„\­ó‘½§‹w1jì ´×¸5챕+WÒo�”RÇÃIºÝî~SneÌÕc9©Césxã¨ÞV Mþ‹°pÄ0 mܸ‘p�´RÇˉ§Ü”””˜�ùS'ª:ë–Ž_JšvÛ—èg“ËÃV©q:�zâ‰'äv»$@Ò²�·®ªª’ÛíÖ+¯¼¢ÖÖVIRCÚT=•s·*üÐJÿ™v3Dž‰NUg. [¡Æ0 •——«¢¢‚$=ÛxÁ»;ˆz[�jì·ª!mjØã………zà�”——Ç ,Á6^OÜn·«¢¢Â¬&ill”Xé¦:ëí5”D-q8Z¿~=Ói–cïàr¹ô½ï}OÔîݻ͕nJú‹ŒHÒêÕ«U^^.»ÝÎOÀrlÉr!¥¥¥r»Ýª­­UmmmÄ ¤ôÊ Ëõ(ñ¥¤éƒ‰N½–QÖ|5¨¤¤DL§â §ýœ®¥¤0@¼~¦º.3`ÔØ’éb‚ÓnÊËË#%{�[õZz‘ÜWZ_]§’ö�mž�«ÚŒ"y&:Ú¯Œñ×íy�AÆ)[2^Tß äàÁƒæŠ7þÔ‰:”1W‡2æÊ¸vE¥—O¨äò ¹®¶�ûë¬ZÄ0 ¹Ýn‚ ަL™bö@�Øßí‰dKæ‹ %:xð öîÝk%R ,©5ŠUk+ïjçõª’u�7ר�V OšSõiùC)Ð|µ´´”#@”——K’Ο?Ï` ²xñb¹\.$”Í*ZZZªÒÒR577ëàÁƒòx544 ˜ùS'ª!uª”eãÖ¸žgpêLQQ‘Š‹‹i¶ @�D`·Ûû&ÍÍÍjii‘ÏçS}}½ü~ÿ€ÁÉH9åå婸¸Xv»]N§SS¦L! Hb`·ÛU\\l6;­¨¨0Ÿóz½:þ¼¼^¯¼^o¿}ëëëû=ær¹úM…q¹\2 Cv»�NýŒ2’ÊË˰ª#4LcS*C¬Ž€XSlÄlræD•~¡€��t¨ IÇ0Œ!mO@`@ÅÅÅr:� €qÃ0 •——iŸ”žžž†ndnø9ñF0Lô �úúzíØ±C~¿ÿ†Ÿ‹azâ‰'\rDÆ›hhháˆ$ùý~y<Þ†� ’8ÉÉÉQNnn{[›ÚÛÛ%I>Ÿ�7€a ‰“%úgºëË_õ×}íw¿Óë¯ýž7€`Š °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$ÀòH€å�Ë# –G@,�€X °<`y$Àòl Æ›¦¦&}òÉ'’¤Y³fiöìÙ `D¨ À€rss•’’¢9sæ ºí–-[”’’b~µ··ºOuuuØ>Ñ455ióæÍÊÍÍÕœ9sTVV¦²²2Í™3GsæÌÑÖ­[|½²²²°×‰ôµbÅ mß¾=¦ó$DU]]m†MMMª®®Òþ{öì‰Û6‹/Ö¶mÛÔÞޮɓ'kùòåZ¾|¹-Z¤¦¦&mÙ²E‹/–ÇãöõÖÕÕiÓ¦MZ±b…šššø ! @TÁ𢲲R’´sçÎaíM{{»^}õÕ·©««Ó}÷ݧööv-Z´Hï¿ÿ¾ÚÛÛUWW§ºº:y<½ñÆfP²bÅŠC’'Ÿ|R===ý¾ÚÚÚôÒK/I’<�xà¾ÀBHQSS“^lÛ¶MR ¬ˆ¥²bÑ¢EšC‘““£ªª*=÷ÜsCºV@r @DÁé4÷Þ{¯rrrÌ#– $''GkÖ¬‘4pIð¹à¶‘Ρ©©I“'OôusrrÌsöx<1M݉¤ªªÊ¼M@ÖA@€ˆ‚Ói‚áE08ˆušÍ`Ipzͽ÷ÞušàkUUUÅ´R�Ûí6ƒœá$k" @?Áé%“'O6ƒ‘5kÖhòäÉjoo�©YkpûhÓl‚LjV="Éì%RVVó¹�7Ô~)A¡ÁJ¤é<€äD@€~¢…Á°dûöí1g *’¾*}ÕÕÕ™·srrb>÷X*M¢Ù¹s§6oÞ,©wjÀH¦½½Ý /6mÚö\ð¾Çã‰i9ÝhISS“<OÌ!ÄP*Hú¾N_;wîÔŠ+¾rss•’’¢ªª*µ··kÖ¬YC^Ò0¾� L0̘5kV¿)&³gÏÖ¢E‹$ÅÖ¬5tšMhX1XsÖ¾B«I:�'R5ISS“¹Dpð+¸Ï¬Y³ôä“OÊãñP=cc*8}æ“O>QJJJÔ킽E Ö¬Y£�;wjÏž=fÊÎ�;5yòä’Ъ‘�– îk°Ê–ÊÊʰ•j¤@�2’©9¬-Õ IDAT€ñ� ˜b�:#B‹XVŠéÛ458½fÍš5ƒ†+³fÍ’4´ ’àù/_¾<âó³gÏVYYYØá€€¦`ß�åË—«§§'êWee¥$iëÖ­ƒ38ÍÆãñ¨©©iHÓkB+N"U‘ÔÕÕ…:�G¯¾újÌÇ ˆ€¦`•Gß)(}Ç`?�Á·¯««‹izMPUU•¹´ð<ÐïùÍ›7kñâÅZ¼x±<�¹Í¬Y³ú5˜` $�¨ Vi ^¬Y³ÆœþËj/Áãm߾ݜ^‹œœóø{öìÑŠ+Â*I^zé%UVVÊãñ˜!I¬ç@(Hê]Y&Ö¥wƒU&Ѧ¿„ �f¼«5kÖ襗^’¨@ÉÍÍÕŠ+´yóf½úê«úüóÏö§á*`8H ¦¦&³wÇ`Ók‚B·JI¬Ókú¾Öû￯{ï½WR (Ù¶m›¶lÙb;•••º÷Þ{ÕÔԤŋkëÖ­aK 0–ù€rrrôÆoH _^w ³gÏ6÷ VœTUU©¬¬,bʶmÛTUUñ9·Ûm+·Û­={ö¨©©Éì}’““#·Û-·Ûm7¸œð–-[´|ùr³šdÛ¶mjoo§ºQJOOOÃ0<{÷îUMM�$iå]_Ò]_þò¨ŸÃk¿û�^í÷’¤Õ«W«¢¢‚7@�ª–X«a0¸ÚÚÚ˜—€€Ín·«¤¤Dn·;æ}¨ @R"‰¯×«Ý»w3Æ•æææ!$ô 0 óçÏ3Ư×;¤í© ³)ùù*[¹’�0fýã+¯ k?1KOO—Ë5“��t˜b,�€X °<`y$ÀòXÅ`UWWë“O>Qee¥fÏž�×TWW§7ß|S³fÍRUUo2`\¢‚`UWWkË–-jjjJškª««Ó–-[T]]Í ·¨ €òx9äñòx<Ú¼y³*++UVV¦­[·šýKfÏž­ªª*ó¸Û·oמ={TWW§œœ­Y³&ê9¶··kóæÍòx<òx<ÊÉÉ1Ï1Ò8´··kûöíª®®6{Âää䨬¬LÏ=÷\¿&ºCÝ^’šššÌ1 í;|o׬YqŒ‚cÒÔÔd¾Æ“O>)�Ç£�;wª²²²_CÜ¡^?€€bÂì[•Z0“�Ɖî?HWã�lÚ´IÛ·o—$-_¾Üü  +^z饰²�Go¾ù¦ÚÛÛÃŽóÀ˜!Á¬Y³4{öl½ÿþûºï¾û´fÍÕÕÕ©§§§_`ÑÓÓcî;yòd-_¾\o¾ù¦ùúo¼ñF¿Ç›7o6ƒ–E‹iùò忇ê={öè�7ÞèÞlÛ¶M›7o;?�Ç£ûþöî?¶­ëÀý×¹S¤ä°M)¤ôåóù|š‘3Åèïï‡ÓéÄØØ˜,ƒßï0=‚BÜW"‘@<—ÓÕ#eD¨°oß>¤Ói´··ËzˆÇã€ÍfCWW—æ˜p8Œx<·Û-ïG]çétZSwž_„^ …rŽñù|ò>õA”a†‡‡eûŒ��Áétš®a#ÚÌï÷Ëk‰:÷Ÿï9"""$DDDDs HÈé)j�@>Ÿííí3né+:ÖíííšÑv»]Ž È' j¦jØívyÎS§NvÚ»ººr¦wtttÀçó!‘Hh¦òˆ¿ë§uˆòÙl³Ÿ^�F5SSÔ×Õß—ÇãÑŒÜP“H$ÐÔÔ¤9˜žÊ"Â'qïêã½^oÎtQê6-öý"äp:�9Á‰ÝnG0̹u`�F5çs¹\¦Û(‹m£›šš‡5åóx<2)$¬#"b@BDDDDE{1ÕD?’"�¢££Ãpm 5bè×”̾¯ôŒ¦½ˆ),ùÎ) õH…|å³Ûí3–ÏŒ˜ª£?_¾ûïW ¢¬3ÝS"‘�#cÄyz{{åZ,êkˆ¶Ó_·Ð÷{<¹¾K¾gGM܇Óé4mSýÔõqf뙈så+q """¢Ys¹\hoo— ‰F£Q¹�¨×ë…Ïç3]ðTß¹5 5ò}¿� AM=m¦¹¹Ùð\â=.\ÈùžYÐ3S”¯þò1ºo£cD€ÓÝÝ�ÞÞÞ¼çŒÇãðz½èêêÂéÓ§ÑÑÑ!ƒ,uÛ©û~}ùâñ¸œúcTˆg!_½¸\®œQA¢­z{{ÑßßoxœÝnG:�F"‘˜u{-w Hˆˆˆˆæ ««KNãèïïG:�F8–S‚Áà¬vzÑwnçƒ:Œ1[ËBxõûÌ:ÖÅ.ÒºP÷]ÈZ(ú‘'áp]]]¸pႜZÔÕÕ—Ë…ÎÎNͨŒbß�FqàÀ�œ0ÄívË`­Xf;åzÿ" ""¢\œbCDDD4G---ˆF£H¥RˆD"ðûýp:�ráN³µ# f»ªžè{<(Š2ãõ1úpÁ,tø" ÏxOú5T‚Á ‰ÆÆÆÐÓÓ#G‚$ ìÛ·Osoż_L»kƒôôô```Š¢ �ç¬K2—gA¬#Οï�Xû„ˆˆ�Í›t:�S§NÉN±˜^#¶¢�ç|»‡ØívÙÁ5{ß|$bD†˜îQÈ=©;àf嘯òÍ–zû\3§N�ÒLMI$ší–].�€ ºDÀ%FÐû~±XlSSb±�€&l2*«b´ ´púôéœï‰`¥˜û'"¢\ Hˆˆˆˆf)ÃëõjvG1ê¸ÎDüVßhÊE"‘˜q]�By<¹#Ž~·uY¼^¯f¤�˜6bT¾t:=oå›-<è·Ìb±¼^/¼^¯ Ä}ÕƒÝnÏ™ÊRìûÕ»Þ˜=;Fí#£ëˆàÍìþͦì¨ïŸ‹´™c@BDDD4Kê_ô�Sõ÷ÌvQw¾m6b±š››åŽþþ~ìܹs^Ë,:ÞÝÝÝ9eV/rªH:::dùÔ[ŦÓiìÛ·Ït´Ãb ƒp:�H$hmmÕ”'‹¡µµÀô6Êb�:ôѯÇÒÝÝ-G[ˆð¡Ø÷‹ÿööönå«•ÔåSo>Œ �#�£»»Û4ˆ+ôþý~?h%"b@BDDD4ÿ\.zzzd'uÅŠhnnFmm-š››‘N§á÷ûgÜ×n·#‹ÁívËßö×ÖÖ¢¥¥N§3ï)ÅjiiA(B:�Ö”¹²²R†"===š‘.— áp6› ]]]X±bvî܉ÊÊJ  ½½ý m»ÝŽh4 §Ó‰h4ŠÊÊJìܹSÓMMMšQ�@~¿étZÞ¿Q=ˆ@¡Ø÷‹Ð+‘H`çÎ�hnn–ïïèè@{{»Í£žãõz‰Dd]ïܹ;wîD0„ÏçƒÛížÕý»ÝnÓQCDD4�»ØÍA €ÇãAWW‰b±˜ 5�@Îè1íF¿C‹ÇãA,C,“k„x<´´´ «« ýýý9¿ýÓ1̘½ÞÑÑ!Ï+ÊÜÔÔ—Ë%·¯5 VÄâ¢ñx©T >ŸOŽxˆÇãO)²Ûíhjj2}¾ûr¹\²¬úú‹Çã‡ÃˆF£ˆÇãò:�@À0¤S¤¢Ñ¨¬·Û ŸÏ‡`0˜S¾bÞo·Û‘H$䢮úçÂëõÂårɲª)Q×âY°ÛíòYP/´›ïþŨ•|÷ODDZ+±D9íþáðþûï¾¶çIìùÿaÑËpâÿÿtâ8àOþäOð�o|ƒ C´ˆþú¯ÿ£££€UÞ?ÅÊ �°RˆSÿûWøýÙ–ÄÏÐx<—Ëeº­m0Dww7B¡�áî'´|œ:u*o@´sçNÄãq ,êv½ü1þæoþਮƷþÓÿÉÆ"¢%«ó¯ÿù÷¿ýÛ¿-ø8N±!"""ú‚ƒATVV.Ü™N§Ñßß ðE_éÁåõz±bÅ ÃÅTÅz$|ˆˆ"""¢/˜˜†sàÀôööÊE6ûûûÑÜÜŒD"·Û=ãb¯ôàëÍ´¶¶Ê`LìÔÜÜ @»Ø,Í®ABDDDô ƒˆÇãèíí5\+ÂívŽ.¡åG¬s�Ç 1õº/DD4¿�-‘Žq ÈY¤ÕëõräH ±Ûír±U±¸+ùpj ÑÂa@BDDD´Dx½ÞE]x“–.î:CD´ø¸ •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$D´¤]¿~'OžÄõë×YDDT?÷^}õUƒA|ðÁ¬"¢EÄ€„ˆ–´£G�âç?ÿ9þëý¯¸xñ"+„ˆˆ–µx<Žëׯ#›Í¢··—! Ñ"b@BDKÚøø8 ›Íâ?øC""ZÖnÞ¼©ùš! Ñâa@BD †$DDTŠ’-$Dô@aHBDD¥ˆ! ÑÂc@BDŒòòrÓ!É÷¾÷=~P$"¢eí+_ý*6nÚ$¿fHBD´°�ÑãÏþây’ðƒ"-wËCø³¿xž! Ñ"a@BDŒM›7㿼ò*?(QɰX, Iˆˆ "âE2µwKön©B]¥…•Qbª,«dûWYV±Bˆø³�ˆhÙc@BDü H¦öÖVaom¶Ú�”šµåe²ý×–—±Bˆø³�ˆhÙc@BDü HDDÄŸ}DD%� -»Šáp˜•C4G×oMáØo'pì·¸~kŠB´Äö1$!"š;$D´ì>(þó?ÿ3C¢9šÈÞÁ±± ›ÀDö+„h‰ÿìcHBD4w HˆhY|P|ñÀÿ�]üÇò{¥’XÊf÷Ïx•eÕ¬�]ˆòSî…*Ãl�«¶®^ÔãæÚ~ ¹àª¥lå¬ÊµÐÏ ÑrýÙÇ�„ˆh~qÕ5"Z6þãŸþ'ÀG¿þ5€é��À²ºOo��­X³³Ìh*‹äämô�^3=®®Ò‚¦v¬_£ùþøämœºø/gfÕ±õ>bÇcëÖtøGSYüæêçˆ]LÏ*8h­[8òQu•|½¶*ç~#£×0>y°cý4ÕØ4ï‰}𯱱 d§îšmukQm]�ªòéÐ`âÖŒOÞÆÿKÉsÝóÞÚ*ìØð°¹–ðXÊVbÿ.‡aT•¯Bã¦éc�|”Ô´�¥l%^n¬É 7f:n¦¶S·Ÿ¨k½§¶oЄê:Þ¿Ë�¾Ñk²¾Õõ¡n}=‰ïo­´à?×oÈ©œm­´àÈGÉœö¯«´à¹›rÎ[m]�jëj8¬_ÂÚòUØZiÁùT–ÿ°1$!"b@BDÄ�DÕÑUuDcÓº<‰ñÉÛ¨²¬ÂŽõ£­nª­«ñÜŽ�xsð¢ì”ZÊVÊöùTïŽ\‘#ª,«ðͺuxlýô�ÆÍý¶^–²•ÈNÝű± œ¹ú9&²wPm]�¦7U`Çú5°ìøÃ�¢Ð{NÞ¸�ØÅÏpæÊ XVý¼56xkì°”­”�ÑùTÇÆ&0šÊ¢Ê² O×oÀÖ{!�þžÔ�ÊÄ­)ô}r g®ÞЄ Ž5«Ñöè:$oÜÆ¨ªƒÞöètg§î¢oôšæ¼êÐå¹Ññ« šãªÊW™÷TýXÊVâ?×oЄTU–U²¬É·Ñ÷É5MyÔ×lÜT�S?Ó,ÞGì2Q¯~fò…/3i«[7Ýþ¿�Ào®}ŽñÉÛ¨«´ÈÑ♊}šÖÜ“GD}œ¹rÙ©»Ø±~ Ú]—è|QnÞ¼‰ññqþ£J‹êúõë Iˆˆ�Ð\½ÿþûxÿý÷YD&!ÉÅ‹ñÒK/ᡇzàîÇûˆ]þÖ¾oôš¦Ã9‘½ƒØ§i$ïM•¨*_…½[ªÐ÷Éôt›îO©ùñ™ËšßæOdïàÇg.£ã«.T•—¡q£µ €ä©íäoÿõ£Æ'oã½³W0‘�ÂÞ-ÓScv¬_#Cˆbd§îâ­_ß���º‹¾O®aÇú5¨*/“áÈ[ªFÜÓ›M[¦;䫵?êÄ´‘‰[SxsðSM}ŒOÞÆ›ƒñâ.¶VZðÔö š CtÜcŸ¦sêiðRU«Ë°wKÕôHË*Dz\µuµæ8o�M¾O]êc³wîâ¹�­{X¶…˜Ò#‚1uÛ럙¹øÉ™ËšÐf4•Å[¿N¢ã«.XÊVbǺ‡5ÏëÞÚJÓgçÌÕMÝÄË�� ªü‹ýˆrýúu¼úê«üÇ” IˆˆæŽ«¢ѲIÔ ·Ž��ã?ønÞ¼ùÀÝKÃF«ìÔ«;›j£©¬œŽà­±¾ÇlaПœ¹Œ#%5Aƒé‡ð²•r“ÁËÓµ:bÓ˜¸·=ì×k+gu߃—3†ÓsÔaKìâg†ÁJòÆt¹Ôë’ìX¿FÖAß'×L§þˆµ\ªÊW¡qsîh‡õK¦÷|ä£$^>õ[ÃõDŠ=.yãß»˜FìbÚ´¬fÁÓŽ kda¶ˆú™™ ýMýß{.ºgN„EfÏŽ‘ôE»víÿ¥/\±�>n%"šŽ yÀmùÃ?ÄGÿò!Òé4+ƒ–µ¯|õ«³I¶üáVüýϦ I´‘$¢S?ÓèŽØÅϰõ^ PWiÁh*«éÀ>·c/e0šž^DU0 9f Y†.Oš¾O,Ô)FFÌÆùÔ-ãsßQ�ú¸q{Æ÷È€bÍ—Tá€yP¦®õ”ó©,¶Þ³—C—'1šÎÊP#;u×00˜íq3µ·¥l¥lo½­örMbæÌµÏMÏ1“3W>7¯Ã·±µÒ¢YgDVå{v/eæ4õg¾•——k:œD‹¡ªª ÛÿÝ¿+ú8Ž$!"b@RÒÉ_½zˆA”Ç®/Ø�D.LÜžÊûÞìÔï︶¬RÓ�ðÁË4nª�‹gz±ßë”ßÄùÔ-Ó‘FÔ#®ßš*üþjêH¡Ô÷c¦˜sVYîÿØ{nǦ®}–²•šúï½&×^ ÑŠ@å|*‹¡ßM†M³=Nÿlµ[`Yµrú¿º²é­½· lòFþó&‹ÇŠj;£€ªˆ LÔÿR°qÓ&ü_Ï›ÿ˜Òƒ! "¢e’¨;‰3…ê‘ê‘ï�½‚ß\½‰½[*áX³ZžwÇú5raÌÁË™¼ÓNdyþ ðòœOgUö²¢’ù¶Vµ‹L]�£&Ôõ?>yo]ÄÞÚÊéhï½&v_ñ>bÇÄ­;èûäºfêËl�p¯}Ö¹ú9[ÿpQAÅö,ÿ�¶.óINÞžõÈ"bHBDÄ€„ˆh™†$÷;ë�÷uu§?û{mÇøÌÕ8sõ†ÜÁ¤ÎnÑL�hÜTÇšÕEmõ;Ó¨�­ö¥ÙÁ�¸uÇpK\ÀAMd§�}W°cýl­,—‹ÆÓë–<·c#Ž|”ÔV³9N¿ÅïùTã7n#yãß0‘½#ß÷Öî­yÚhéü¨OÞø7ù÷jëê¼SŽ1$!"b@BD´È!É÷¾÷=<ÿü󨩩Y’eVw"k¾„3W 쌚ü†^ì`"{ݱ~�Y"F4äûíþlG…ŒÎa1Ðù¬Ë­•T•¯š—òˆÐ©ï“kš-“�é…rÍ®Qèqm÷vÜÉNÝÅ›C‹�£¾×|³\f6ÔS¦f ûˆh~0$!"*w±!¢’³ëË_Æü֟ʯ¯_¿Žüà¸xñâ’-³Å0ÓoÕ6Yïß×½õA7Wà¹Ñöè:ÓκØ¸�k¨GTÌ4B¤qóýÝw–D=ªFÕˆ�x ;e+±—Omß ßWm]�¶ºuÓ[)[rC±½°XóC=�f6ÇÕ©F÷Ä>M›†#fS…ÔëÕíÄ#<¶îáE«ÿÑTV>?Mª-ŒsŸ› þCE4�¸» Qa�QIÚõå/cðÊ˧wúÈf³K:$£=Ô |êU[WË-T“7n˵cÍ—°cýxkì†ôéNùýßæÏ´h§XX¼�˜Ÿ³qs…½`¶íb¼t1Ú¶Gך¾Ï[cG]¥EÖ'Üœº ï#÷¾¿ÑjÞÑ-,:Ûã õõÚ*Ó{ÁÔÞÚJÃó7n®(x-–…x–�Bª*Ë*9r†ˆæCsõ�ÖÍêϳO?5¯åÈd2Èd2%[ÿoykYßgr|œÿ1 !"Zº6mÞŒ?û‹çˆ�䨨„ìì>·cSÎoØë*-Ø¿Ë!¿V¯¯¡ÞNõ¹Ç6æu•LÜš*hêÉ»#Wd§þ冚œN¶·Æ.×Θ¸5…c¿�ȹæ[»·â­Ý[±×¤ƒ¿²SwÑ7:=Z¦ª|þª¡F³Œ¥l%ön©ÂÞ-ÓeJÞ¸-Ã�‰ì íÝRo�='àxjû þnrNÇ©ÛÁ(ˆª²¬Âþ]MÝë×yïìÿ'ïõåÆ4nª˜Þ §Ò‚§¶oøB¶Ò�]LË�ê¹±wK¶VZdÀ÷rCÍ’Ù½†ˆ! -–h¤O~­çFFXËL&“Á�Áž¯5³2\ƒ„ˆ’Ü››}ëÖ-’¼ôÒKKnM’Ÿœ¹ŒçvlBUyžªŸîÜŽ¦²Ú…Yïê5DÆ'o㽑+xª~ª­«Ññ„Ù©»Ÿ¼�jëjMgô'g.T–‰ì¼7rmuëätqNuy&nMá'g.¼…ðb¼”�cÍ—à­±£ÚºÕPcXöä�Ûxë×Iͱ?>s_uÁR¶m�®CÛ£ë0šÊæl¹;x9ƒÁK™9×7zMÖqÇNMh"Êú›«Ÿ£ÊRÇšÕš]zDÈò“3¿“ÌSÛµ�HòÆm ^ž\Ôb=•?Û±Ž5«±·¶ {kµïÏ+-\HÂ5Irmv8ÐÚÖVðûǼ\whp¯¼ü2Îeê�Þ0~xä+‚ C’ù4>yo~н[ªÐ¸©–²•šýàå N]üÌp�ÕÁKdïÜ…·Æ&w®Qû›«ŸãØØÄŒ[¯êÏ9šÊÊFÕçÌNÝÅàå ŽývbI…#2xøä~sõs´=ºŽ5«sÊû4­é ïÜï­­”Óoô�Pìâýpçz\ìÓé2ì­­BUyYN€»ø/eà}ÄŽ¶ºé‘!ú�…Î\½�‰�îà±u£®rzç¢ó©,FSYÄ.¦5#Zk!݉ì¼õëäô»ëjd§~�ñÉÛòf@BÄ�d±9|gÿ‹|@ˆ�1$1 I¾õ­o-©‹Ù©»èûäú>¹¦}PHÇV윢ïœ�OÞ6 1^³ HˆˆJ<$ù/¯¼Ê‹ôÀ»9uWîz$Fk…(bÊÏbí4´Ó‹Î:¬«1šÊåc)[‰¯×V@A£‘ˆˆ!ÉR�Obÿ ß446¢÷§ïæ¼§7Ư}ðÂþýx¼¡!g'œ×¿ÿ=f¼óî{š×~xäˆéN/õõõøþoät¤Ï�ŒÀÿÌÓ€¾þ~žyF³SÎÈÙ³xåÐwe9zú.’Éq¼zð`Î5^ÿþ÷ðÂþý†Ó�2™ Þxíûˆôõ™Ö‘£ºG~øCÓÎ~±2™ ö¿ðm t½ŠŠ ¼ðü_`rr»÷ìÁÛGdzîy ïôöÂjµbè×ÍêzêºD=ëë1“ÉàÐÁƒ8qâ¸á¹Ÿ ðÊ«‡r¾‰ôá‡GŽàñ†¼rèö¿ðBÎn9oy ¯½ñZÛ¾‰‘‘³9Ï0½NÊ|¶ ""~X$ZR&²w�¼qŽ5«åB¯£©,VP0=ÍJ¬?’�º›³ÓÐB9sísl½Üìßå@ìb·¦îB°¶|ön©”;úˆ‡ˆˆ?÷–º=O>‰–Ö6D#}Do8  p?ˆ9+ÑmÛêeçøñ†Lf&qî܈|ÍZaE}}½æüm¾ÿ#÷v¸Q/&{nd'OœÀÈÈÏ<ƒ¾h¿éȈCætŒ[t‹ÒF#}ˆôõÁjµ¢¡±Ûêëå5DH³m[=ö<ù¤æ¸À3OËòéG´Dúúp)™Dr|�gžÁñ˜—Ñ êk¶´¶ÁQí�aÕô(Šñœ:Ù½çID#}8yâ2™Œi9¢‘hí-özÖ ëô¨�dRŽ"y¼¡a:LQ-ð›G[‹O¶�ºþN?�sçFðN8ŒÑ×ÿKÃò^ºtIÛ¶Õc÷“{4íöêÁƒ°Z+p蕃P»÷ìÁ¶úz âá!$ÇÇqèàAÓó3 !"â‡E~X¤Þ[¿NâåÆGPU^f¸[ °ø; Å>MñæKhÜT!G¸é½¦Ùчˆøso¡Ý˜œÔL�™‰èì ¯:„¡¡A\J&qôí#سgÕÕrtX­V¼}ô¨<æ�wßÃÐà iðÊ¡C9ÓezÃaÙ17Á12rþ§ŸF&“Á«¯4½2ý¾Íñ‘¾_䌈ôõáñ†¼}ôGšð@]ÆwzÚ€$Ò÷ Y¾ƒ¯ÒCð�ý/âõïïôö"“Éàä‰ãhmûæœÚjhpP^³÷§ïæÔÙ³?Ú|Ó¡Ã;½a¼rè»òûÑHŸ¼W}YàÄñã2¬Ò‰ãÇ‹¾^}ýv¼óî{xûÈ[rý¨ xãµ×�Éd`µZñÚojêö;û_D¤ïxõàAŒŒŒàí#oŽàIŽ�Ãjµ¢¯¿_Ó¦âXØÿ·±m[=Þ>zô~ˆ¶²mFFF048h:]«T¬ä�"¢ü7nÚtÿCJo/>þøcV=²SwÑñ«Þ¹‚ó©,’7nËPä|*‹÷F®àÍÁO}Ë{g¯àÈGI ^Îà|*‹ìÔ]d§îâü½�u:>¸�wÑY"Z¼Ÿ{?ÿùÏqýúõ’¸ÿ‘‘<ûôSÿÑ«¨¨Àëo¼ 2¬¦§kˆÎõko¼9ã:j™LGßžî\ï޳ǰs\_¿¯Ý»îÐà é�Ç4Ç›…¯¿ñfÎÈŠ†ÆF¹Æ†>D£6;†�Ãt°sÿºÉdrÎm%ÖFe3ª“gý~<ÞЀmªÑ8õõÛ±ùÞè�þˆñt q?Û¶Õߟ.so„ÏL×#3Š zÄ´šö¿˜32G´Ó³~?à§÷B&#Ï9�WkÛ7aµZåׯ:”óü©G%“ã%ÿï G�-sb'œ¥dôÞVÃD´´e³Yܼy“Q †ÆF¼°?~xä†ñúkßÇ;÷Fâ´´¶v€ó972";ÄÏú¦ïÛóä“°Z­˜œœÄÐ�ñ(€Ý{f¾öã ¦ζúz¨½}ôGÈd2y;×ó½è¶m÷CˆW¾Œƒ¯ʹ†5’$øxãµïcddÉññœLÅup0—ëå HTA�Y¸$Ê"Fàœ1iß=¦í&B-³pG˜�ðêAÇ€„ˆ(χBõPcðûýø£?ú#V•ÌÏ½ššš’=i´0êl|gÿ‹šµ#Dû•C‡æÔ�žœa Puuµœ&�ý0èÏ<²Á᨞Õ=WTT ¢B;záá!Ù¡WßÇ|PB‘¾éuS¦G¹<‰ÇÏ»Øèž={äz0‘HŸfTÍIÕ"©êõGg}½¼í{o´�£ººàé]fØLeÐO #c HˆˆŠø�ÈõGˆˆˆ?÷h&o=Š=_k–_¿rèЬFQLª¦S|çÛÏtÌ�ÉÉY—[,<:ÑHN?�¡¡AÓi ó©÷Ýwñ�o[.€ªž^䨮Æî={ðì³þœ1ⵓ'N ‰h±8ëî={4íUQQ1ëëÒVÉñqÃi[3=4ÿ�ñC"ñçîÍ£º©(?íí�Õâ—ê)Ku@&“Ñìð"lv8àp8äú%m>ß¼^·¾~;NÄpâøqœÔÚ.C-«Õš·�ô×;yℜRev½|íûáжÕ×/Ùö-5ÜņˆèžË—.ñC"• †# ãÐÁƒrÛÖ1¹Àç¡W"9^Ü.!êÅAÍv§FFÎ}þ¹R�ºØ½gÞ>ú#446æ„ Q®‘‘³9«ù^}ývøD~ùK|uzÍ±ŠžXcääÉ“îïj£^{¤�ë}gÿ‹ˆüò—xaÿþ¼×3"Ö…)dý±¦ -,$DD¸ŽÜºu ÀôV‡Ï?ÿb8Jhdä,Þxmº|V«µàõMhvøÉŒˆŽ„#555¬œÔ¸©Ž5«qä£$²SwY!DD G¾p ¡þѺ¢Žéýé»hhlÔŒ±Z­xí�75ïSoýûÆkßGCcƒìˆ«w=9úöüÓɨ°VàÈÑ£€ö¿ˆH_&''ñú÷¿‡ñxc#êëë148ˆH¤ONaiim›õ–³³QQQ�ÇðáÐNœ8ŽW¾¬éÀGú~�H_Ÿ¬—ÉÉÉœhvɓض­çÎ�àÕƒñáÐápLou|òÄq6<ë7ßY¦¥­ o¼öýûõg2½f.×SO7Úÿ p8x¼¡AîžóÊ¡Cð?ó42™ üÏ<�gÔ××Ãj­ÀÐÐ ¢}}rDÊl¶Š&$DD G¾ Ç~;�cc¦¯ïX¿{·T±f5ª­«±wKú>¹¶¤ïé|:‹c¿�`ãÃ2¥îd‹©5z¯½ù†ÜÉåÐÁƒèëÿ¥|íY¿ïôö"“ÉäŒ"¨¨¨À‰�^=ø2Nž81½àë‰ã9çÖïÇ+‡¾»è÷þúoâÙgžÆ¥d‘¾>ˆ›¼}ô(Þ ÷"éÃÇçÎÍK0óöÑ£y¯[H�´Þ HÔ_Ï÷õ±m[=Î�Ar|Éñq(Šì¿ÿz_?^}ù Î�Á;Sm¬V+^9tˆ£G�-Œ³ÿú¯øûŸýÑEvæê Œ¦n¢ã«.XÊVÂ[cDZßN,éQ$£©,FSY61Y¦Äô—Ùp8Èd2Øìpà…ýûaµV˜î€R_¿G~xTîv"¶—€W}Ûêë‘L&qnddúïããr$ÂtýGr×”s##Èd2÷Fp4bÏž=†£$÷Ê%þ>S¨×ÑÐkhh”�zÍ5ª«qr †Hß/048„drüÞõª5S�ž øá¨ž.ƒúÞ ¹¶aÝß»îÐà ††5ÁR}}=ZÚfM£³{Ïž¼;ÏÌåz½ï¾‹H_.%Ç‘ÉLÊzP?‘_þ'Žǹs#òÜõõõØì¨Fk[›aÙÌÚDM�v™[û3 !"Z¦>ú—Áßÿügòk†#‹ü!}ê./gà­±ª­«<€¨²¬ÂDö΂ß[µu5Æ'o!å´”­œuдXõCD G–"1Õa1Îq‹X£ŽìÌ£êë·5…ÆQ]]PÙ yOCc#ótÄ¿™÷ÌÊ>×ú—åšEΕÉdä2-­m v½ŠŠ ø�Ÿ�ýEÞ{…©®®F `8²ØÖïwä÷ïšþíÆÐï&1xÉx·ÆÍh¸·�ê‘�’¦×¨«´àëµU¨Síœsæê üϱTQ!F¾ëYÊVÂûˆ�›¬¨*¿¿#Mvê.FS7 ºVµu5¾^[‰ºÊ‡äµ·î`ðòdÞ©=–²•Ø[[…­•T[WËëŽOÞÆ©‹ŸáÌÕ†×j­['ï¥qsžªß �=66�اi> D GˆJ‚z}³àŠJ "*FáÈK/½„‡zˆ•³È,«VjÂu¨çóŒ(©Z]¦ =Œ4l²¢qSîPÔë×`Çú5xoäŠiSèõ,e+±—C†ú× ¹VÃ&+v¬_“³£OUù*ì­­BUyÞ;{%ç¸jëj<·c£&”×­«´ ®Ò‚ÁËô}rMS¿âuQ"¯UqW¢e…á‘–˜–Lï($v×y¶€ÑTøIˆˆJÑ¥ÃR¶R^ÌeJŠ™ÆMÈNÝÅ{g¯È‘;Ö¯‘Û ?U¿ÉÉÛsº¶·Æ.ѾÑkš‘êkµÕ­3 #Ê9rg®L—³®ò!´=ºUåehÜT�S?Ó”S3bJͱ± ^Ê ;uU–UØ[[‰ÆMhÜT�‰ì”颹Omß §;eïÜE]¥±‹Ÿñ%Z&Žå:72ÿ3Ok¾·ÙáÀ³þ+‡0 ™7ÿtâ8þÉ`5i"š_ùêWñ _ Ñ\]¥­uë䈉ØÅ…›Îq䣤&X8sõ&>ºƒ¿j˜žNµ·¶ ?>syN÷ƒ—39ÓRÎ\½œžÛ±QŽ&1šò’�º‹Ž_%4£<Î\½�ìÔïåt£ÇÖ=¬¹�½[ªdýéïq"{ï�½‚‰ìön©ÂÞ-Uüݤáú"–²•øoCåñÇÆø|šù,�æÏxZtååìüã?†Åba8B4OôëuX­V¼}ôhÞÅY‰ Ñ’óÏ¿úÕ¬’þ×ÿÂû¿ì—_3YX�›­Øj2ýE?MeâÖÔ‚mŸ;x9c8:d|ò6/gи©�­xN ›ŠiBúi.ê ãÈGI\¿5eºêàåŒáõGSYd§îÊ)1"¼;ÿÓá’Ù˜ccð>b¿·NI¥á4�ß\ý|AFï,G©T 'þñYô…xâßÿû¢ÞÏp„(?±“жmõhhld8B HæÀãñàƒ>ÀÄÄ+ƒh úûŸý>úõ¯å× G^Uù*ÓÀ@-v1½ Ûû]žÌûš˜âSWù�áÈŽBŒOÞ†cÍjÔUZðW 5º<‰3×>ׄ!3íÎó›«Ÿ›¾–œ¼�6©×;9Ÿº•÷Üâøµ&íÁp$¿uëÖ±è wóæÍ¢ÞÏp„hfùv"b@2555xýõ×YD èÏÿüÏguœ>q»Ý GØ„Éh œOg󎨘/×oMô>Çš/áÌÕÙ]£ï“kr�ÕjëjT[W£íÑu˜¸ug®|ŽÑt6o2U–ûaÇ×k+ÑTcËóÞéñƒEd yãßøÀæ±víZ|÷»ßE<geТúøã�1::Zôq GˆˆæŽ -;úpä+_ù \�|Q ^ʘ. º˜ò03�ê(¸3roý�¶G×iv¢©*_ï#vx±câÖô}r}Ö£TôªVßÿ±]m|èéwȹ_þßó��AMM ·§/D± Ã"¢ùÁ€„ˆ–†#4u°0×Qb§œ÷pu•<¶þaÔUZàX3}�ªòUxnÇFüäÌïæ-$~ræw 9ˆˆáÑòYW-jûØú‡ó¾÷©íðÜŽ�ð>bçƒE´Lö1!"š_ HˆhY|@d8²<ˆµCÌŽjëjÓm„Õ¼56Ã�¥Ê²Jî`3x93§r6n®@]¥E¸èU–OÔLÞ˜ŸcÆ'oãü½PÇ[c×,ÚªVWiA㦠ìX¿Uå,J´\ö1!"š_ HˆhY}@üÚ×¾Æpä&FtXÊVâ©í4!ÇcëÆþ]Ž‚ÎSU¾ ûw94Bµu5^ܵyúÙ™º‹¾O®Í©¬bÊL㦠ìÝR¥)«¥l%ön©’#GÎ\™¿ÝlÞ¹"ÿþrC v¬_£y½qSžÛ±IÞgìâg|°ˆ–ùÏ>†#DDóƒ¿V""~@¤%#v1 ï#vXÊV¢qS7U ;uW†Ù©»è½†¶ºuyÏ3x9ƒÆMèx‰‰[Ó£RªÊWÉs¼wöŠfºÊlô}r ÕÖÕp¬Y�½µUØ[[…ñÉÛÈNÝÕL»IÞ¸�ØÅô¼ÕÑDöÞ¹‚¶ºu°”­Äs;6˜—ª­«5uuä£ä‚o©LDüÙGD´\0 !"~@¤¥Ó¶SwñæÐEì­­”SaD‡ÿ7W?Ç/F¯amSFÞ;{Éÿ†½µU2QŸc>BƒìÔ]¼õë$¼5vê¨×™¸5µ`Û^Ê`4•Å7ëÖɵHÔ¡Ìàå Ž�¥Žñg� ñ"ÍÉh*‹OžŸ·óMdïཱིWä�ìÔ]ŒOÞÖ¼nt=}9bŸ¦û4�*Ë*¬-/›qAÖ·>J~ÿØØ„iÈ‘�º+_×ßW—y6õeVu=üøÌôÿâÚù®»mEDüÙGD´œ0 !¢F:•Â;á~@,•ÁÔÝyÙÆv"{gQFR,Öu–Úµ‰h�ÿ-d8BD´h�Ñã­Î¿Á­[·ø‘ˆˆJB6{“áÑ"b@BD ŽX,|ë[ßâD""ZÖþùW¿Ò|Íp„ˆha1 !¢ŠÅbÁK/½„ššV• †#DD o%«€ˆ Gˆˆ¨1!"Z HˆhI«ªªÀp„ˆˆJÃ"¢ÅÃ)6D´¤ýå_þ%>øà<ñÄX»v-+„ˆˆ–µ'žxï¿ÿ>†#DD‹� -ik×®Å7¾ñ V•ÌϽ×^{ 7oÞä¨I"¢EÆ€„ˆˆˆˆh Y»v-GM}¸ •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •¼2VÑüø}âC¹6Ί z@ܽz‘•@DDD"¢ùêl%þwY DDDDD$N±!"šƒššVÑ2°víZVQ‰ã"¢9øÆ7¾�uëÖáæÍ›¬ ¢ÔC=�ÇÊ ""*q HˆˆæØ±Ú½{7+‚ˆˆˆˆèÇ)6DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%¯ŒU@DDDDD…º}û6Æ/^dEѲÀ„ˆˆˆˆˆ víêUü�Ÿý+‚ˆ–N±!""""¢¼zè!V=p,KQïç"""""Ê«¦¦_ùÊWpíÚ5V=0vîÜYÔûW(Š¢°Úˆˆˆˆˆˆˆ¨”qŠ •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%� •<$DDDDDDDTò�QÉc@BDDDDDDD%¯ŒU@DDDDDT˜D"� .ÀétÂår}!eH§Ó8}ú4 ©©‰�²Äë“íõàà"""""¢ƒAx½^D£QÍ÷‰š››¥ ±X ^¯¡PhÙ×÷áÇ‹Åæõ|ú¶‹Çãðz½hoo_òç§…Å$DDDDDD#<�ü^4Ekkë¢�ˆÇã9eXŽjkk‘H$066¶ çÌ\ës¡ÏO o…¢( «�ˆˆˆˆˆhv‚Á º»» …ÐÑÑÁ ™±X ÍÍͰÙlH§Ós>_<ÇÎ�;çí|‹}~ZœbCDDDDD4ÇÎ1À Q§^¯÷�h#>˧ØÑ¢I$èíí„B!Äãqtww#‘HÀn·£½½]vЉ>ŒD"!;Ëfënˆó& $ ¸\.x½^øý~Ã÷‡Ãa\¸p¡Pét½½½rí—Ë…P(”³k,éS§ÐÔÔ¯×+ÏqêÔ)@?NŸ> ¿ß¯9V§žjáóù ;ÿétÝÝÝp:�ˆF£èîî–õår¹ÐÛÛ ›Í†`0˜s?ííí°ÛíèííE8Î{?êòuww#�NÃãñÀëõÂçóÉ:e™�¡··W† Fm�ÇÑßß/ëê³Ï>ÃáÇe�ëÛ[½>‰XçÃn·›¾oÅŠšóéŸGý3ÕÝÝ�·¼s9?y¬xÆ[ZZà÷û5÷PLyhŽ"""""¢E‰DJSS“ÒÓÓ£Èù300 ôôô(v»=çµöööœsêßëv»åß½^¯’J¥rŽ Øl6exxXq¹\9×±Ûí9Ç555)”žžEQÃãô]¬`0(¿ït:›Í&¿ƒ¦õÓÞÞ®„B!Íy‡‡‡e�555™Þ�Çã)è~R©”ÒÒÒbx===Jgg§@ …Bsnwu[ëëÁãñÈ÷éïY]£sݧú½3�Oý<.DyÍΟJ¥ ÛIÜÃØØØ¬ÊCsÀ„ˆˆˆˆˆ�èPº\.Åf³i:³"€ÇP($;õ¢³n·Û5çßןkllL%~¿_sÌÀÀ€,ƒÝnW|>Ÿì�¦R)ÅétætÊE‘!ÌððpÎõõ`EQ¯×+›��ÍõE7‰Ö�¨‹P($#EQ”öööœÐbxx8ç~DÕ÷ÓÙÙiZ>õýûý~M;èËX¬±±1M€£.·(›>„�}¨#}{§R)Y7úgD}>}ð ê[¼¥R)Ån·+6›MÓn…”·Ðó‹pMÿì‰v÷z½s*1 !""""¢%Ntõ�eugÒ¨S.B õ ÑqÔ‡ꎹ"ÔWj˜u,}>_Îk¢“¯!"ÂýyDGÞívŽ`#ô¿ýõcvOâuuý¨G�°1*£¨k³ò©Gáè;ýÅmçóùr^3a!Þït:sÞ/‚}x¥*Ôu—ï|úQý)ðJÝIDATAê÷…^¢¼êv+öü"È1:*•ÊyΊ-Íi%""""¢E#Öëƒ9 Zе|>ZZZrÖæ ÙJ7 "�NÃï÷.Žér¹ä÷Åêë8�NÃ]gĵÔçÇè·òÛþê×9pà ««Ëp= qâ¼úóµ··Þ“¨?õkbý ³û÷®^ƒD¬k2SùœN§éÚ%³i{±Î‹zÝ���tuuåÜ�Ñý���axxxÆ5QÔe瀪¨O£õ`ò•·§§gVçkÌ0l+»ÝŽP(„���Y—‡f�‹´Ñ¢P‡êFõá€QçרÚßßoz.½X,&;©â:fljN­~¡U£N°Q¹Ä¢›N§Ótu(‘N§a·Û‘H$d8cT6u¢.›ú~ŒÂñº(c8F:�Î[>a>veñz½p»Ý8}ú4Z[[ár¹ÐÑÑ!#Õ—a¦l\.N�:…x<.ë+�NËú±Ùlšz˜©íl6›¦>‹-o1çG¾º×'Å–‡f�#HˆˆˆˆˆhQ’¦¦¦œŽ|:�ÖìVcˆNh:�F:�†Íf+¨¯>§(‡~”Š>Ä1A¢>�:°Ð¯ƒ/ʯKÄ5Ün·á¨ ³‘ 3Ý�~DL1囯mkc±B¡l6‰�jkksFD¹�ŽºLûöíCee%¼^/‚Á :::ÐÑÑ�H$‚T*exœÙ(Ÿ|#?fSÞBίޭf¡ê��Ñ�ä @ÜnwA£ òunÕj1DœÓl†¾Œú©4ù¦¶èË`4¥Åì~�NgÎ÷Ì:Ïf£Uн~ä+_¾©'³a·ÛÑÑÑ�D"¡éè·¶¶j:ùù‚²ÖÖV„Ãa8�Ntvvb``cccP‰DBÖ‹ú¸|çË÷<ÎGyó…jFÏø|Ô1 !""""¢€QÈ�¯ó¯~ÍlDÃgŸ}fz=õtq\¾õ-Ì:µfS1Ì:ØêõNf*›: ™ij‰ÑH…™‚"£×Eç\=ŠE-‹@¢»»‡6ìèûý~÷×kQ—×ívçÜG,ƒÍfC<G0„×ë•í¡ÃŒêG>³º™¯òÒ6F>ŒÃ‡Ëc‹-1 !""""¢ÀLSf OÔ£ ÔÓRŒ:úétZv,ÕkšÌÔi5 qÌŽ1› ":íb�£{ííí ]k$_€”N§ ËQh@¢ ò•/�NË·Ùhžbˆi0úÐÈn·Ë{W¿VÈZ/Feëªè�Í÷\Õ÷|•×ìüùFäÄb1ttt ³³S¶Q±å!$DDDDD´ÄÍ4Ä,l0ëä{<9=EývÑÉommE"‘€ÛíÖ,zi¶^D¾ÀA”Ýl¤ˆþ~D �Çs¦?Äãq´¶¶B¡�Œp8\p»‹‘ú6R׫:ø×ÖשøúôéÓ9�X4ÕŒ¢P×�ÙùÌžÇBÊ«-RìùE�vwwkîCL—¡ˆ¸‡Ù”GŒB¡YàNÇDDDDD´Ð:;;ŠÏçËymllL ˜uOšššJ$Ñ|xxXçñx”ŽŽ% *v»] ¸Ýn%•JÉ÷§R©¼×P(6›Mó}�Ç“s}õ¹<�âõz•ááaùz(’¯¥££C ò{~¿_s�žžÃïëϧ~]]õ}êëG?Š¢(‘HD @q¹\²ÎÜn·@éììÔ#ÞÓÞÞ^p»µ‘º.l6›¦ÞD[ÛívÅëõ*===ò5Q.—Ë%Ïãõzås%ŽSS_}>³çqxxX±Ùl—·Øó«ïQ܇ú™Õ·±åωQ›ÓÌ�Ñ‚óûý % ™vÖ›ššŒ;-÷:Øccc†pÑIœN§ …rB€¸Ýî¢B³¢³³Sv^�B—H$¢8�NMÙššš4�~}ýèC }G\ýº¸§Ó™÷~ÌêuxxXñûýŠÛíVšššä¹EYÕoucTþ|rÚHÔ³ú¢Lê:S_kllL† êú0 ÙÌΗïy�¯Ï¶¼ùΟJ¥”öööœgÖ¬^‹)�¨£`†f¶âÞÿDDDDDD´x<—Ë5çu3æ›Øéd¾¶Ì]H±X ÍÍͰÙl9SY‰jkk‘J¥f]ljDétzÎu‹Åæm‡�Å(o¾óÛíö‚ë³�ò´´´ ¥¥E³ö† Q‰ˆÅbhmm…ÇãÁÀÀ@Îë�@½½½ðûý9k�8p©Tª¨5HhqÅãqx½^¼Pq�•R'pÅ @gg§f�îînùõØØ˜fqÑ`0ˆX,†X,ÆŽ÷‹Å‡etͲüƒ Qéƒèîî–_ÛívÍtšžžžœé‹5¥…f¯Øé:”‹ Q‰‰Åb‡ÃH$H$p¹\ðx<ƒ†Û0•$DDDDDDDTòV² ˆˆˆˆˆˆˆ¨Ô1 !""""""¢’Ç€„ˆˆˆˆˆˆˆJ"""""""*y Hˆˆˆˆˆˆˆ¨ä1 !""""¢’vøðaTVVbÅŠX±b¼^/+…‰DÍÍÍhnnÎy-�N#�³’ Ä€„ˆˆˆˆˆJV,CGGÒé4œN'l6ìv;+†¨g8‹AQÍ÷{{{Q[[‹t:ÍJ*P«€ˆˆˆˆˆJU8øý~ùw¢‰ËåB(ÒŒ|J§Ó�ÇÃJ*"""""*Y§OŸ´´´°2è�äõzs¦…‰i5N§“#¢ŠÀ€„ˆˆˆˆˆ¾P§N�¸Ýî‚:sñxŸ}öl6[Q¿O§Ó8}ú4œN'\.—¦#™oÝq455Íézf÷bôñZ!×M$¸pá‚ì›]o¦û+¤ Ô×*¤>й�ùz– }6Äû‹-›¨ƒBëZ]gÅ<·ÅÞ�‹Åf|®É€BDDDDD´HŠÏçS"‘ˆb·ÛòOGG‡é±‘HDq¹\š÷Ûív¥««Ëðý¡PH D"¥§§'çZFò^/_ó]ollL¾ÞÞÞ® çœ; )Š¢(ÃÃÊ×ëÕ¼æñx”T*eX'ú÷P\.—‰DrÞ?<<¬Pššš”T*¥ƒÁœúT×A!õoVÅÖßl¥R)¥££#§}].—ὤR)%¶04¬gqÃÃÃJKKKNÛŒ��–ͨ-EÙÂá°é=™Ý�¾ME{Úl6EQ¥§§Çð¾B¡�266&¿6#žÑ¦¦¦’ü÷‰ -Ñóx< Åï÷+¡PHiooÏ ÔÔÚööv¥§§G …BŠÓé4=¦©©I tvv*§Ó©455)MMMJ(’¯»Ýn% iÎ!‚›Í¦´··+‘HD …BŠÍfS(---_Ïï÷k^oooWìv»,‡ßï—÷&B#§Ó)Ë$®ÙÙÙ©¹žº3ìóùäûõçSekooW<�ât:•öövM}Øívû8§Q›é˦^ŒÚ«½½}ÞÂñ,Ùl6Yê{Q‡ê÷«ë¸½½]Ö³×ë͹†8—¨3qœ¸—Ë•S6uH&ÚZß>úzK¥R2P1ºÒèŸh ÈÿÇÔm%Žß7 �R©”,¯YHÆ€„ˆˆˆˆˆhžø|>ÙIëéé1ì¼ë;è‘HDvÕ�CÑ©[ýoñÕ£ôQuYôáŠúzúsª¯§DçÒìzêßèë�u»Ý¦‘èðú|>Ãά¾ÕÇèG¨;ç~¿?§£lÔ £~Œê_tÐÕm6Ûöš q?n·;ç^DûªÃñ~u] êês‰û7:N}?êv˜©}ij®o:廸©ïGýÌä)"‚£D\[}þRÃm~‰ˆˆˆˆhш5ÚÛÛå.B0”kbˆµAÒé4öíÛˆF£9ë0Øívyžh4ªY÷BðûýòÜjbÝ ýî�çÓ¯/¡¾žXçA\Ol§jt=uy:;;s…ktø|>tttÖ�úÞ‰ÚÛÛá÷ûsê1ßÚâž�N'ºººL×Q_ÔGGGNý‹k»Ýîœ÷wuu¶—¨u{ÍF"‘@oo¯<—þ^�l6›æ{.— >Ÿ]]]9çS·u"‘ÈYÏÃf³åìt¤~ÔmŸáýx½Þœû×4*¯ÑõÍê^„‚Á`°¨~�Í‚è¸555™vÂÄoÄ1ÊàÂ… X±bEÑ×òz½†�Z³ß²²«M¾óµ´´ä}Ýè¼êðĨ^D�è;¼½½½ˆF£H$šÎ¹šúõˆ£m�Õí£¯³zÔ›m{͆ŠÙ©EŒ”ˆÇãšQ?júgB'Ån‹ÅÐÛÛ+¯eD]öbwžï×oå›ï<^¯‡Ö< ±X Ñh6›Í4¸d@BDDDDD´ÉL[êªCñu!ÛϪÃu`‘¯s©ÄoÖóm©j4¢c¦ëå{=_‡V=êF]¦}ûöÉN§MMMòx¯×‹ÖÖV¤ÓiÑ>Ÿ/o ��¯ý i¯b¶­5 ;ôí�O8–Ó͹.—Kþ‰F£èïïÏ™ÊdTÿFåP·_¾öñx<Ø·o_Nûˆ ¦Ðz1k³@M]WbJ>|òN¹b@BDDDDD4�Ô¿µ6ëÀŠ¥è܉NcWWWQ#Äqfǘ$êifŒF.Ìt½|�_³²¨_S�ê‡Ã‡Ãp:�†ë²¨GF�PÈ7õCŒ>°2 DÔ¯‰÷Û^sQÈuÔkËtvvޔӒŒÖóÈZˆçA¼G´�ÍfC4Í)Ÿ:t1*»Y࣯k£öœ)ÐQO�e�Åbp»Ý¦S¢J i%""""¢E1Sø ÜTwÔD0`¶^E8Fss³fm ÑqÌ7ŠÁhqK9‹zúÑ êé:F×S/òY̳º’‚Á á1âuu¨’ïžõí£>§Ñú.ê{mm-V¬X!ÛÇh=µh4šÓ^seôl„Ãa¬X±B¤Ói¸ÝnÃpD= Æh�Ùýˆ5Wl6›|D{ƒAúV�FÏ‹Q]G£QTVVjžŸ| ´êÛ^M¼–N§5£Gˆ -uÓ((¿ÉÖ¯ƒ :«"<ÑŸóÀ�ˆÅbEýÖ?ßoÙE'W¿[‰èPŠ)¡PHvng32è,ùÖ')t4†z�VuyÌ¥ÌBq£âÀ�H§Óš5eÄû�!�ÇãØ·ob±Ø¼,*Ö Ñ�LRwüE#êØl]Ñ®úzÓïT¤¿ŽzqÓbÛG_"¸0ºŽÑýµg!#^Äu[ZZ�H$4ÓJwb'""""¢…ÖÓÓ£PœN§b³Ù”––ellLS::: %‰hŽSl6›@ Êðð°266¦tuu)v»] ´··kŽñûý ¥§§Ç°,‘HD 455å¼6<<,ËÒÑÑ¡¤R)EQ%�*�G ¸Ý'^…B¦eÑŸSe“ßëììT(.—KPEQR©”¦Nô×È{qNŸÏgZÿ]]]òZÁ`P Øl6exxxÆö ‡Ã²l~¿_s�T*eÚþ…nllLó܉çѬ=›ššJKK‹‹Å4m#„B!ÍýªŸ­RÇ€„ˆˆˆˆˆ\{{»ìŠŽ ú�ÍfËhˆN·þ£ÐAt(�:‡ê¢>XQwp�®'Ž�ÔB¯'^Ww–õõbTÖØl¶œ×Ün·aùB¡�áý‰ïéÉBB³úp:�†÷\l{‰Îþl:ë¢þôšššrÚI„@F: Q×*•2¬sŸÏ—sEQŸÏgúüäk‹BïÇìúðc¦ú6û Tq‘V"""""Zpê¡ÿ---F,“ÓcZZZL§]´´´ �#�"�Ãn·Ãår™ãóùàóùL§¸\.„B!Ói�@^¯W^Ofu½|¯{<Ó²Øív„B!ÃkÆãq„ÃaÄãq$ Mí²3Ó={½^y�fõ‡‘H$`·ÛáñxLõS7DùÔínÖÆ~¿VSoºþÿöîð8m¦�ÂðÉ×r’+©¹p@&€+ˆU¨ä �+@®ÀKl*Ðû#ßj¬ˆc6÷5ã™±¬$“:óì³ uo˜n·«$I¼×éz‚äy^ïšïÝ_FÔ<÷ êíp‹¢P ­÷3Ïó�¿�»_Qí|§Þz=mß›Ùl¦(Šêkñíšä–u:�Ú æ+øVUUÅmpÒ�ÿ÷~xyyù'ý'ðµŒF#ApVÍB'“‰Ò4Õt:ýRAÂÍÍ�Š¢hÝÍç’QAà¤þ´‹ .[š¦;»�‹c[3VÍm} G�8©×ì¬�Ëd­Õjµª—­ð½=Íu¤i*k­ò<—;mH|ȃ&[‰b_õCû9ùJUO®Œ3ŸÏù¿Ø‚$Nª,KYkëf—À¹³ÖÖ �?{‰1¦¡Ž5Ê €þÇ-—Ž€\<pñHÀÅc›_ŸÂÓÓ“$)ŽãO³Ž1F›ÍFa²{pæ¨ pöŒ1J’DI’|ª­‚g³™’$Ñb±à�œ9g¯,KI¿«G>“ççgIR’$ü�3ÇgÏ$Ýn÷S�÷z½æ�|T�8{EQH¢ÀéPAàÃc”e™Â0Ôh4Rš¦Êó\Òïê�étêí1â–ªø*HŠ¢P–e2ÆH’¢(R’$‡ïµÖ*MÓz~cŒÒ4­+T’$Ñt:=·X,´Ùltww§ ”eYÝW$Š"M§Óƒ&¬EQèééI½^¯v^3¿›Ãwî���õ¼î£(ªçê÷ûŸ®Ê8|�ù|^Iª†ÃaÕív+I;?ATëõzgÌz½®�7m·[ïg¸Ÿn·[m·Û�1Ë岞>ŸWAŒ ç펭×ëÖóÞŸ«ßïW’ªŸ?Ö¯­V«7Í¿^¯«(мó.—˪×ëU’ªÕjÅ— x#–Øø0®R"Ë2UU¥——UU¥ív«8Že­Õ�?¼cz½ÞÎë···*ËRqk½^«ªªú3ã8VY–šÍfÞÏz~~Öd2Ñt:Õv»UUUZ.—’¤<Ïe­=E‘nnn†a=ßv»U†²ÖìTã«zqK…Üüwwwõü«ÕÊ;¿1F7772Æì¼½^+ C�ÇcšÁÿ €óôô$I ÃPEQÔËR‚ ¨—ÚEá (öƒ†¢(ÔétTÅα(Šê`Ä�Ý(ʲTžçšL&õr–Á`p0gó߯ ‡Cåy^ÏA=n?ÔpK~š¡…û,7ÿl6«ç÷½O’Æã±¬µ‡zxx¨ßßívë0ÅZûévøÎ  €ãü›úNE Ãð ð$I’¨ª*•eÙÚ¯ÃÇUZL§Óƒj‹æ_X†áAEJóx³IÛ¶Ä. ‡G«=ܵ6ƒ ‡‡‡ƒ÷EQTÏAõð>4ið!\õF†;ÕMWWWÚl6;¯¹PÁ¸Æ¥û�Šo×cL‚ŒF£ƒÏj!¾×G£Ñ«È6›®úæŸL&Gï�›Ç-Û Þ¹%íT”x;´½gÿ÷N§sP¡q{{[/cijö*ñõÿˆãø`Ç™æñýós�/Ôi†¾¹|¡I†­»ñì�q××(IÒ¯_¿Æø{$>„o)JS3ìh.1ÙøwMK]_W]ÑÜJ÷êêê`ÜŸš¶^'Òï€Æ@´5�=VUÒvøæwáL[õˆ1Æ;Àß# ð!ÜÃ~×c£ßï×�€/Ô˜Ífu8²¿sŒ¤ºÙk†Þ¾ m‰;¿¿ UÚœ×V•4ùv¢éõzGï›»þý€ÀߣI+€“³ÖÖ"¾ªÖZeY&i·?‡¯÷;LSš¦c$ÒüLw^¾­m¡Æ±ª“×T•øîOsþý^$ûs»k¥A+ð~$N®Ù[$˲�ß­µõ’™~¿ÿÇ€ÂU…¸ ¢ù9ãñØ;¦­—Éþñý]g|UM¾²¾ª“²,½U%šß…EY–í„$EQÔ÷¬í3¥ßaÑýýýÁ½r‹Å»Ž_ Klœœ{À‡²Öêû÷ïõN2yžËZ«8ŽBép©Ìd2Q–eʲL›ÍFI’ÈZ«<ϵÝn½ËR^[ Ò 5¬µGÇk »?¦­WÉþµîÏ“$‰†Ã¡²,Óx<Öýý½¬µõ#WuÓv].`™ÏçÞããñøUÇþüI• ¾<*Hœ\34X,ê÷ûZ.—Z,ªªJwww*Šb§i[8Ñívµ\.†¡Š¢Ðl6Ó|>W¯×“1¦~�oV©´íPs, hî:ãk’Úv~Çz™´Çú“, -—Kõz=u:õz=-—˺Ùk[UL³êãØ®9mÇ›÷�°¸ߪªª¸ NéúúZÆ­V«ƒ¥'ïyøv½;.ñ~2™(MÓÖfµþ$NÊÓÚXõ½ÁF_6™Ífº¾¾ÞiZÛôøøè½§Þ†€ÀIµ5 ÅqQÉãmj{{{+cŒâ8®{¹x'õ§þð�FŠã¸nj{uu¥oß¾éêêJyž+ C–Öÿ»Ø8)k­z½^ÝT¯W–¥‹E½Ó�1FQi0h4y›Çx𴀋ÇpñHÀÅ# �€\<pñþ·MÎb¬*IEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2513566 ceilometer-27.0.0.0rc1/doc/source/contributor/6-storagemodel.png0000664000175000017500000014720115250267452023310 0ustar00zuulzuul‰PNG  IHDR$Dβõi‡zTXtRaw profile type exifxÚUŽÛ €0Eÿ™ÂxÊ8ÆhâŽ/XMãù€››æPØ¯ó€¥ dÐæÝ  ^3t"1R휃w eâYƒðÝQçC}û�&Öípu·f›mœvÞ…¤ÕLQY±¾SÂú(óì¿WÅŸnA¹,"jD—@ iTXtXML:com.adobe.xmp ·"¢äsBIT|dˆ IDATxÚìÝw|Tu¾ÿñ7)3“IÏ„„tJ¡ƒ ˆPQ îZײ°ºö½«ëº÷®îªWeu®¸å*kY]•âª4Å‚„PÔ$ %…�^'™ôß“ ÒH†„¼ž�Ç>–Ìœsæ|?ç€9ïù–~õõõõp!7J\�@¸�p9 àrÀå$€ËyP wûú믵jÕ*Ùl6Š}”ÉdÒ‚ 4~üxŠè5è!ôr‰‰‰„ÐÇÙl6mÚ´‰BzzHçÏÊJåçSèc ,U�ÐëHçÏÊJ…e� ÐÇ”úøHz%†l—#�.G \Ž@¸�p9 àrÀå$€ËH—#�.G \Ž@¸�p9 àrÀå$€ËH—#�.G \Ž@¸�p9 àrÀå$€ËH—#�.G \Ž@¸�p9 àrÀå<ÎÕ†ååå)??_éééŽÿo*==]6›M’4tèP§÷,‹‚ƒƒ)‹Å¢èèhîºÐ9HX­V¥¦¦*55U)))ÊÌÌìÐþlõgI R\\œ† ¦¸¸8s÷ÐI½6�ÈËËÓæÍ›Û@Œ&…„9~6š¼< ÂñsiQ¡JŠò?—ª¬¤ÈéÚ¾}»¶oß.éd@1nÜ8�?ž; €èU�D^^ž’’’”˜˜xÚb@Ô E Œ•¯ |ýƒ9hh§?/7+C•¶ OûQy'2••vXU•öaMŠ   �7N“&MbxíÐ+‰ääd­_¿¾Å¡�Dxô�3 ZÒ?,R’œŽ››•¡¼ìL?vXGS¨ªÒ¦‚‚mÙ²E[¶lQPP�æÌ™£É“'swp=:�HLLTbbb³ ¦ac'hØè d2{»ôœú‡EªX¤†�»P’t8y¿’÷~£c¿—dï9±|ùr­[·N“'OÖ´iÓäííÍ�@=2�سg�V­Z¥‚‚Çk£I£'NUü˜ ò ´ô˜s?ZƒãGËVnÕ‘ÔÚõå§*+)RAA�Ö¯_¯M›6iÆŒ4Ñ£‰´´4­ZµÊ©G„�_€Æ\xÉYé Ñ&³·†�»PÃÇ]¨Œ#µóËOt"ýˆl6›Ö¯_¯ÄÄD†rРGV«U«V­r¬`!Ùƒˆ .¹Ò14¢7‰4T‘ƒ†:�C95wî\&¿ôig=�HNNÖ’%Kd³ÙW¯hš1fÂÔÝ#¢=ƒ‰ÃÉûõõ§¨¬¤HÔ¢E‹4{ölÍ™3‡;Ð'�Õ@båʕڲe‹ã瘡#tñ•?éQsDt…Áñ£=XûvnÕþo¶ªªÒ>Œ#%%E ,Ppp0w" O9+�DZZš–-[¦ÌÌLIö^—Ï™¯Áñ£ÏÙB›ÌÞšxéLÅ�™ �W/U~N–<¨§žzJ ,Ðøñã¹}†›«?pÏž=zþùçaÄ€¨Aºù¾GÎé0¢)¿@‹æÞù[�?õ I’ÍfÓ?ÿùO­\¹’»Ðg¸´‡Ä×_­7ÞxÃñóä+4öÂKûdá'^:SáÑCôÉ»ËTUiÓ–-[T^^®¹sç²<(àœç²K—.u„£Isn¾»Ï†�" ÕÍ÷="KH˜$iûöízþùçeµZ¹3ç4—K—.u,ééã „[îQä ¡T_ö¹%n¹G1CGH’233 %ç¼n$š†–�0ÝxÇoÔ?,’Ê7a2{ëšy·+nô’%ç¾n $N #n¹G&3ó#œÎôko"”ô ÝHFt¡ /è–@bÓ¦M„gàÔPbÉ’%pNéò@"99Y«W¯–Dq&š†ÔŠ+( àœÑ¥�„Õju|›o0štùœù„g`úµ79–ýì³Ï´gÏŠ8'ti ñüóÏËf³I’.Ÿ3ŸÕ4º@Â-÷È`4I’–-[¦´´4Šèõº,�X±b…233%I£.¸XƒãGSÝ.`2{ëªH’l6›–-[Æ$—€^¯K‰ääd}öÙg’¤Qƒ4uæO©lŠ4T“¯H�dŸäróæÍЫuI ±|ùrIöy#®¾q!Uíc/¼T¢I’6lØÀÐ @¯vÆ�ÄÚµkUPP Iºà’+™Ä²MO¸Éi> z«3 $òòò´aÃIö¡c/¼”Šv#¿@‹FOœ*É>tcÓ¦MÐ+�Q Ñô[úé 7QM˜xéLÇR ëÖ­c‚K@¯Ôé@"99Y”$�?õ ùZ¨¦‹\>g¾$ûªLpÙýöìÙ£»îºK+W®¤ÐE:H¬_¿^’}"Ë1¦RI꩸ÑH’6oÞL/‰nÖ84fË–-Zºt)€.Щ@¢iïˆÑ§2‘åY0á’+%ÑKÂÕ¶oßN(] S�½#Î>¿@ ½$ÎB 8s$èÑsÐKµû‡+°¸$B 8S$¶mÛ&‰Þ=�_ E1CG8]tƒÁK×Ìý¡t�V«UIII’¤�q£èÑÄ��(I*((О={(H73yyJ@èP ‘””$›Í&I3‘Þ=ÁàøÑ2MŽëƒîG(g®C�Dã°¿õ‹¤z=Dãä–Û·ogrK!”€3Óî@"//Ï1™å˜ /¡r=HüØ Ž?3—„ëJ@çµ;�h:`PÜ(*׃ô‹”%$L’”’’BA\ˆP:§Ý�Dヮ%$L~�*×ÄE‘$íÛ·�b¸¡tœG{7LMMuzðEÏ2hØ(Øõ•$)99Yñññ>½+ZVQQÑæ6�¡Ä‡«þ¡ÂÜãÚ¾}»$iáÂ…ZЮ@"99Ù±ºFÄÀXªÖEêøsRRR‡‰_ÿú׎kŒÎ!”€ök×��ÆÞ’=˜ªõP¢5»^íÑ4pÂéM^mnÃð hŸvõ�hìÊ? j�LfoªÖCE ŒÕ‰ô#ÊÌÌìô1.¼ì§ ìA1[` i_]è)mkW ‘‘‘!I åAµ' �¢oµQRçç‘졈˜8Šy†% uí²ÑØ�ß/0ˆŠõ`~'¯O{&bD÷bøœ^›�Drr²ãÏ–�p*Öƒ5]Ž5==�‚ô„в6‰üüü“¼ô�èé,!a’$zB h®c�D“oà]i|´¯ÆGûjÉâEmnûÎëKÛ÷E>þ�Í®Î>B pÖf ÑøM{ã’’gÓ{o/Uuuõi߯¯¯×Ê7^=£Ïxå¯Ïöê0#x€}âÑ3Yi݃PNj3�(//ï'êîœlmþhÍi·Ùöåf;|Pîîî�þœ/7Ì]�nC(v½åD== Š1L+—½¬™ 7´¸ÍÊe¯(nø(¥ùQµµ'W™(-.Ò’ÅÒ矮WnÎ ëÒWëÞ‡S`�}Êž�Ûô‹ë¯tì3>ÚW^fo%&Ÿh×þåÖ2M¦èA±ZñÑWZôÈúj˧ª¨°jÇÁ<î44 %X´÷êçæ¦�Ó¦)zêTYâ‡Éè ~ý¤Š‚å%'ëȧ•±m[—}Þ­Ÿ&Izó²ËÛõ:z‡–®×ô%n½åDkjª5㚟(i×v¥|·¯Ùû™iGõÕgŸèŠÙ×Éf;FØlºcîÕzïí¥zðÑEÚ²çˆ~üÏZÿŸwtÇ�3UQaï2~Â$íI+uì·'­T‰É'Ú½¿§§¡!˜(Õ_žüåœ8®{ûýæri�Œ&/îê^JÐS¢÷ñ‹ŽÖœ×ÿ¥‹ÿð¨¢/½DÞ¡¡ò0ån4Ê',L/¿\—?ó'Íxþ/2øùQ°ˆ¿î§ŽrP@ ¡Š ûý_€å¬žhMM�®þÉ\õë×O+ßx¥Ùû«Þ|M’4ëºùN¯¯\ö²R8 [~y¿®˜ýSùúùëŠÙ?ÕÏïy@‡&kusN´wOOIRqQ¡ óó´ä­µš{Û/5ïçwº´NM—fmºd+%ÐaDd¤f¾øò8PÖìíüûßõÁÍ·èí+¯Ò;3¯ÖÇ÷ÿJG·lQ}]�ÂÎ?_3þߟåæqæÑÞ¼ìò>ñ�yP\7õ'5NŽèxÖO6,"J&_¢�>X­’¢BÇë6[…Ö¬|S&_ª°ˆ(§}6´V’4㚟8½~éŒk$I_lú¨ÕÏlïþýúõ“$UWUéæ;î?£y,зC‰¯¿þšÂôPÿáQ}}ub÷n­[¸PÉッÒÌLÕVU©ÆfSî�ÚúäSújÑ"Õ×Öʯáso¤píd‰F¨?èCú`µŠ‹ tíÜ[šmäÇTIRdŒó*!1ƒc%IY™é­~^gö�6œ; íVVR ²âÇÏf³™¢ô@^(K|¼l……úâ‰ÿUu+þݼEÁññ 4X¥-¬xÓÄ Ÿ;W!£GËè離ÒRå~÷½~x÷]e'%5Û¾£ó „�¾â¯¿NÁ#FÈàã£j«Uù))J]»Vé_5¼<½½5Ãz§¥iím?×à+®ÐÈ›n’Ox¸¬ÙÙú~Å úȾÆÎš¥7Þ ŸðpUèІ Úÿæ¿Ï¨�#v“λóÎfíÝúäS:ºeK§jfðñѼõëT|ô¨ÖÝ~‡.¸ï> ¼ü2y˜LzçêkÚ¬á€ñãÃõêßPCkNŽ~üä}¿b¥j«ªÎ¸æÕÑöw¤ í©GÛw¦õö_>ccuèÐ!•6é‘p6M¿æZ=ûLJ´úÍé–_þJýúõÓª7^‘�Ÿ¿.Ÿ9§ÙöÖ2IÒÔ‘-/?7»ÕÏëÌþþAÜYh—¼ìt}¸òª®²I’n»í6�?žÂô@Ñ—^"IJ]»VU%%mn¿ëÅ—Z|}ÈÌ™šôðoÕ¯I/*S` ¢.ž¢¨)“õÍßÿ¡”÷ßïôyŽœ?_çÝ}—ÓkF…Oœ¨ð‰uà­·´çÕלޯ­¬´ÿÁhÒÀË/×”G9ùïYL´&ý÷ïTž—§ÀÁƒuÞ=w;Þó0@ãn¿]•%¥J]³¦ÛÚØ™ãÕ4´ÉÝhÔÈyóÝOÛýyÃo¸AÜw¯ÔÐóM’|#"4î¿PÔ”)úäWÿåü@߉šwDgÚßÑ6tõ=u&õÍ”å÷ˆöò2ëŠY?Ñ+ßÔןo”¯¯Ÿ’ìÕ 7ÿB¦&t4ûøª´¸H‰É'äeöîðç�éþ®–ŸsÜñçøøxîð^FL™2…ÂôPýGŽ”$eîØÑécøF„ëÂß<(I:ðÖ[:ôáG*ÏÍ•—Å¢�Ó¦iìºà¾û”µk—JÒÓ;|üÀ!C4þ—wHõõ:ðÎ ýøñG²fçÈl±(æòË5îö_hÔÏ~¦Œ¯•ûý÷ŽýêjjìÿA0{iìí¿ÐöçëÈÆOeô÷×ÄT䤋4ú¶[奯ž~Zi[¿’)àä{ƒ¯ºÒHt¦�ß½ý޾{û�{‚t¶fŽ6yy)îÚkµí¹¿èèæÍNè-ñ8Pçßs·êjkµóÿ§£Ÿ}¦ºªJÅ Ó¤ß>$˰a}ë­Jú׿ΨæÝyÏt´ ­ÕÿŒï©Öô-n½ñ¤çÜhšñéº÷´þ½’¤„†kHÒà¡ö‡òœÇ;õYgº¿«U6Ya„è:^ûľ%ii�>Fܵ?‘»Á ¤×—jÏ«¯9æŸ(ËÊÒ�·ÞÒ¾åËåæá®¡³gwîø sÔÏÝ]‡>üP{^yE%i骭¬Téñã:ðÖ[Jù`�Ô¯Ÿ†\su‹û}}uì³ÏupÝ:ÕØ*eÍÎÑ·K–H’BF�VêktdÓfÕV:¿çÓmmìôñêë%I¦€¥õ•mØ ›M5­ÿ9ìÚõsw×�·ßVêš5ª*)Q�­R9ûöéË'ŸR�ͦ�Ñ£»¬æÝÑþ޶¡[î©NÖHôxçMœ¬¨˜ÁÚ™ø…¾þìS :L£ÇOhqÛéW'H:99e£ïöíÖÌ ãõìrzÝ£aFüÚÚÚNíFœ›<z`UWØ:}Œ°óìÃqúi‹ïÙ´Y’:nl§Žß¿á!óà† -ãFûv ½=Zrx£ó¹•ee�Üóæß3x›»­�]q¼£§œwkBÇŽ“$ûüófï:¤wf^­Ox KkÞÕíïhºûžêHý@û»­ 'Ø«ª´õ¨Ÿ}ÃÏ´äù§%I<òÔi·›{ë/õáû+õêßþ¬ÁCã5é’é:”ú½þøà�Ê9q¼Y�5p°ŽJUÒÎm>z\‡÷#ÎMÕÖ2ýýeðñVeqI§Žá=`€$éúÕ«ZÝÎ'<¼SÇ÷ •$k¹GqCïï��Ó£<'Çéçºêê“ïåžæ½&Ýð»º�]q¼âŒŒö×0ÌþyeÇ�»¬æ]Ýþ޶¡»Ûבú€¾¥ÍQQöe4 r²zԉϹágrss“»»»f]wÓi·3šLzu凚¿ð.-~ê]<2\wÍŸ-Kpˆ¿öŽf]7ßiû‡Ÿø ‹ŒÖÝ?›£Ÿ\:¾ÃûŸm™GI²OF Âtk¶ýa¾~úõïŸÒ¯ÿT›Ÿ5é’éú0ñ»NíßÒyºZcO–Ž$Œ@×Êýþ;Å Uô%Sub÷îN£º¢B­œ=GUee]~ŽÕ6|¼åáå¥j«µùíÙËqÝ¥«ÛØÝ5kéÛÓÛ[F??ÙŠŠÎzÍ;Óþ޶¡§ßSàÜÕf §@¢¨€Šõp�=Y{¶€0]£qþ„!3gÊ7¢íá!£Fiö¿^Óà+¯t¼Vš™)Iò‹îž¿ŸÖì’¤€�[|ß?Æþºõĉn«SW·±»kvª²†ÚøEG÷ˆšw¦ýmCO¿§@$š.y<íG*Öƒåf�§K A�.þûµÿ€NìÞ-“I—=õ´¼,A§Ý6hèPM}âq"˰aŽ×O|kïY1bî¼÷ Ÿ8Q×¾ù†ÆÝ~{§Î1{ï>IRì¬kZ|?¶a%„ì}û»­N]ÑÆ~îî.«Ùéj8䪫š½8dˆ~öÉÇšùâÿ¹¬æ�iGÛÐZý{Â=úp !I���’¤JºdöhM—ülÚ³„è‰þ³*òó0x�æ¼þºFÞ4_~ÑÑr7eôóSððášð«ûuÕ?þ.sp°òSSµçÕWû§®]«›M1—]ª‹ÿð¨ü"#åæá!/K�â®MÐ%�?&¿¨(yz{wêüR׬Q]M­b¯¹Fãy‡|#Âån4Ê/2Rãn¿]±×\£ºšZ¥®]Ûm5:“6ÖØìWb.»Tn2v{ÍNupý:Õ×Õ)öš«5êæ›eô÷—‡É¨Ð±c5õ±?ÊÝhTî�ï\Vóδ¿£mhµþ=àžç.�öld±XTXXH‰®éõ‰îdWÝCß}£ìL®sK†Ž˜(߀ö=„ç&kvŽ>¾ÿWºä‰Çe6LçÝu—λë®·Mÿê+}õô"§‰ ËNœPâ³ÖÅxTƒfÌР3ší—Ÿšª½¯ÿ«SçW|옾]ò’&üêWuóÍuóÍÎÔ×ëÛ—^TñÑ£ÝV£3ic~rŠBÇ�ÕÔ?þQú£ýµ7/»¼[kvª¢ÃG´ç•WuÞÝwiü/ïÐø_ÞѬÆÞzËe5ïL=;Ú†¶ê¶ï)ÐljñãÇëСC*ÈÉ’­Ü*“Ù›Êõ@]±Âơ£�§‘Ÿ�®+~z'aDW–•¥ïº[1—]ª˜Ë.“%>^¦€@õsë§òÜ<åìß§”÷?P~JJ‹ûûüs;¦óæjÀøñò R]M�ŠÓÒtlËgúá½÷œ–Úì¨äÿ¼§¢ÃG4|î�ê?b„<½}TUZ¢Üï¾Ó÷«V+gß¾n¯QgÛ¸}ñóšôÛß*(.Nu5µ*iXR²»kvªïV¬PááÃ~ã ² &O³YÖìûâ xë­f“;vwÍ;Óþ޶¡µú÷„{ œ›úÕ×××·µQZZš-Z$Iºê†?šÊõ0¶r«–.~L’4kÖ,%$$thÿ'Ÿ|R™ “§¡e¡C4û¦_÷¸0â¹çžÓ¡C‡d.)QÜÁC\(ècR‡ÆªÜÏO±±±zøá‡) ×hW‰èèh™L&Ùl6e=D ÑO;ìøs\\\‡÷ì±Ç(büm¡g´Ÿ[{7l|È=zð;ªÖ5×0™LN+£À5# cÚHŒ7N’TV\¨’Â|*×Ã4E�é�3C×î@bذaŽ?'ïÛIåz�ܬ •J:Á5# sÚHk̘1’¤”}»¨\²ï›­’ìÃ5$\‡0:Ï­#7¶‘qä Õë!ޤp\oo–duÂ83 $¦L™"“É$‰^=ÅI;T]i(ž4iqÂ8snÝ¡±—Dêþ]²•[©àYÖ ²º† F@×èp 1}útÇŸ÷íÜJÏ¢Œ#••vX’4yòd Òͬ¥…„ÐE:HDGG+66V’}2EzIœ=»¶~*É>™eӠݣ¬¤€0ºˆ[gvš3gŽ$©ºÒF/‰³¤iïˆéÓ§3™¥ FÀ™ëT O/‰³ŒÞ®c6›&Œ€®áÖÙ›ö’Øùå§TÒ…èáZ“'OVll,at!�Îî¯1cÆhß¾}:°ë+Å�� þa‘T´›ÙÊ­úlýJIôŽp•ñãÇküøñº�Û™ì‘epp0EÐkY­VeddH’RSS%Iéééš={¶¢££[Ü';+KëׯoóØÜÜ”T\Üâ{eÙÙí>Ç]¯½&£±Å÷N;&I²Ùl:xð`³÷ƒ“[¯¿^Õ11òôòj¶MZZšV­Z%IŠŠŠ’Ùl–Åb‘Åb‘——×ië@ ÑDtt´fÍš¥ 6¨ 'K›×¼£é×ÞÄß°.r8y¿ìúJ’ÑjÒ=ѦM›”””¤òòreffžv»‹ª’“Á�­¸X•%%ö ¢Âi[³»»¼ÝÝ�^ 4ä_Y©âSzN45-$¤]çlÍÉ9í{æŠ �òó“$eÛlÍÞÏ­ª’$enݪÄ]»œÞóoè=‘S]í3Z 5${ùI“&iþüù§“�� ””:tH©ûw)_¿JÁ¡,zlåVmY·BÕ•ö´íž{îaU g=|HMMUzzºÒÓÓ�¾Ùÿó#�¨,;[¶¢"•eg«,;[µ =Ì%%êo0Èàî®@OOÜÝàé)ƒ››‚N@œë‚ŒF]æôZYuµÊjjT]W§Âª*UÕÕÉ»¸X?nÚ䨯Ý`�Oh¨L2ùûkõêÕNLjˆˆPTT”ã=5¤è±é8Ð IDATÒ@ÂÛÛ[÷Þ{¯-Z$IZóï%ºö–{%:iÍ¿—8æ�¸ñÆ™ÀY±nÝ:¥§§+%%E¶†'4úzÙ2ùxz¶ø^¼ŸŸâ†8àô|<=5<]ŒÚª*§§«8=]eÕÕÍÞÏÌÌTff¦¶oßîx-""B�=öX�j«[W0::Z·Ýv›$©ºÒ¦5ÿ^¢’Â|îªÚ¼æGqÑEiÆŒÀYñíÎ�Ú»wo³0¢¿Á 8]¤i!!§ #Ð}|<=õ³˜%„‡kjp°Fùù)ÂËKæSæÖèWU¥¢†‰8{ �î8è”)S$Io¼ñ†cå�ko¹G&3à Úcóšw”ºß>éIll,K|èViiiNó?”ž8¡â´4;¦¢cÇäkµªÂÓS�ƒBŒF }v˜EOÕØ³¢i¯Š²êjVU©°ªJÞµµÚûÖ[’ì“jú„†* &FÁ Ã9V®\©¨¨(�;ÖeSxt×�§L™¢üü|ÇÊ�Ã7%ÚFDDDèÞ{ï¥(ºÜž={”””¤¤¤$Ùl6-úÍo”—šª¢cÇ«Z4ºÀb¡`½PK!…$Çp�Ì]»än0Ȧ-_~éxìØ±7nœâââÜmççÑ��OHHP~~¾¶oßî%¦Í™Ïœ-°•[õõƵNaÄC=Ä$–ºL^^ž6oÞ¬ÄÄÄfÃ/>~ã�N¯�Þ«¶ªJ©©2»»«¼¶V’´wï^íÝ»W’} �I“&uËœ†ÝݸÆáMC &ºtf+·:M`I +%''kýúõN+b4ŠðòR¤—aDd4ê'‘‘*¨¬Ôá²2eTT8‰íÛ·kûöí ÒìÙ³S4tW4náÂ…²X,Ú°aƒª+mz÷_/è²Ùs5|Ü…}þÂçfeèãw—©¬¸P’}Έ{ï½—0@—X±b…>ûì3§×ú ìã£H³YÆS&?Dßd4*ÈhÔ’ *+•RZªŒòrU×׫  À±„kWñpUÃd±XôÆoH’>_¿JùÙYºøªŸôÙ‹}8y¿¶¬[¡êJ{W©‹.ºˆ ,t©±úL’g¿~Š4›5ÚߟÕ0Ц £Q“ŒFUÖÖ*£¼\‡ËÊT½c‡’ËÊ3uª¼Îø3<\Ù )S¦Èb±hÉ’%²ÙlÚ¿s«2�êsóJœ:_„$Ýxã�,í  Ë䦤èÇ�UYR¢©ÁÁ 1™è �3º»kˆ¯¯†øúJuuÊÞ¿_Ùû÷+æâ‹1a‚<½¼:}l7W7&>>^úÓŸ+IŽy%öîø¢O\ÌŒ#µú_/8“ɤG}”0@—¨(*RÒ›oêûÿüDZZF”·7aºÔ±¯¾ÒŽ_TnJŠ$û„©åq6NÜÛÛ[?ü°Ö®]ë˜W"qãZI9 isæË/ðÜ[RÆVnÕ®­µçVÇkÌ +äåå)88X¹))JY·®ËÇú-©­ª²_è?;vt¸ç¿ÇÙ<ù„„ÅÅÅiÕªUÊÌÌTVÚa½õâŸ4zÂT]0õ ™ÌçÆƒúÞ_hç—Ÿ:æŠ0™Lš3g½"œ±´´4=ÿü󣑧,å ¸ÂG»ì#V¯^-Ií~Öõ8Û'¯Ç{ÌÑ[B’öïܪä};5öÂK4ú‚‹{m0‘¼÷íÝñ¥c9OI3fŒ,X@¯]bÙ²e²ÙlÚ›’"KHˆœÁ˜~ 3f„†êËÜ\UWkõêÕŠŒŒT|||›ûyô”$$$hòäÉZ¹r¥öíÛ§êJ›v}ù©öîø²×É{¿ÑÎ/?u,å)I���š7ožÆ�ÏÝ  K¬]»V™™™’¤ó #pVøxzê’þýõQV–ªëëµ|ùr=óÌ3mîçÑ“¬ûî»OÉÉÉZ·n�:äL Š¥ac.Pä ¡=î”æ+eÿ.%ïÝÙ,ˆ˜3gަL™Â] ôp•ƒª  ôB^ååò¨«£úœmÛ¶I’ú Š÷ó£ 8k|<=u^` v¨  @{öìió y�žØ�øøxÅÇÇ7 &R÷ïRêþ]òñÔ˜‰S5(nÔY�ÓVnÕуß)yïNe¥vz� èùʽ¼T "?ÙFôjž••yà;  OIKKSAA�ýŠ0=À__í.,Tu}½RRRzg Ѩ1˜ÈËËÓºuë”””$›Í¦²âB%n\«Ä�kåã¨Aq£3Dáу»}XGnV†ŽüN‡“÷;Í Ñh̘1š>}z»ÆËp½77å…†*ߤj£‘‚çþ>è‹ÊËËöts£ è<=•[U¥ôôô6·õè ÖÂ… eµZ•””¤¤¤$íÛ·O’TV\¨ý;·:–Ó Sph„‚„+84B£IýÃ";ü™%…ù*-.Tiq�òNW^vf³^�"""§3f(88˜;è¡rú÷׉ð0Õy8ÿÓg U`ÔpI’_è@ ô"y‡“”ûc… ‡(¯­m÷¶½©aÞÞÞš2eЦL™Òb8!I9Y*ÈÉRê~ç}=�&‡†·ù¥Å…Ns@œNDD„&Ož¬qãÆB=ýE//¥ÅD; Ëð ‰QÿØñò0H&ß@ŠôR%ÙG)€>«ésHŽÍÆ„–8ëʪ«e=W‰¦š†’}üTjjªRRR”žž®ÂBçP¡ºÒvÚm1™LŠŒŒÔ°a稨(–íz‰ü  eFE:zEÌþŠw¹B‡žGq@¯–——çøóa«Ucù’g×î&ÏásæÌis{�s¥áÑÑÑŠŽŽÖŒ3¯%''K’RSSU^^Þ®1,ÁÁÁ²X,Žÿ>½W~P�Ò tü1æ2E�ŸFaÀ9ç’˜©ßL]íÇÒReTTH’Æ…h×¼ŠçrA À“@ßÓ4Œp÷4jÈ”ŸÊ3‚€sÒ‘/¾Ô€k®VUI Å€Ë%kwQ‘$ÉT^®‘ýû·k?¦bpÎ)ò÷w #F\µ�0œÓªË­úþ½÷e`ùO¸Pem­¾ÌÉq #bSReòôl×þέ ¥ Œ‘t2Œð±„SpΫ*)QÒ²åò`rK¸È—99Ža�a„G]]»÷'�pNI‹‰qL`3ájÂÐçì{ëm•d�P­Á 5Úßðí5Ð•Ü M “$æåw8Œ�Îñ9$ô-Eþþ²úùJ²O`ÉJ ¯ÊضMYG�È®ýÅÅ:\V¦Á>>Šóõ•ÑÝ�áŒtxó;¦áƒŒUU�:�€sFfT¤$ûP�°áQЧy—–ʳ²RÕF£¬µµÚ_\¬ä’Ezyi˜ŸŸ‚ŒFŠ„VUÖÖ*ÇfSTÃÊ“MƒˆF§†ùAAÚ�•¥ê¯¿Ö”)SZ=>�€sB~P�ªþ£9îryšÌôi~eV�<ð�òƒ‚”"›Ù¬êúz)/בòrxxh°��ùøÐkN—•)£¼\òì×OaÊÞ¾Ã)ˆ8íïå– ¥ŸÈRib"�€¾¡8 @’d0û+|Äd ÎyÁÁÁš5k–v½úªŒUÕ§ÝÎRP KA�J|¼Uh Va°E’TTS£ÝEEäãC1û¸ÊÚZeVT(£¼\Ù6›ªëëïU××kÛÆM²tùçHèýÿ€ * ´�Ññô ÁÁÁJHHPæ“Oµk{¿2«üʬ�•¥â€X‚d¨¬RIU•"Î;Oõµµª./§°}HºÕªýEE*ª©iöž[M­‚òó”_ sÃJ]�@@¯WÖ$ÕŠNAZa¬ªRHNŽBrrTãæ¦Üº:åî? Iò�‰Qè˜Ñòéß_Uååzÿðayöë§P//…� 1™Þq±yz:…n5µò/*’Q‘Š‹»ýó $ôze¾ö•5Ü=� LAÚû@xÊ2�ÅÇŽ9æ ¨4d=J’TTZª”ÒRI’·»» …� 4êåE!{˜‚ÊJUW«°ªJƒ¼½˜üü¤úz•df*ã›�*³Ùd2X>¥¥ÝÚâ´÷— @oWið”$™P €.â^S£ÐãÇUêã£r??ÇëÖÚZY+*”Ñððàé©kÂÃ)ØÙø=¸¶VEUU*¬ª’µ¶V…UUÊ©¬tÚÆ×ËKY'”óÝwª*)qzÏ()þ‡ä³vþz½Æÿ@š|)@W=,ÖÕ),ë„Â~.ññ–Õ×W^fU˜½+œùÔÖª¦ºZæ  ¹¹{ÈVTètœo dps“g¿~ 4äíá!OO |†öêÀ)CKŽ»[599=óã28W¼(@7iœ³Q�››ÊÍöá©;w9ÿ^æç§�‘#åáêq*owwy{xÈÛÝ]>žžÐ7—Ënèib­­•µa>‡l›M’t^` c¸…›Á ƒÙ[õªWme¥¼óòœŽãVS+S¹UæŠ y•WÈ«¢ÂåC0:Š@Ðñ‡Éº:§€¢©ª’elÛ¦77™†ÅÉf67ÛÆZ[+km­$ÉÓÍMçGG7RÐh_a¡r*+h0Èàææx=¤áa½Q€ÁpV&Ýl:ÑTksklÌÊRî)Û·¤07OîîÊØ¶­ùg °‡åÍæé÷� ÷INNÖ /¼ �ž§¤œ68«œuuŽ9 * UÞ m=¼†´«ªêÓ»Y t~x¸&N™Òæ¶€-2#³Å×˽¼Tã~r‡¹¼¢Åá ’½‡†_d¤Óö-)>@ŸÕWÿ ¬swçâzŒŒÈ�HMÕá+4þüV·%�Î6ooý8,ŽBè³ø7€³¯ÜËKåÖ2ù¤§·¹-C6€^Îl6S€‚ƒƒ)ÐÿÞÏš5K¡Ç�ËXUMAÐëÐCèå,X ôv¤�ç²^x�€|ðÁ>Ýþøøxn � VBB‚2Ÿ|Šb W"�z9ooo~ Èèu²\Ž@¸�À¥Šò²µpÊÝ5m�êjkÏé¶–æiá”úõœÑgåó sOhᔺÿêáÜx Ça hÇô§«^Ñß~¥ü*/-–·_€b†�Õ´ëhÜ”+šíSVR¨Ô¤í:ï’«)à)ަì“$EÅŽ�›»{—·'ÔüÔsÈ<œ,IŠ:꬜ϱ†Z6†À9---Íþïmt4ÅzzH@+2~üA¼íríܲVs<¨ÿ·úýã£dÝùØ‹:‘vHûÝ­Ú¸êU§}êjkõì}?UÊžm°Å‡ä½’¤˜a]×k 'Ô¼¥s~þÅZúõ =´ø�³rN�áÏÀx ç®´´4-Z´H‹-ÒÊ•+eµZûLÛ­V«RRRTêã£7í@ ç”·þú¨ÊŠ ô‹ß¿ ó¦Î”Éì-/o�œx©î|üEöÓñc�öÙòþ2eNÖ áã(`kÉÃÆvÙ1{BÍ{âu?Úþ ŒË�àœU^^~òßâ-[ôÈ#�(11±O´===]‹/Ö�ÃâTnöâf@�™‘©™Cb5oÞ¼6·eÈ´âÇ»%IƒGœ×ì½ØQhñ{??±ð KÝïøùåÿ½W«^zJ‹?Ø£¼¬tmø÷?t`Çg*Ì=!£—YCF^ 9 ÐÐÑûÔ××ëž+†ÈËÛWϮئ·ÿöG}ûùYDê—mÒ¾m›µqÕ«:üÃUUÚ£‹¯ž§+æÝ)O§óÛ·m“Ö,]¬ôƒßËËÇW]q�n¼÷úÓÝstä‡$=»r›B#Ù™++чÿþ?íú|½ò³3e4™5tÌ;í y^›uêÈyK±×((4Bÿ|âíß¾Euµµš<óÍ»ÿqŒ'¡úrÝÛúêÃÊ:vHU¶ ‡EiÊÕsuõÍ÷©_¿~§­ùóïïî²:?šªµKë‡Ý_«¢¬T–‘ºøšyšyÓ=r÷ðhõº?xíXåeëÏ«v($"F’Ú}/HÒ½W•úõÓ3o¥7Ÿÿ½¾ßõ¥ &/M¿þšóóÚ¼.ÇZÚÓöŒÃÉúã­—)4j°ž]áüK½µ¤H¿½þõssÓ_ÞÝ)³¯»ïŸ¶îoè 6›MË—/Wbb¢,X àà`Џ�¹¢B~~íBE h…—�¯¤“ß4·æ‰¥µøƒ$I’��–~}B‹?Ø£ÃßïÑc?Ÿ¦|«»Ÿø§^ü$Eÿý�ÿ¨ 'S¾ÿ:Ü÷�ãùÙª¬(WHÄ ½úÔ¯4jâ¥zaM’~ÿÒmûä?úë÷¨ÒV®Ç^ûX_ÿ�†Ž¹P«^zJï½üŒÓ¹ìül�þú𭲄FèOïlÕ“Ë·¨(ï„ÞüËÿèhò^ùXaDNæ1=¾`º¶®[sï{L_@-~[YÇé™{¯UòžÖ¿eêÈyäª07K£v}¾^×Ü|¿ž{w§&N¿V[Þ[¦uËþêØöÝþIKŸý�¢‡ŽÔ3ï|¥çþ³S¡QƒµzÉÓúàµçZ­yWÕñÐ�]úßÛ¯RMuµ~ÿâZ¼f��¯wÿ¹Hï¿öçVÏ¡(/[EyÙòö t„»2Ua-U`ÿúàõ¿èg<¥'–n’››»Þ{åY¥îÝÑêu)Ì=¡¢¼lùø)8,ªC×*4r�ÜÜÝ•wìbÌ’¤ƒjåÊ•èÁ$ WÜx‡$éoÿýs½÷ʳÊh˜¤ðtŽ$ÛL'¬ª¬ÐKø¥êëëõ›çßÖ�QçËh2+zè(Ýô_Oª¶¦Z¼þÇþ‡~�$¥ÿø½¦\=W¦%ÈÓh’Éì­Ý_~$³�¿n}èY…F’—·�®]ø�$)ñ“wǨª¬Ð¿?"ßÀ`Ýþèß< JþAýõóß=§›ÞW}}½bâìó7ÔÖÔè¥?Ü¡¼¬tÝ·è5�7u¦¼¼}5høxÍ¿ÿ ûù5<üŸN{ÏK:9„ÁhÒõwý^ÑCGÊìã§Ù·ýZ’ôÍæ“¦[Þ[*IºîÎßËÇ?H~�Ášÿ«'döõw ˆN­yWÕ±¦¦Z¯>y¿|üƒt×ã/j@ôùøꆻ•Éì£C¾mõŽž2¡dGï…ôCß;‚…îzDA!á ‰ˆÑ¸)W6„»[½.-MhÙÞ¶{Œê£ÚÚå�Hwº·6­~M^>~ºrî/;|ÿ´v] ³‚ƒƒ5kÖ,E�¾Rñ—êüY)|ØÅr÷4)öÂyª¬®§H@Å� hŬ[ÿK’´þ�¿iÝò¿jÝò¿*,f¨.¾fžfÜx»ÓðI:šì~ �¬´ƒÓð�ºsËZKݯøó¦(nìENÇ2ê|ûÃöI­Ö¨½çeoŸ}hä+¯—�_ ãõÀ�0Iö^�Œ^Þª°–êÇ»4ú¢i’¤�ˆ�zñã”VkÞUuÜþé{ÊÉ<ª9?@žFÓÉsí?@K6jû¯�éÔ½ÐÞL¹z®¼ýNnܯŸ$ÉÍ­õJNNh9¶S×*,&VÙ釕�~ØÑ›æËu勤(_ ’Ù×_Û?}¯C÷Ok×Î$�HHHPéÖ““YÆN¸N1c®R^…—>øÆª11 �4P, ‡¡‡´¢_¿~š}Û¯µøƒ$ýâ‘¿j̤éÊÉ8¢ÕKžÖ£7_¢ÂÜ­>˜îþòcIÒ˜I3šÛÃ`ÿŨ¦ºªÉ›}ÿ‹¯i{ ºº:Ç·×ýÃc¯ïßñ™$)nÜEÍ?ÓÓÓé!¹±GBKK—zûÚ‚ëêë:T³Ó�—ý!Ûþ@:bÂ%N¯×TÙkài0:^›yÓÝ’¤¿ýî6½òäýJIjy�–‰3¯ãž­ök7tì…m£µsèì½p¤á˜ñã&9m›—e_Ún@ô�6‰½Ž‡ÿÎ\«°˜¡’¤ìŒ#’ì½i>Y±D^>~ºjÞ��º:r]àLyí½¯jêúi÷‘jmÜ[¡‚²Z ô ô�€v0ûøiê¬ùš:k¾ sOèõgÔ�Ÿi鳿Ñož»ÙƒiãJ ÇRíàQ±#›³1ÌìÖì�}øùS›mŸôõF}þÁrKݯÒÂ|ÕÕש_÷åQCF4;FäàøfÇ(ÊÏqzHn|è]ñ�'´âO´Øöà‘­Ö¦½çe mùÜ s³$IA!áŽ×®š·,"µvébmûä]mûä]ÅŸ7Y÷<ùŠã[ý–jÞUulýš¢‡Ž”ÑdÖ^~Fëßø›¢bO>L6~ãátŒ’Â<dgÊ7À¢àQNç÷ÏM‡eô2w¸9¯Ò¢|4 Éð p®Çñ£©->d_pÙl]pÙl¥îÝ¡wþþ˜’w'êõ?= žû÷ikÞUu,noüƒBZ­AKçÐ8¡eKµnϽи¿_P§�¢07K%¹  ËéÏ«±¾Y�Ž´ÝÄØ{Häd“$}ô֋޹#uäþiíº€+¥¯ÑšK•“v@{÷îÕìÙ³5mÚ4y{3Ÿ Ðʽ¼t¼´Tiiim~ Ù€üóñ»õë9£•{<­Å÷�^ö_Z»ÚK-w‘÷òö“äܿѾDû2‡ç_z�¤“ß·ôÍ÷Gï,‘$Ýñ‡¿kèè Ž¹!Ø%IŠzò[÷ºZ{ywOçå+w}¾Þ~ü&ççáÙpþý:W§ŽœWcïIrssþÏOãê#'^ÚâçÄ�½PÿõìrIÒw»¶¶Z󮪣ÉÛ·]5hé{LÄtò^p 9e¸ÅÑ䆶 ëx´ÝHØ{†äŸH×þí[”ñãºâ†;œæ³èÈýÓÚuW3ùŸì‘·~ýz=ýôÓJNN¦0@ȈŒÐG‡Ú·Ê �´ÀÃÓ ’‚\mßø^‹ïïÞú‘$iÔ„“Ðé +4í’?â‚‹%©ÙÒ™ùÙ™úfóZE Ž×yS¯vz`�6¶ÙççgK’ú‡ŸL™Óp<ÈGîx=$r $)7ó¨ãµJ[¹>~{I³‡Ü!#Ïo8ÖwÎÈÛ6ë÷7MÑÚe/´Z§Žœ×±† -%û* �ª«*õÍæ5òö Ðù—Î’$­zé)ý÷Ü‹='$©¾a.ÿÀþ­Ö¼«ê9Äþçœ&u<¸ï=�0FËŸû]»ÎaP“@ 3÷ÂÀSÚàh[ܘ6B’æ+lt¤í’}˜R@p¨ s³ôñ;Käåí«+æŽèÌýÓÚuW8v¦Æ_ýùõ,I*((Ð /¼ —^zIV«µ×´Ãl6+66Væ’yÔÖqaÑëH@ þF¾­]ö‚>~g‰òOd¨ºªRùÙ™Úôîëú÷ó¿—%4Rsï{¬Ù¾%¹²•Û™™{ïc2™½µâOèhò^UUVèÈIúÛïn“Éì­{ŸzEnîî l�ߪ7ÿyXÃĆŸ½¿\U•:ðÍç:üý¹»7ywùO~.IúÏËϪ´(_Ç�¦êíþ ߀ û/aM’zÇïäáiÐ[/<ªôCß«²¢\»·~¬Wž¼_ù'2ÝÂ|�=¯ÆoÈ'NKЊ¿?®ÜãÄ'n IDATi*.ÈÕ›Ïÿ� s³4ïþÇO.ÿX_¯œÌ£zçï�«8?GeÅZ½äiIjöP|jÍ»ªŽ³n¹_’´nÙ ²–éXê~½þ̃²–iüÅW¶óN�ºâÇœRÃÓ·Í9�h>ÉfGÚÞ(,f¨*¬¥ú~×V͸ѹwDGíó3ŽìÙÆ½Oü–;.Žéh”�‘¶HHTË>›LÒŽqžšñQaÞ�"JHHGÒ’„„Ù¹†¼?ëW̸;Ö±Tcµ­ŸV­J1ü¶{$i–ûÿÍ‘=Û ïÏ­“§) "Ò%øE0lÂ+Äüø)vŸ6ðåþB Š47ƒÈ•¢e?ED®!ÞþÝÉÉ8ÇÿÍy‘G^œ‹›»™Ý[Ö²aù"ÂÞÈèŸNU�¤Qee¥X“ñþ¬_añôá©Yïc0¸^×f³ÙHMMm²œ»»{£Ë”%%%5«½¨¨¨÷edd�™™Ùdþþþ\ÑëIII¡°Ïž‡„„4¸,b[\O{Ƕ±ëiïØ^é{¥¹±m�{¥¹±mÉõ4§¾–r÷ô =¯‚u{ìô 1Ó˨_"JHˆˆHC&<þ"Þ~$¬_ÉoIyY9݂ø󡧹ãþ§p5º)HÒ¨—ïÿ!YÎ;œŸ‹îÁ×]×µwï^.\ج²M 1�;wn³êiì’„„Ö­[×dwÞy'ãÇ�¯wŸÕju SoÍõ|ôÑG;v¬Ézf̘Attô»žöŽmc×ÓÞ±½Ò÷JscÛ÷JscÛV÷J[(«pâû”R¬e ék¤$ï G�%>>^¿4D”�‘jÎÎÎÜ~ßÜ~ß †\–?¬ØyM^—Ýn'°G0çÏ¥5Y6=·‚eÿný²�Õq*¥´Yu|ŸRJ~õdŸ-jVM]Oznó†£o ûÄ ÊŠŠ¡²’R»�ó²í�wøú­?qÛmuêýê«x…„�}âË'M&áOâì¾}çæRQVNqn.g÷îeûÜwùhòdrNŸÆ§W/~ôÊ+zÚ�FHˆˆˆˆˆˆ\¦€°¼ûrúÀÎ$mÃË¿'�A¡ ÌdòñàBbb£åÿë_œÜ²…ÂììZÛ  ×Í7Sj³±î×3È?s¦ÑzòRÓøì×3˜øáôu Ýú÷çÂáÃz!Ú€FHˆˆˆˆˆˆ´‚«›…ð'0pÌÇü„O¾µqü\©s…䥦Ðïîñ�¬¬¬“Œˆ¼c,‡þõ¯&“Õr­VŽ|ò Q�—ÖSBBDDDDD¤ øEàéBY…;“Kør!E L;°|97Ϝɭo½EXÜ\ŒÆftqòÑ“[¾jQ»Ç7m®u¼´žÙ‘kÞöíÛÙúïrl• ºõ)DDÚEz^Ÿî*d`˜+Q=]9ÖŠÙlÖ¤—­tä“O0ûû1ô¿ oüúÆ�¡¢¬Œ ‰‰œÛ·Ÿ´Ý»Iݵ‹òâúWVò  ëøñµ[]Þ³G½mD ¹æeffrúä1BD®ŠïSJ9r*‡=Ÿ- 7'‹»îº‹qãÆ)0­°çý¿“´î3®ŸðS®»åüúô!pÀ `ÐS(+,äȧ«ÙµhÅùùµŽu5[(-,lQ›¥öªeU]-z‘̹£G9±|9“'On´¬""""""WعÔãäæd°víZxøá‡‰ŽŽVp.SÁ¹s|ó×|ó׸y{4h= "ä¦éÖ¯'O¢×Í7óÏG¥03³VbÁÍË 7OOŠrršÝžÑ£*Qj+PðawwÇn+ÀÃjm²¬æ�¹ÂÂb<öY,>Adeeñî»ï²zõêË®344”3fÐ') ³½°KÇ·87—S_ÍÎyóøøÁ‡Xqßýd&'ãÌð§Y«lÞʼn,»µ0äYu|Zšnè6¢„„ˆˆˆˆˆH;ðô¡ÿ-?ÇÍâëØ¶nݺˮÏb±�W� C…&Ϭ)39™/_þ-×�Ykß¹}û¿õÇ-ª³Ï�Fp滽 pQBBDDDDD¤d¤à»ÏæPl«ZŠÒd21}út¦ÂâFpÓOàîïßdÙê‘ 7·ZÛ“.&�"︃€¨¨fµëJôøªeF×¬Ñ ÑF”�ifO_ÊK‹6l³gÏf°–�l‘!?ÿ9Cý9Ã~ÙôŠI·Þ Ô]Mã‘#œÚú5ÎcçÎÁ?<¼Ñz<{öäŽwßÅ`2qô³ÏZ¼:‡4L“ZŠˆˆˆˆˆ\a½»»0dXýýÂßß_“Y^¦o,dÜ_ç=nÞ!!\±’sßÀž™••˜||ð ¥o|<×ßsû?ø N=_ÍšÅOûôÆ;4”ŸýßÿräÓÕ߸‘ÌcÉç`ôðÀ·÷uô5Š?û“‰Œ¤$þýÇ·õ"´!%$DDDDDD®³†G¹Ñçªë§ ´BÚîÝlþ¯rËË/4x0A�Œ0©(+cÇ{&yý†:û ³²ø×£�1æµW >œëv×ÿìžë:öå—|=û÷”Ô¿ÂÆ»w5zÞ †Þ¨¯JHˆˆˆˆˆˆ´‘ÂüL F®n„t�›‚ÒÆŽ~þ9Öo¿¥ÿOî&ä?À·W/ܼ¼Àɉ’‚rNŸæÌž=Y½š¼Ô†WÄ(ÌÊbíÓÏrÓMDÜ~=bbð Âå✙ÉÉœùn/IkÖp!1Q�¿”�‘kÞˆ#èÙ—ïN”*"rÅœ>°�Ô#[¹.úFž|ì~Þ NÓ#""""""-tîø·ßý‰cO“Éĸqãˆ��Wp:9ëÎ�\¹’÷ÞKȰpï°8öé�¶¥„„ˆˆˆˆˆH3ægrtÇrrÓ�;¶ÅÄÄ0iÒ$ kÄ¿ÿðG²Žãú{ïÅ',Œò’RrSN+0mL éR‰ô“»›,g4ûþƒ÷Ÿ=ö %öœ&ëés[ƒû²Ï&“wáD“uxuëÓèܧlh·ëéÞ{(îžþ­Šm[\O[Ŷ-®§=bÛž÷JS±mÏ{¥©Ø^z=.®&BúÝrå;PF¶œª•|}}™4iƒYzR:¯C«þÉ¡UÿT ”�i£Á SeV³:eþA}ˆ¹aXäžÞEæÙ¦;ˆCo¾½Á}é‰Ç9}à‹&ëˆz+Ý¢"Üÿu;^OØu}èæUÿ7ÀéyÍ‹m[\O[Ŷ-®§]bÛŽ÷JS±mÏ{¥©ØÖw=ÞÝûâérå:OΕü0Ö�Æ‘‘‘Á¸qã4W„ˆ""""�óópah_ÿnFY_‹3·27¸ÿÎd6£žÆê(:íJsVµïèÚh=«Úñz†ö5]=‰nÍ‹m[\O[Ŷ-®§=bÛž÷JS±mÏ{¥©ØÖw=e%Wne‹È 1½Œ¸¹:¦y"DÚ„Seee¥Â "�Ù´iÓŽEØàÑ ˆH’²w3i¾`Ñ¢E ˆH”˜˜È»ï¾ ÀÀ1O´Ùò¾¥Å6\Ý,x›�Ú×HC‡¾ö>IIxØtCÈU—éçGÀðá ¼õÇÄÅÅ5ZV#$DDDDDD.ÊÏLåÄžOqu3ñÀÔéô 1*("-�éï‡õÜYò”�iJi±�3IÛjÍoQtá„tÜ +¸óÎ;Ùýßÿ�[IéU?Ÿ'v荒­²¼œâü|òÏžåÜþ$oØÀùï¿ï°1­¾-ïÙ>œéʲÏ&óÝgsk%#î¼óN"##;ôy0~üx‚Ξí¤¤Cž£“‹ &ºõëÇÀÉ“˜°øïŒ_¸��mÖÆÀIëM†Hǧ"""Òi•Ør‘.Îl6Nzn£{‹Ž--¶qtÇGd¦tl gÒ¤I„……)¸—©æè'Ü<=ñ§÷¨[ˆ7Žà¡C™°x1ÿœúçÏ·º½nýú)è�”"""Òiåg¬`ˆtQaaaÌœ9“eÿnÙ„Ž©G¶rúÀÊK‹0™LŒ7Žx­ Ñ¦*ËË)ÊÉ!m÷nÒvïfß?þÁØ9sˆŠâdzgñ¯GkuÝ¢•�謔�‘N«¬¤ª#áïï¯`ˆH³•ÛH;²Õ‘Œ6l'NÄb±(8WXÁ¹ó¬}æWÜ¿êcz D؈¤$$Ô*còñaðÔ‡é‡gPÎöŒLÎ|·‡ï/!ûäIO}˜a¿ü¥ã¸êÇ6¾|éeŽ}ñE³ëiHÈM7qÃ#S ˆŠÂàæF®5•¤Ï>ãÀ˨(+oñùÖ8p 1÷ßG��1˜ýý(+.&ÿÜ9NnÙÂ÷­ (§îÀÀôÀÅÆbòñ¦8/�óßdÿpfÏ%$DDDDÚKQ~6…9UC}£¢¢i¶ë‚<òèT–ýc)S§N%::ZAiG…™™|¿ü#†<ö(}F�®•�ð dÂâ¿céÞ½Ö1=‰¼ãúŒÃ'�?΅Çm£5õT”•sÝÈ‘Üþöqrqql÷ ïËðgž&pÀõlxá¿.»�>£GsëïgתÛèêŠx8þááôûÉOXõðTlééŽýQwÝÅ�~÷ÛZǸûùqÝ-#¹näù÷ÛïppÅ %$DDDDÚC–õˆãçŽ>ñœˆt f# �r£‡�èÇ[o½¥ \%'·neÈc�ûÐ_<†¥{wÒbÛœ9d;@@d$7?ÿÝúõcØSO±æ©§Ø»d){—,­weŒQ¿}¹ÙõÔQYÁ_˜É‘O?eÿ�—šŠ›§cÇ2âWÏÐgôhzÝÇémÛ[t¾Õ~ðä8¹¸°wÉR­Z…í &7‚ßÀ_˜‰gP?xò 6¿ú^!ÁÜòÒ‹|·x1G>]�-=s@·ÝÆ�Ó'îÙgIýærNŸîT÷�VÙ‘N)ïlÕX___M>'"MjàŽÌ“rÕ?ÃSSp÷÷«µÝ+$„R›�M¯þ?Îÿ=e……”rnÿ~¶¼þ�4YkêqvuåÜ�l�ý{rN�¦¢¬œÂìl|ðûþïDÜ>ö²Ûñº8ïўŋÉ?{–в2J lœþ÷¿ùâÅ)ÎËÃðŸ{÷Þ‰¸�|»pßÌÿ+y©©”—”�æ ß-^Ìîÿþoœ .ôÿéO;Ý} w£ˆˆˆt:EùÙd§&{É·k""P5ie¦õ ñ?û%CûºáçárÍ]cbb"ï¾û. ¹�>IIxØ:͹—Úpu¯½2ÊêéO4xLÖ‰UǘÍMÖßÚz®ü¸ÞíÇ6~Éà©Ó½¿Ën'ûÔ)ü#"õòKlŸû.öŒ Ǿôƒ‡øûè1µÊ‡Ü8€¤uëêmçèçë¹é‰'è9ä†ñÚúgfÏJHˆˆˆÈµçÔ®Ï?kF|©)?3•¤bÏ9 €ó…üés¢£qóô 87·Î>“�7î�HÈM7∻¯/ÎC­ùš£5õd&'×»=7Å PkCKÛÙüÚkŒÿë_ ¿õVúÆÇ“~èi»v‘úí.ÎìÝKeyy­òž={ðÐgë=g¯��Ž‘�ÈÊbHϞܧ„„ˆˆˆ\[rΜ ÛšTÍŒPã�BézRRRX±b鹌îd¦tìóõõ%¤ƒtÒ¤¶€‹“çŸ;W§óýÓ÷ÿK·n­ª¿µõ”Úíõn/+ªZ™ÅàævÙíd$&ñá½tÿýô�CàÀ�È ?ÿ9öŒL¾]¸€#Ÿ|ê(é(’†›1r¤£QBBDDD:�Ò"»ct„Édbܸq ŠHg·ÛI¾äÛl“ÉĘ1c?~¼ÔAõ3 Îr•ßyK·näŸ=Ë7óçsvß~Šrs©(-¥¢¼œ'v}Û¬ú[[�‹›e……u;Ð&SÕï£Â¢VµS˜™ÉÎyóØ9oÞ¡¡„Nßø1ô¼áFýö·¸¸9¸råŶ 1zxðþ¨QRPpMÝJHˆˆˆH§qj×zÇRŸcÆŒÑè©#<<œGyDŸ˜_ß¾DßuP5ÿAMÕó%¬}úirNÕ^1£G�f·ÑÚzüúô!ýС:Û}zõ àü¹6;ß\«•\«•ƒ+VÐÿ§?å–—_bДû ‰\«•nýúás]/Òº¦î­²!"""�BÊÞÍdœØçèpè›OÀd2a2™˜>}:3gÎT2¢óéÕ‹±sæàìêÊñ�ëÌÕàb4`K¿PçØÿTVVud u¿[¯9gCkê¸~„z·‡ßv+€#Yq9íÄ¿ù¯ÿœî®¯sLò_Ôz$õ›ªQ±ù¤‚""Ž„Ä{ï½Ç{ï½ÇàÁƒ�ÈÕb!pÀFüú×Üû�ÿÃ+$˜\«•­³f×)›}ºj”Á°_>…ÉÇ£‘î×_Ïíoÿ£Åƒ¼´4 ê‘gWWà?ó:ô�ƒ³Á€»ŸßeÕS­¢¬œà‡2â׿Ʒwo &7<ƒ‚4å~ÝwIë>»ìó'ÌüxÖ,z݇ÑÃ'<ƒ‚þôÓd;æ8ŸC«VQVTDßø1Ä¿ùÞ¡¡8 ˜ü¹þg÷pÛ[¿Ç;, £‡G§»7œ*+/¦lDD:©iÓ¦3аÁ£‘kHÕœë##L&Ï=÷aaa ŽˆtyŽe?¡C,ûùÄî]Í*—¶k_¼øE99uöEÜ~;ño¾Qg{Á¹óüóç?gØÓ¿$rìXÇöCoäîE‹ê,y¹ñ·¿kq=ΦíÜIIA_¾ô2cçÎÅÙPw¥ŒÄ5kØòÚë—}¾žAAܳt î~~õƧ¼¸˜µO?Ùï¾slëOü›o48¢ãBb"«§O§¤ƒ,ý:ä±Ç¸iú´&Ëi é�rΜàÔ®ÏsF(!"Ò9•cÏÌàÜþýý|=Ö;,›¼~=&o/Nš„gÏžØ32Iýöv-ú¶ôtv-ú¾½{ãß·¯c…Ž­¿ŸÍ¨—_¦[¿~T”•“sêÔeÕãb¬Z9£´°�”„Ö<ù$C}”ný¢1¸¹‘“’‘O?åàŠ•­:ßü³gYqÿb&Oæº[Fâѽ;.F#¶Œ ÎìÙÃÞÿý?²Oœ¨—ã7’}ò$±>@ðС˜ýý©(+#ûÔ)Žmø‚}DEii‡x½�F„³oïwl}ûmfΜÙhY��‘NO#$D®-EùÙX÷mqŒŠ€ª9#ž|òI,‹$"rQG!!U »—áááM&$4BBDDD®º¢ülòΟ"+åÙÖDÇv-Ý'"Ò°èèh-ZÄ‚¡7*Ò))!!"׌¼ó§°îÛ¢@ˆtEùÙÛr(+.t<–QÓ°aÃ7nœf˹F)!!"׌üó§È?J�éÄ|}}‰��%>>^‰‘kœ"Òé“vqI%é|ï_BCC‰��Õ„•"""]ˆ"Òé½òÊ+ ‚ˆˆˆˆH'㬈ˆˆˆˆˆˆH{Ó ‘HLLdÁ‚Õ»ÿÎ;ïltEˆêejÓœe²DDDD:;%$DD®q7ndß¾}W¥m³ÙÌÃ?ŒÅb¹fâyôèÑ“ÇÒ ùüû¬Vµ‘e+sÔ‘“žÆ÷ÛÖág10qâDͱ "")))¬X±‚3§¦b.,TPäª3âDhhh“e•�¹Æ­\¹òª¶oµZ‰ŽŽ¾&c3ªÎ6gŸ0Îç—¶è˜K¹yø8êÈÉ. óÌI2»Ý®ZDDìv;ÉÉÉàåI™‹žÆ—Ž!$5�!·�å¦É“›,«„„ˆHa4{ãæáÓníå§Ÿ¾æc6xt»#"""r-RBBD¤‹è>¸];Ã;–^›«ŸDFFRP\αô"ÝT""""­ „„ˆˆH DGGãÔ›’C9 †ˆˆˆH+èA#iwJHˆˆˆˆˆˆˆH»SBBDDDDDDDÚ�"""""""Ò&RC‚Ywô(Ë—/o²¬&µ‘6awwÇn+ÀÃjm²¬"""˜Å¯ý~<•Ûøª€ˆˆˆChh(3fÌàÓéÓ1Û ét”�i�Õ«W³nÝ:†?üúoÏÕdƧg¢£|©Åb±Í–›‚!�’æ�‘v§„„ˆˆˆˆˆˆˆ´;%$DDDDDDD¤Ý)!!"""""""íN iwJHˆˆˆt`EùÙX÷maÍš5ddd( ""â`³ÙHJJ"ßÃ2guí¤óÑ]+""Ò�åg“º k×®UBBDDj±Z­Ì�;—ãQ‘ØÍî ˆt!©iÜÞ7œI“&5YV iæÂB‚½¼ k²¬Aái>Âz÷%ÛV¦`ˆˆˆˆ´‚"""-GŸCÙp(GÁi=²!"""""""íN iwJHˆˆˆˆˆˆˆH»SBBDDDDDDDÚ„ÝÝ�3ùù¤¤¤4YV i©!Á|~,™�>ú¨É²ZeCDD¤3yú3ŠØP  ˆˆˆ8˜ÍfÂÃÃ9óÝwÊ+ét”�i�íÛ·³u[Y¶2Üþó+ÞžÉÓ—°Á£?"P … IDATÁ‘Z˜9s& †Þ¨`H§¤„„ˆˆH dffrúÄ1BDDD¤•4‡„ˆˆˆˆˆˆˆ´;%$DDDDDDD¤Ý)!!"""""""íN iwJHˆˆˆˆˆˆˆH»SBBDD¤Ë9s‚K_aÚ´i$&&* ""â�’’Â;ï¼CrDvwwD:ÿÌ,b{0bĈ&ËjÙO‘NÈn·“œœ ^ž”¹è»féü³²Ò³'7ÅÅ5YVw­ˆˆˆˆˆˆˆ´;��i�#FàÜ›]'  ‘VPBBDD¤(uõ$¹8GÁi=²!"""""""íN iwJHˆˆˆˆˆˆˆH›(sn~šA ‘NÈl6;~.5º) rÕ•9;spp,ïïýŽíÛ·7Y^ ‘N(,, ___\�Õ­“Ž!+ Àñshhh“åµÊ†ˆˆH dddp2å¹g ðê}ÅÛóéÙ‡á¿ÎÔ� ¾ˆˆÔ1uêTºy{³î±_`S8ä*²»»s.¨ÁÁÁ„……5yŒRi"""-���ÀÒEó8üÅbCDD®ºèèhüƒ‚¸ùùç ¹jìî Â`Àd21uêÔf§„„HçääTç?ƒÁ@·nÝ:t(Ï<ó ;vì¸"mõÕWôïߣÑH÷îݯڵ‹ˆˆˆtv}~ô#~ö�ÿÃÕbV0¤]åyXÉ€‰'6kt(!!"õ(//'##ƒ={ö0oÞ«ÕÚ&í¥¥¥‘žžÀÖ­[),,ìqسg�né´ºEG3ñÃ0iÉœîÖ¢eEš+×LJŒ‹É/“ÉÄôéÓ[”Œ%$D¤Õ�lŒ=š÷Þ{�ƒË™3g˜ÿüó:evîÜɽ÷ÞKPPF£‘ÀÀ@î¾ûn¾úê«:e‡ F¿~ýÿ®o.‡ .ðüóÏÓ¿, F£‘°°0|ðA>\ïy65'Dcûßzë-œœœ8xð`­²Ë—/× """�Nhh(111T œëÄÁÁ±œî†ÝÝ]’ËND yì1X³†›¦Oã¹çžã©§žÂb±\V}JHˆH³ôèу§Ÿ~€U«VÕÚ·dÉn¾ùf>þøcÎ�;çxäcõêÕŒ=š¿üå/-jËjµ2xð`æÌ™Ã‘#G°Ûí”––bµZùÇ?þÁСCÙµk—^éŠò³±îÛš5kÈÈÈP@DD¤Yxê©§˜1cáááŽíÙíß�Äè(Ò»uÓ¨ iPŽ·©!ÁXk%"L^^UÛ/3¡„„ˆ´ØÝwß À¶mÛÛŽ?ÎôéÓxñÅINN¦°°�'N0kÖ, 3fÌ ))ÉqÌÎ�;©¬¬tü»zîŠj¯¿þ:iiiÜtÓM$$$�ŸŸO~~>Û¶mcÈ�!òÒK/µéµýæ7¿©÷œÚê‘Ö$$R÷oaíÚµJHˆˆH‹EGG3sæLf̘Á°aÃþóûÅbáLX(%nn ’ÔJBœîÆ�A1œ '#0�ÞÏ<ÍCëÖÖJD´ƒB."ÍÕ·o_Ο?ïØ6þ|Š‹‹™5kV­$AïÞ½y饗¨¨¨àw¿ûûÛߘ3gN³Ú9~ü8žžž,]º”èèhÇö¸¸8Þÿ}bccÙ¹s§^‘$&¢££7nûöícãÆ�<ôß#qÍZN~õ¶ãI×Pl4RàáA®�žŽÕ2ª™L& ={^±ö•�‘fóððÀf³9¶mÞ¼¹ê—ÙCÕ{Ì”)SøÝï~ÇÖ­[›ÝNu�õ¹þúë(((Ð "WEdd$ÅåK/R0DD¤Ó  >>žøøx222 [t4?œù<I\³–3{ö�uì©!Á”ÝðÈÏÇ£ sYMÚF¦ŸÖÞ×Õ»/&&†#F\ÖD•JHˆÈ‘�� €ŸŸŸcÛ©S§€ª‰“süøñµ•‘‘ÁüùóÙ´iV«• .PZZJYY™^¹ª¢££ñêMÉ¡CDD:µ€€€ZÿîM·‹£S‹òò˜ùâ‹””•‘çë€sYùU‰ K~>îv;†Š ²ê5ò‡DDFbÝ»¨ Kll,‘‘‘­žB is{/~`………9¶5w¤B~~~³Û9yò$7ß|3gΜQÐEDDD®‚rn:”¤¤$Ç—Ry¾>AUY›�èÄ$¬È÷ð Äh¤ÐìN¡»›—'=­©ôvr" *’à!CˆŒ"øÆ¡Žcì«W[ëïûö¤„„ˆ4[õê£F�rlóðð 77—ììl|||Ú¤�^x�3gÎЫW/fÏžM\\þþþ�F ...-®³¤¤D/ ˆˆˆH3Y,yä jäjRRG�­• °tï΀A±d=ÊÙ‹_^Õ”é燱¤—òr=òцò<,Ø=½ª’nFJ˜œ4èÎ;xè©§¬güøñWõ:”�‘f9xð K—.ªæ…¨Ξ={HJJâ?øA›´U=‡ÄúõëkMj púôé�srr¢²²’¢¢"L&S­}‰‰‰zEDDD.C@@ÄÅÅUó‰Y­VŽ=ŠÙlæ‡ññ޲É?{–Œ¤£dMât~>Ù5–u-.ÆX\‚{¡Cy&» —ò =þQCþÅyÛ‹‰»¯/'ºwo°ŽððpBCC‰ŠŠêÐת„„ˆ4)))‰»ï¾›’’î½÷^ äØÏž={xç�wX¹re�cׯ_Ï3Ï<ÃĉyóÍ7›Õ^qq1ÁÁÁuö½öÚkŽÄCII F£Ñ±ÏÓÓ“¼¼<öîÝËðáÃk÷Ç?þ±E×\VV†Á �H‘KY,Ǫ—ªž‡¢Ï�~À§Ó¦ÕÚ_êæF©›6/ÏZÛc÷|×`{y*\\p)¯êœwöÑÕ£j&êå0¸¢’€d$nžž‚ÑÓƒnÑÑdddðòË/Œ¿¿?¡¡¡Žÿ.�D é|”yy>|˜•+W²hÑ"l6ááá,Z´¨V¹éÓ§3oÞ<>þøc¦L™Â«¯¾J¯^½ÈÌÌä“O>á¿þë¿ÈÏÏ'77·ÙmGEEñÝwßñâ‹/òꫯâááÁ�xë­·pvv¦OŸ>?~œU«V1aÂÜ.~x8�íÛ·óì³Ï²hÑ"¢¢¢¸pá¿ÿýïÙ±c¾¾¾µ†ÖÇl6c·ÛY¹r%÷ÜsÙÙÙꆹ sçÎÅjµ’™™Iff&V«»ÝNjj*EEÿY±jʧŸ�æ,i{öPœŸOæÑ£ä§œ�lù¬ w{!�gÏàU`«·L±ÑH¶¿“çëZ\ŒVVƒûÓ»w§¢ÆãÃvwwÊ/yœ¸ÜŹÁy5\-fœ""9×ÈõTë9¨ówygäTYYY©·‰H×åääÔ¬r£G�fùòåtë֭ξ+VðÀPZZZï±7Üp›7oÆÛÛ»Þ¶/ýZ¶l<ð@�zBCCIHHà7¿ù Ë–-sl¯>~ùòåÜwß}uŽ3�¬[·ŽGy„ÔÔT***prrª·ýQ£FÕY¢´³LN»øÍDpÌ(Â�n·vw,}€3fÔû Jgv6·˜ í´ÊFi‘[Ö9nàKhhh»Íz-""ÒRRR°Ûí�þ­0þ|8Ðd]·÷ 'ØËë?Iˆ‹ €ŒÊJv¹87Y‡oEÃhøïã�TÖz¥!¯<ö ÇÏþQ‘˜jœ×êÕ«Y·n&“‰��G‚ÁÿbÂ$22Òñ·ïµþ{_#$D¤^îîîôèу¸¸8¦L™Âí·ßÞ`Ù‰'Ò¿Þyç¶lÙ¹sçpuu%::šÉ“'óôÓO;F14Ç”)SÈÊÊbÞ¼yœ:uŠ=zÏk¯½FHH¯¾ú*GŽáàÁƒµfžœ´´´zëtwwgëÖ­Üxã�µ¶ÿéOâÙgŸ¥¾�•!C†°mÛ6L&S­sê”â‰'X¸p!qqqlذ‹ÅÂ’%Kxì±Çê='''þüç?óË_þ²Öö’’ÜÜÜèÝ»7ûÛ߸ãŽ;êM¾¬\¹’Ÿýìg<òÈ#,Y²¤NWWWV¯^ÍØ±cë=çŽlÚ´iÇŒ"lðèvkwÇÒW˜1cÑÑÑ×Ôûêlnq»­²Q�y†Sß~N —+“&M",,Ll"""rMÐ ‘®ðFwvæé§ŸæÑGåÈ‘#”””pþüyæÎ�‹‹‹ ÿüç?Y·n�£ü믿NZZ7Ýt äç瓟ŸÏ¶mÛ2d………¼ôÒKµÚ8tèÏ?ÿ<ƒ�ùóç“‘‘�ÍfcëÖ­DGG³gÏÞ|óÍ&Ïõ•W^aáÂ…Üxã�|öÙgX,Ž?ÎôéÓxñÅINN¦°°�'N0kÖ, 3fÌ ))©NªFJ<þøãüâ¿ %%…ÒÒR¾ÿþ{nºé&þò—¿°eË–,Y‚‹‹ ï¼óçÏŸÇf³±ÿ~¦M›ÆôéÓqvÖÇfW—˜˜ÈW_~Žuß–viÏÿ'Æ>ÊÌ™3•Œ%$D¤s)))aøðá,Z´ˆèèh\]]éÞ½;3fÌàù矨õèÂñãÇñôôdéÒ¥ ><<<ˆ‹‹ãý÷ߪŸ¨iÁ‚”——ó›ßü†'Ÿ|Ìf3#GŽäÃ?Äl67ùØÃ¼yóxã�7ˆ‰‰aÆ xyy0þ|Š‹‹yýõ×™={6ááá˜L&z÷îÍK/½Ä+¯¼BYYûÛßjÕW=#33“‘#G2þ|BCC1 0€ùóç°ÿ~þ÷ÿ€gŸ}–çž{ŽîÝ»c6›‰‰‰aÞ¼yŒ9’ŠŠ ÝP]ÜÑ£GùêËÏIÝ¿EÁQBBDšòä“OÖ»}âĉìÞ½Û±móæÍäååÕ;̾z>†‚‚‚ZÛ·nÝ À½÷Þ[ç˜ØØXl6[ƒs=|øá‡üêW¿"::š/¿ü__ßZçðÐCÕ{ì”)Sj�C}žy景%77€o¾ù€ûÞ:ªGiˆˆˆˆˆHë‘®!&&¦Þíœ={¶ÖöŒŒ æÏŸÏ¦M›°Z­\¸p�ÒÒRÊÊÊê­çäÉ“ôíÛ·Åç¶aÃ~øa\\\X¿~=Ý»w¯µÿÔ©S„††6ZÏñãÇÜYg›»»;ðŸù ¬Vk­˜\jÀ€º‘DDDDDÚˆ"]„§§g½ÛÍf3………µ’ 7ß|3gΜivýÕÇ»¹¹µøÜî¹çÇd“ ,à­·ÞªµÿÒÑ ÉÏÏopŸ‡‡G“ÇÛíöZ1¹”ÅbÑ�$""""ÒFôȆHQ3áP_'¼fgû…^àÌ™3ôêÕ‹eË–qêÔ)òóó)..®w•‹šÇgff¶øÜùè£�pssãí·ßfÓ¦Mõ&²³³©¬¬lð¿†Fo4Wõ ŪúÑi=%$DºˆÃ‡×»½zeŠš�CTÏÙ°~ýzî¿ÿ~zõê…‡‡F£ÑñXÃ¥z÷î T­@ÐR;wîdâĉüþ÷¿§¢¢‚|�ŒŒ ÇþðððZçz¥ôìÙ€'NԻ߾}º‘DDDDDÚˆ"]Ä¢E‹êݾ|ùrǘÅÅÅ×)ÿÚk¯9V¯())ql9r$K—.­sÌþýûqwwgøðáõžC·nÝøõ¯Ím·ÝÆÙ³gyä‘Gûãããxç�wê=~ýúõDFFòÛßþ¶U12d+V¬¨wÿÂ… u#I»+È<ÃÁõç�wÞ!%%E‘k†"]€Á``óæÍ<÷Üs>|»ÝΩS§˜;w.úÓŸ€Ú+XDEEðâ‹/’‘‘AQQß~û-&L //�>}ú°jÕ*GòâñÇÇÙٙŋ3{öl.\¸€ÝngëÖ­Üwß}1bĈFÏÓÉɉ%K–Э[7Ö®]Ë{ï½T­na6›ùøã�™2e ÉÉÉ”””pöìY,XÀĉINNnõ#“'OàøùË_ÈÈÈÀn·sàÀ¦OŸÎ®]».kŽ ‘Ö(+."ÿü)’““�X‰ˆˆˆ\ œ*«§—‘kNii)F£ooo>üðCÆ�_ï< S§NeñâÅŽ/[¶Œx N¹ÐÐPøÍo~òeËÛ«?FÞ~ûm^xá…zÏ¥_¿~lÛ¶ ??¿Z ˆšÇW[»v-ãÆ�ÃÍÍ��;wËŠ+xà�“_^ê†n`óæÍx{{×IrÔ×FCûïºë.Ö­[W§œÁ`à“O>aÊ”)äææRQQá8¶£›6mÁ1£<ºÝÚݱôf̘Qï²�ÕêÕ«÷Èð‡_¿âíåœ9Á‘/—\“±‘®M#$D®aEEE@Õ„“cÇŽeãÆ�ÄÇÇãëë‹Éd"&&†÷Þ{�ÿùŸÿ©uÜ”)SøóŸÿLDD®®®„††òÈ#�°}ûvBBBxõÕW¹á†0�ŽùfΜÉçŸέ·ÞН¯/®®®ôéÓ‡^x��;wÖJF4æ®»îâ©§ž¢¸¸˜ûî»›ÍÆÄ‰ùî»ïxøá‡ Ãh4b±X2do¿ý6 u’—cÕªUÌž=›þýûc2™ðñña̘1lذ�;ï¼Ó±ZIul¥ë1b=þKúýxª‚!"""Ò !Ñ]:1àÑ£Gë”±Ûí N^ØÌfs­I«ùûûãïï_ëßzÑDZA#$ÚÞÙÜb6Êi—¶4BBDD¤6›ÍFjjj­¾KÍDDD4ú;³¡¹É.õüóÏ7¸oûöí¤¦¦ÖY2^}šæ3(�_FF†c©ÅêäBÍ„Bff&YYYî¼÷ïßßâc"""€Ú �šoøê¹DDDDD¤óõiìv;‘‘‘µ–¤¯iãÆ�¬\¹²ÉúÆÜ|3>ÅÅ50ÇXrrr³ÎëÔ×_7¸ïøþýl?x°É:zè¡ìرwwwGÿ¦+õi”�èª3€V«•ÂÂB¬V+v»ýŠ&|\]qmÁóñ�&¶òr x¾¿>9¥¥”¶p€NÍŽÆÕ‰‹ÐÐPÌf3!!!˜Íf%,DDDDD®’””Ž=Z«OcµZë< û‹ûï'üºë(ÊÉq$ Ο§¬¨ˆ¢ììfµeÝ·�¤FF|w3?]\ðuuuü»¤¢‚ì‹«É�Þ¶­Á: óò0»¸`//oô\Îý5ûj,+ïÓ«WUgÜdâ\a!k×®­÷¸àà`DZ!!!ÄÅÅ)!!Wö š™™éH>Øíöfgî.evqÁâ шÑÙÙñfó©ñf3:;ã×�V (./w¼ùJk|d—–Rrñ ¡Æö†õÅÏÏÏG²"22’���³°"""""Òz}ôÇŽk²Üñ�±¹»×»Ï©´”øùa1ü§+ëa0àQ£�Ó? jõõD{yíåÕdŸÆ£¬ŒÜÉ‘š?Ÿ+,l°þ´´4GŸ¦_DD£ ‰””üýý;]ŸF ‰«$)) «ÕŠÕj%33³E‰‡êÑ ®®X\\° X †—\¸n..ô¸äÃ'´‰7UÍ7|úÅìêù‹ÿ¯oFVVYYYub^�¨ˆŠŠ"$$„ÐÐP=ë%""""R�š£¸kökÞ{ï=J ±¥§;F5äœ>MQn.¦ôô:}šê/O›Û§ñpumqò¡#ôiÒÃÝ�û{õª·OSýelNi).çϳuöl\ŒF<1˜Lx∛·7³fÍê”}%$ÚAFFG�Åjµ’””äÈt5'éh29F5ø�¸]õ õ¿ázãgc++#»¤ÄñhɥɊú&“‰¨¨(BCC‰ŒŒÔ#""""Ò%¥¤¤�ššêøbµ¡>ͦ·ÞÂPQQï¾>Dyy©Os™}€ò’Nj̋ý–š�Í7Õ§7nœ]%‘””ľ}û]ÐÕÉ WWM&,¾Fc§åÐù¹¹áçæVg´Eu&2§´”ì’ÇÏÕŠŠŠØ¿­ù*‚ƒƒ½z9þ«–˜˜Hhhh»ÎC¡„D+“ûöíkp„«“ÝM¦ª7ëÅoè¥s¨QÑ×Ó¨I‘^TDvI ©……ŽŒcÍ+W®$88˜#F(9!""""�ªOcµZyå•WÈHJ"çbÂVc·ëÍf"L&õi:aŸæR¹V+¹V+§·mÃÅhħW/"#y÷¯ªV,Œ��m—>�-”’’¦M›LBt3 µX”€¸Æ¸¹¸j±j±ãëëHP¤ÖA‘––ÆÊ•+ɉøøx ¤Wç»”·¸6•—”�™œÌ¾}ûÛ’““INNfåÊ•DDD0bÄFŒ¡„ÄÕ”��@BBB½«a»»âîNˆÙ¬áJ]0AUáRíöZ£'ÒÒÒXºt)&“‰ØØXÆŒCXX˜‚'""""íÎf³±ÿ~Ö¬YCVVV­}®NN„˜ÍT”–‚¾Tí’B-n78QP@ja¡ã ×êäÄG}Ĉ#3fL›ŽšPB¢‰ˆúÞ´Áîîô©Ñ!•®­z8TŒ¯/¥¥œ((à„͆½¼œ¢¢"vîÜÉÎ�;‰ˆˆ`âĉJLˆˆˆˆH»Ù¾};+V¬¨3»ú‹ÕêÇ”E}?77†Rõ…kÍäDQQ›7o¦²²’É“'+!q5fúX,ôñðÐ�%i�‡«+1¾¾ÄøúbµÙ8a³‘VXTeg͚ŰaÃ7nœæ™‘+ÿ÷©››#QݧÑò›Ò˜šÉ «ÍFb^JJ號Oö©Sø^w]›´£„Ä%RRRX²dI­•2Ì.. ôöVæPZ¬ú±Ž‚ÒR¾ÏÍå¤Íà1q×]w1zôhÍ1!" *ÊÏæÂñ}¬É´0|øp%2ED¤ÙJ IÛµ‹œmÛˆôðÀ×hTŸF.»OS\^Nå… øà¼CCéõö:1¡„D kÖ¬aíÚµJDH›ópuex@½½k%&Ö®]ËÞ½{™:uªã‘zåg“º ©û«f½VBBDDšãìþýÿòKÊKJêﯠH«ÔQ“kµràƒ8�¾ññ¸^²ähs9+¬U¼,^¼Ø‘Œpurb€—? Q2BÚTubb|Ïžt3�ªÉ/çÌ™ÃÞ½{ i•ÒÂB®\ÉÑuëÉ‘+åü÷ßóÍüù¤9‹)‹$ì IDAT/¾HJJJ‹ŽWB˜3g;wîÀÇÕ•1��Äøú*0rÅx¸ºòã  n¸xŸ±páB¶oß®àˆtpþþþ„õî‹g÷^ †ˆˆt6›�×_�-_~ÉþeËȬge@‘+¥¼¤„?ÿå/dee1gΜ%%º|BbùòåŽù"‚Ý݈Ÿ–º‘víåÅèîÝqur`ÅŠ-Î*ŠHûŠ‹‹ãçOüŠcU0DD¤CX±biii,ÿøcŽŸ>­€H»‹òòª¾h�3g¶‹�¨7¥K'$Ù²e P52â–îÝ5Ó¬´»îîü°[7ÇxÅŠ Šˆˆˆˆ4ËÞ½{£½ƒÝÝéq™Ïò‹´F_OO~àç×â>M—NHlÚ´ ¨š3bL` î"¹jz¸»3àbV199™ÄÄDEDDDDšÝ§1»¸0LWÊUÔ×ÓÓѧٹs'MÓe6›�åé©‘rÕEyy9ÝØ±c‡"""""�ÊÈÈ ùâ|Ñ^^êÓH‡èÓTKHHh²|—MHX­VÇÏÝM&Ý9rÕ¹¹¸àãêêøå"""""Ò˜š3Vÿ)ÒQú45ûÜ Ñ*"""""""Ò6*+°ÛíM5(Z�SZªÉ_¤ÃÜ‹""5™<} ŽEl¨…€€DDDjþ^°••) Ò1ú4-¸•�’òòèm±è™+¹ªŽççSz1›("×öíÛÙº-�,[nÿùoÏäéKØàÑŒ¡É—ED¤¶š�lÌÍ¥¯§§‚"WÕ�ìlÇσn²¼Ùlåå|—•¥@ÈU“U\Ìw5Þ¼"ÒqeffrúÄ1òÏŸR0DD¤Ãè¯6¤ôiæåh±ßä1JH\tÒngÇ… „\•7î¦óç5:BDDDD.[yj*ÎF£!WµOà\VÎ-½®kÖqJH\ T%%¾NO§¸¼\A‘vaµÙj%#ªïE‘–È9y’ó‡))!íîxþÿoïÎãªïý�¿“™Ì>Ù& Y&���Õ° !* Ä¥²h­µUŠÚRʽ·Z­Uéí½ŠôÖŸ m5ˆÖJ\+à "bXDHØ$ „�=!“möÉÌä÷ÇdÆ Ù·I&y¿ž§�Í™3gÎ|rN˜Ï{¾ç|µn=MLq1”bqÏzq–ÏQ0Ië@ËŒF|VY‰R½ž…¡Ac¶Ùp¢®‡jk]'®úR1$wDDDDÔ7WΜEõ¹sùû³äGjkq¬®Î­§ llìñóHðµYŸ_€ Z Ç=%ÕÖb_Utœõ€X‘N‡�ËË‘¯Õ:Ž?« c/\„Š÷1!"""¢~ºræ,~øàCˆZopy¦¡�= šˆÀ@€ŸÙŒÄÎ÷º§á,ÎBØíˆ½| (;v¡5f3>®¨À8™ S¡ðóc¡¨ÏŠt:œih€¾Í%Aþõ ˆ).†Ðng�ˆˆˆˆh@Xšš�ûÆNX¦NÁ~~8ÓØÈž†”¯H‹^Ãî=�ƒÐêš>õ4 $®ØØÅ™3(W«Qâ¸Sí%ƒ— „‰ÅHR*¡–ËY(ê]s3.éõ(ÒéÜ‚?³Q¥e½ÎDD£SCEιGÞÖ¯_�ääd…ˆˆzDså éÖÓDK¥'—³§¡3Ûl ® ÂW @Þî=°´Î¨QYÕçm3�è¨(­£%ÆTV¢*"ÂLԘͨ1›!¯¯G´L†qr9‚{x³]'l¹Ñˆ2ƒeF£Ûc~f3ÆTTòò """"t•UÖÔ¹õ4eF#ÊŒFÈëë§P`œ\ÎQÔiOS¤ÓAoµbÅĉ0h4(øàÃnŸ[��Üœ“8øÂ ظq#‰¾[,®`âJX(êT!° ÐÛlÈ×j‘¯Õ"P(DœB�0‰„áÄ(?akL&Wqõž²¦&¨4u "ˆˆˆˆhÈzšªˆ4ºzš3��8ÓØˆôÈH†ä !ª�F\j�ôÁéë?”ÑÝ $zxG—•#º¬šà`hTÁ0´Þ¹¶ÁjÅɆ€\ @´L†0±a‰kX �Lºæf” ¨1›Û�„7« ÖÔ"´æ Ä F4B¤§§cÖõ7ãós , ©äädlß¾ÛfÎêq0a--Ec` ®„‡Á$“Anµ"88ðñq Á§Ñ×Ó”�¨1›Û=îg6# ¡ÒúC@Uçø–Û,¡10uª`˜d2p9ab1Â%Ç¥Roœ¬5f3ª[OÖ¶÷„hB44  ¡�÷‡ """¢aGh·»õ46�¹§Nbc5k$þþ05Ô¾×h ØÓŒPE:Î^D9{š M-üuúÁ=ù+è±Å‚°š„ÕԸ �B�¦ @×:Î{N8 …—J$!L,æ°¨aÎ<Ô[,¨6™Ú]†áäL ZC""""òªž¦­ÆË—Ñxù²ëç¨E7 @§s[Çù¥k Ÿ‚D"ö4^ÐÓŠDn£÷ýd2øŠDÐ_¹ß’ 0ƒ ­þ ƒB0�Äpšr4B§TºFOŽË;ZGO€Ÿ�‚D"×ÿäSÇ! kn†Þju…ºæf4X­�®ïkµA¡Õ:B�Ž—cшTxä($ccÝzš«¿tõóñA´L†¹!!,Ø0êiê-׈îE±c£TÀPW‡êÓgÜB'_D¢!ëiH �Þ•*Y}}a�I¡W*¡U(\÷ž€æ––v'4àI!.‘¸B‹«S-ê½:³z« ÍÍŽÖfC½ÅÒéÈ×Y³ ­R£ ­²Aº~Šˆˆˆˆh8‘�H>Ÿ«¯/tJ%tJ Ri»ž¦€$0ƒöÛ*£>{švQ«Å%½¾Ã{?´uú»c¨++ïôñ¡ü‚•�Ä`Ønw­Ë R)tJ%,"¿v'4àI�Ötëja­3y‰Dùú"ÐÏÏñ_žÜ¨3›Ñl·»Bgè ·Z;¼ßCgáƒÔ`„Ôh€Ôà „v;d""""Õ=M`c£ÛåÉ©ÆÖ¾ÆçR1N~s ò÷GؤIð�Š‚H.ƒ¯@ˆC%%n_:{šp‰ÄýgŽwõ4Î>¦£iY}E"ˆdrm6Ôt0‹_ÛžF®ÕBf¾_¨2�2£±Ý·ìf‘F©F™F© V�o» €+¤è*sžÐ"__‰D¹Py›À çÉxõ{·´†`±Ùº¼¼¢«àÁÏl†Ìh„ÈÒ ‰AïÑk¥ˆˆzüµXeX,Âýý k3d–ˆˆh8ô4WOkoijBÙ‘#®Ÿ­¾¾hž–Ò£žæžØØN_¯T¯GsK‹[Oã͆stCw=MD` ¥R˜šš`¨Õ æÜ9×l(©²è(¯îiH b‹b‹¥ÝMÍ"Ì"?˜drؾÐ*“L»°ã@¡í‰]ÖÃË œ—‰\­m¨1Ú† WëÉå½!1àkµBli†ÈâH }mVDÔ/yyyÈ9›‡ÒÔ)7 úë)T‘˜|˃X3/œÅ'""7%%%ÈÊÊBEB¢Êʆå¥ÅB»‰?œ‡UàÛ®§iû¬¬© W !V(!’Ë wü»'‹á\¹‚jƒ¡ÛžfNH‚[¿ ½š®¹Ùt%T,Ƙ.ÂŽ3 ¨6™ºíÅ®Xœ#@ÓÔÔí¥pþ냎‚¡Pbá…a÷;—�PFD@­V3�)A…³‰Ž¸êqƒTêvrp�àv¡Ðí4]q^&Ò‘²aö‡Í×jƒÄ o­�#là d#/³ ¢ASPP€¯¿ü<HuÆ`0 °°ðWÂÚÚ GΠ¤³ž¦I!‡ÐfÇ•.úmB<ÐÁò«{›Paa‚P,A�¶ g**ºÝß”°pŒ ê<�hsSÈ®4UV¹�q{?c kíÛ„6;¤Fƒ[O#¶4{íÍó£ËÊ1cÉ-˜½z5‰‘îê“»£øq¤…S³H ‹øÇ‘6�†NRÀ®Fcô–sÔB»±ÍIè$o3#ÉÕˆˆˆˆF†ž|ÎO,¼Ð®§Ñ+•nëXDb\þá|§ÁF“B$%uûZ•¹98ùé§�>î7yü:ÝpuOSVYÕé6"*«:ìÛF£„s¤Å�ØÜ‘÷ö4½ýÂÒ_§Gʉ“ýÞ�IgÏñ—1@|Y"""""""ò4DDDDDDDäq $ˆˆˆˆˆˆˆÈãH c:MÎ~ö:^|ñE”””° DDD4b0� ""Ƭf´ÕÅ(,,„¡›ù׉ˆˆˆ†ZYtöà�wÞév]βADDDDDDD • ×AQZÚíº $ˆˆˆz!11:³ jL, )µZ�õë×ãß�<™ÁÈ‚�×a ADDÔ ÉÉɈ˹ƒˆˆ†”\.Grr2èô,y%ÞC‚ˆˆˆˆˆˆˆ<Ž�y """""""ò8DDDDDDDäq $ˆˆˆˆˆˆˆÈã8Ë ½^�¬¬,„„„`ùòå,QȃÇ`Â�k°drÔj5 BDDnŸµÊÊÊ U( 5 ´ÛYò* $ˆhÐþ�ܼy3ÊËËsçÎEHH C^ïã�?ÆÞ½{ÇõýÏúëùIdŒŒCrr8‹ODDnJKK±eË )qùùðçôŸ4 D—•#nùtÌXµªÛuHÑ€+))ÁæÍ›a2™\ËjkkH�p2£Qþþˆ‰‰év]D4 òòò°mÛ6·0‚ˆˆˆˆˆèj¼©% ˜ììllÙ²ÅF„ŽOaQˆˆˆˆˆ¨C!ADb÷îÝØ³g@à'Fì¬[ –âÊÅ\‡ˆˆˆˆˆÚa ADý–™™‰£G�p„o΀B‰†Š"‡ˆˆˆˆˆ:Ä@‚ˆúìê™4dAáHºáH”A,Ñi6`¨¯F~~¢££!—ËY"""x "ê“ÚÚÚvaÄÄ›2F 0}]~ø"/½ôJKKY"""Ö R)*´Z”””t». "êµ’’<÷Üs®0"t| ®I_?‰ŒÅ!""""ÅÊ¢£ðé…BìÚµ«ÛuHQ¯äää`óæÍ®™4“¯Eüü;Y""""“Édˆ��‡¬© B›�!¯Ã{HQ�eggcçÎ�®ŸãæÝŽð„é, �*ééé˜uýÍøü\‹ADDC*&&7nĶ™³X òJ $ˆ¨G®žI#qáÝŒŒcaˆˆˆˆˆ¨OHQ—ôz=²²²:œÖ“ˆˆˆh¨åå塬¬ sçÎåLDD^†�uª£i='Þ”Á›WѰ±eËÀþýûqÿý÷#99™E!ò¼©%u¨¤¤Ä-ŒP†Å2Œ ""¢a«®®[¶lÁ®]» ×ëY"/ÀDÔŽ3ŒpΤ:>…3i‘Wøê«¯�››‹ 6 $$„!ÆH‘›ììldee¹Âˆ¨© 3m C4D$Ê DM]ˆµœ¬‰ˆº:>&m=´5—áã'æßL"/À@‚ˆ\öíÛ‡wß}×õ3§õ$j/;;¿=Œ:½“—<0è¯'Q!fÚ"¤Ï gñ‰ˆº ’"~þ�¨øá0üÃÇâÓ3u˜Ÿà¥dä¶<%%%ÈÊÊBEB¢ÊÊ 3y Ð�Siê�˜�ˆ©óæu». "Ð~ZOΤAÔ1�FƒËEX"¢abýúõ8VÔ£À9ÑÑUk›ñq®)1 LŠ™³o þJX¼= ªº:̈ŒÄìÔÔn×e A4Êéõz¼ñÆ8uê@$ @Ò¢»F‘WHNNFQ³5Zk»ÇšíÀñbJ4f,š±�M;ÑpÂ3’hsNëé #dAᘺ|-Ã"""QªµÍxïû+È+Õàw¿ûvïÞÍ¢ !A4J•””`Û¶m¨««E'a|êƒ6­gVVd2NJDDDC£Ù¼ýÞ¿QWW‡={ö ''kÖ¬ALL ‹C4DH�Byyyضm›G§õ,//gቈˆhH…ÅOƒ¶º†új”——cÓ¦MX¶l–/_Îâ D£Lvv6vîÜéúy0gÒ��APt¬ ?ÔìE„ÅOc!ˆˆhTS¨"qMú:”ä|…òÓ_GK ågT–€hôؽ{7öìÙÀ1“Fì¬[uZO?‰ É‹ÊÂõCCEιGÞpÜI>99™E!"꧘i‹“Œ‹Ùr´ÑâM-‰F‰ÌÌL·0bâ̓FyB~~>®”Á¤­ïÕóœ£%¢¦.t-Û³g233YT¢~(HˆÇk9'ñ /t».GH�pz½Û¶msÌQ ÇLãSïàLDDD4"¼ôÒK€¨© 3mQ¯Ÿßv´„Y×€éóobQ‰<„�ÑV[[‹­[·ºn() ÇÄ›2m& ¢Ñ`Þ¼yPE�ÃñK:ƒˆh„pŽ–h¨(BÎ!j­õ˜Ÿ±px(ONNÆöíÛ±mæ,þÉ+1� ¡JJJ°yóf�ΤA4„„„ ÙO‰Bs‹AD4ÂFÆJë-xïû+˜Ÿ€X•„…!$ $ˆF œœìرÃF„'_‹¸k—²0D^.++ 2™ ÑÑÑX½zu§ë½øâ‹=ÚÞoûÛNËÎÎÆ‘#GºÝÆÜ¹s‘ššÚác%%%ÈÊÊêvݽŸwÞyeeeÝngåÊ•�Þ!¿§ûÒÕûñtmâý TmûXéim‡Ó±Âó°÷µõä±2šíÀ�üF¨ƒŒ˜Ÿ«Ù�FÙ8ˆHQW^<5­' >‰2Èõÿ�—_iMVä”t~Ɉóž1ÝéjçŠ*{´ÿð±�©;ÞNÙ%M�¶ÑÝû9¡U¥—ºÝΩKhܯ}éêýxº¶ñ~ª¶ƒ}¬ô´¶ÃéXáyØûÚö±"ÎHçh‰ª£ï¢àüÎÄAÄ@‚ˆ:²k×.|õÕW3iŒO½ªØ‰, ‘—QS¢©êÇF E†SeúNŸ£ ‹íѶ»ÚF½]Þ£íÔÛå�nG×Ô³}éîý´Èà ³w»�ËM€¦ŸûÒÕûñtmâý TmûXéim‡Ó±Âó°÷µÌcE¢ Bh\Ê ý-Ö6Ö£àüŽ™8rrr°fÍŽ– ê'Ÿ––––ÑøÆóòò°eË@\~>üuz 4ä âað÷G||<6nÜØ«çfffâèÑ£®0bâÍœIƒ†Ô‘7ž¬_¿ÉÉÉ#ê½U6šñù9ÞC‚ˆh4Ñi*p1ûCê«]ˆËh ÞÔ’¼µ§ñe¹ˆ¼›^¯Ç³Ï>ë #dAᘺü— #ˆˆˆˆ�s&ލ© ]ËöìÙƒgŸ}%%%,Qð’�!¶öû〼ݻqà�Ïv»þÜ_ÿ )÷Þ `t$¡Îú´Õb³Á¬ÕB[Y‰ªS§Qøùç¨>s¦WÛ»Õ‹N‹úâb\þö[œ{ï}XtÞ5�_II vìØáº®\‹¤îæ´žDƒ¨¶¶—JªÐX©C@Ä8„ˆh”‰™¶Á1É®Ñåå娴iÓ�Œ–¨­­Å‘#GP� �b‹Åë?÷÷¤ÇéhÝ‘ü™$ã‰abüâÅJº¾��@€Ä%·ŒúZù�"tÂLY½ wf¾ŽôW·A1&¼w¿Ð±�ˆ”ÌyôQ¬zçøGEyU±yófW:>“oy�aÑ ;|ø0ÞØþ2~ø"“Å "¥:-‘�� ½Þ³—�×ÖÖbÏž=¨ŠŒ€YäÇ_ÌüÌï� /àÁiÓ{t :GH ~2â-BÁ'ŸtºNÌÜ9�…¨Fe}Ú¦Ÿ>ÄJ%Tññ·ðz$/_ލ™3qgf&>X“]uu·Û�Hip0¢gÍÂì_®…bL8æ­ÿ5>ûíÆa_�«§õŒšº1ÓñD""""ò ¶£%¢gß‚Z“r9ë2\úoÿÌ?p„Ä0`Ñé`njBÒ²¥]®—´l9ÌZ-,£üœ-6L (ÿþ{|ûâf¼³jjóó! Å�ÚÔãíØ,說�·{7¾øÏÿr4ö^r̾}û\aD�:™aÑqŽ–PŒ‰Ã�üFì?_³ÕΠÞü™4à‰aÀW(DñÁo0þÆ4(ÂÃ;ü†_¬Tbì‚ëPüÍ7»`A§ÛŠž=Sï^�°É“!V*aÑépåüyœ{ÿ}\úú Ûº~r9:ø5ꋊðÎÊUˆœ>3z¡ÉɈĨ¿T„Sÿ|…Ÿ}æþ">>˜¼b&¤§#06V³U§Ná»W_…¦°÷}²ò°0ü}þ|XMæ>í[o說±çñ_áž÷ߨk®A̼y(9|¸WÛÐ\¸à;ì6¯8fV­Z…Í›7Ãd2¡¾4…‡>@Âuwòd""""b¥õ¼÷ýÌO@¬J‚ #Ý}æ—bÚšû›š eD|…Bj5¨8y'3w þÒ%·õçLÁÔ{îÆ˜)S!SÃj6C[U…KàÌ®,˜ÚÏÈ>y2®¹÷^D¤¤@sSªÏœÅ©·ßFʼn£¯æa9ô~~(>t>¾¾H\Úñ(‰ø›o†@$BñÁo ‰:\'åg?Ãò­¯ öºë ‚¯PI` ÔsçbÉ‹/âÚu¿t[ßfv„±ѳgcùÖW={6ÄþþJÄ�0iÏ?‡¸Åîß¾/úÃÓ¸î?6"$9 B©’ÀŒ½~îxí›< ~2Ç8µ¶aDo÷­·Œ μ³ ·¨÷£Â'OT�>íÇLLL 6lØ€   @mQ.ò¾zÍ&O("""¢!Öl‡Ûh‰]»v¡¶¶–…b]}æW„‡cåÛÿDʽ÷"hÜ8%ø …PŒ Gâ­·â®7ßDèĉ®õã-Âÿø;âo¼Š1áðõóƒH¡€*>3þs¬ü×Û�‡…¹½FÒ²e¸ãµ`|ÚbÈBTð ! ÆØëà¶W·aòÊ• $Èó|:›Å‚¤¥·v¸Nòòe°77£øÐ¡W%$`Σ뀖ä¼±ÿºë.ü-u>þyÛí8öÊV´Øí˜¾f�ë$»Õ ɸþÉ'ðÇâÍ¥Ëðêµs°kÕjÔœ=˜²r•ë9ѳg#iÙ2´Øí8¶uÞX²ÛçÌÅûkÖàÊç±ðÉ'ÛÝœ³/ûÖ—:FYD¤¤ô¸îŠðpL¸ýv¤=ÿÌMM8úÿþŸ×7111xê©§ÕzSžúÒ<œû<“¡Ñ0QZoÁËo~Œ¯¾ú Ï=÷öíÛÇ¢ A¯Õ“Ïü3þäaa¨9{d<€¿_·¿n>|ð!\9B‰sÖ­s­í/×ÂG @ÎŽ7ðÖòtlŸ3¯-\ˆOÖÿÚÊJÈCCqí/׺Ö÷�ŽÂõOüp23ÿ¼ýüm^*ÞJ¿ Ç^Ù »Í†Ôßü�±±£ê÷ÃK6†‰f½¥G�b삟2ÅmËÀ±c6i.ûm§SÔLúÉ�ðpþ£�pôå—]Ë›ÊËq23²�LYµÉ·¥£úìY·çJpùÛC8ô?ÿëZVwñ"¾ùŸÿÁ]oî„*1Áµ<ñVG`r6+ '_ݵ¼æì9ì~ôQüdÇø ¶o½ÑTV�ª‚;|¼ó©€¬(øôSœ|=�¥¥^uÜÈårlذ;vìÀéÓ§al¨Æé=¯"é†ÕP¨"yb 1‹­`2™ðî»ï"77kÖ¬AHH‹Ó‹Ïìý}~wŸùý££Ñ¬×cÿ3@Cñe×òªS§pàÙç°ò_o#|Ê�_ :gê8‘™‰æÖÙU,:+.:c}–ýõ¯�µùO^±‘Ç^ÙŠ“™?ÎÔ¥­¨ÀÉÌLøøú`öÚµ˜xÇ8ü—¿Œšß7GH #÷íà ÑVrë|ƾü²ÓçŽiðÃGuø¸söŽ1S§vøøéwÞi·¬®¨ V(\ËÂ'Oäïý¤Ã“üDæë¾o=uŒŽ‘~RiïN¡ã.Ä´ûïo7¬Ê[B‰uëÖaΜ9Ž?„úœû< E<©ˆˆˆˆ†XäÄy˜²ìˆd€ÂÂBŽ–ŠÆ·›Ïü?²ÿ¸~¡[qu_ä'“¹–Õ>ù„[ð8¾¬}}Ñbìyô1ײèY3[û¨½÷DŸ:îÛ9cº××ZŒ“••ÈÎÎîv]Ž�FŠ¿9{s3Æßx#¾}q3l‹ã¾·Þ›Å‚âƒ�ßøÑ?"Âqb\*îðqç £3¦ÃÇ.—´[æ¼Ç||~l~[Oކ˗;ÜNåɜ߷ž+ýæÆÆ¿z øø@ ÿˆÄßt#¦¬Z…±×ÍÇûk2 ­¬ôºã'##‰‰‰Ø¹s'ìÍfœÿrâæÝŽð„é<¹ˆˆˆˆ†�B‰©Ë×¢ôÔTçs-±víZÈ9WhçŸÙ;ÐÕ(Šþ|æ—`òŠ•ˆž= Šðp×½ï|‚v¯óÕÿˆô­[ÓMŸ–†šsçP~ü8ʾ;ŽŠœ´ØÜoœ©ŒtŒ^¾ï“½]¾7ÿèhï$TÁ(­ª„öða¤¦¦v¹.GH #�¥ÇŽA¬TbÜõÆ-à IDAT×¢¯½òÐP”=ÚåtŸÎû6X�Æw.¿úþW?Þçó›;YßÔÔ4àûÖS!IImUUÏžÐÒ£Fƒê³g‘ýÒ|»ù%HU*ÌyìQ¯=†RSSqß}÷AÒZˢá4÷O.¢¤R©3n<”a±,õ˜ŸD†¸k—bÂ�k ðpŒ–Ø¿ß?ÿ†„`éÒ¥¯¨€ØÒÌ"÷ã3¿22+ÿõ/Ìzøˆ˜6 ÊÈH¥RøúùÁÇ·}Û\›—�­X‰œ7vB[Y‰ð)S0ý��þê6Ü·w/&Ü~›ûᅦ£¸EmFaŒ!1Ì\Ü·±óç#iÙ2\øòK$/s\¾qñË®‡t5M)äJ¥®k˜ÜN™´Ë ¡§l–f%bÅ"·Y4œÄJÅ�í›s6�¾N—“¿g®û��P·^úàÍ¡„Z­vM ZvêLÚzN J4€çXÜ䙸ü\‹ADD½‡¸‹PxôSH$¨Õê~ééé(ö9¶ŸŸùç>þ8ä¡¡ÐVVâØ+¯ 2÷L��°77Ãn³aíñïÚm˨ÑàèË/ãèË/#@­†zî\ŒO[ŒÈéÓ±ð÷¿‡ÀO„³ï¾ëêuD ^[xC§÷�8Bb˜¹ôõAØ­VDÏž±R‰ØëæÃf±¸f�茶²×áãAã˵ýÚ?ƒÆ1]‘TÇC‰"¦M’} ?ÞÞ8¯¿ê-gòÙÙ´ªÞÄ9-¨sŽÚ¢\œýìuÎÀADDD4Ä&Œ‘ⱟ¥ã†nÀSO=…iÓ¦±(ÔÙg~ç=ö<ö ?ûºª*X�FØ­V(ÂûÝnci)Îfeáß¿x7ý pÍOïq{Çr„%‰aÌ¢Ó¡ôè1øúù!徟ÁO&CÉá#h6tÝHV´Þ»aâ·wü‡ï¶tÇz9¹ýÚ?MA`ü�ií&¡3Èðø¾ÆÆâ–Í›áë燋ûöASXاí$,¹À�÷´i¡„¶º˜Ó‚ ™È7O ĵqþ }±zõjδ1:ûÌï (ô5WÚ=gÖ/~´´¸zH{þ9ÜÿÙ§k½é[…_|�‡†º–•sŒ°H¹÷gî—zî\ÜóÁû˜½v- Z[ï¸;é®»Zþ²Ûçœ{ï=Ø­6$§§ãÚu¿„tb1ÔjÌ^»n» v« çÞ¿ûÖz�ÛôûïǤ»~q@|…„Mž„导Òáeƒ±o~r9Â'OƼ_ÿ+ÞzþÑQh,-u¥‘=%RÈ¡JLĵ�®Ãü À5¬j$�Ëåxúé§]3pªqòƒ-Ði*x¢ ¢¶_M#Åm)!ˆ³0C 'Ÿùë[oÚ?çÑu�@ !lÒ$,yá!’+ÐT^Àq™¸¯ŸÈBBpã¦Mˆ�Ÿ ‘B�ʈÌ}Ì1»†æÂ…{¢÷߇ÕdÂø´ÅH{þ9¨Õð ! QaÒ]?ÁÍþoÄÄ@¤PŒªß ï!1 t\¶!V*a3›qéà7Ý>§þÒ%þËÌÿío1=#Ó3®©ÐÒ‚Ã[^B}Qÿ¦‚,üü L¼óNDNŸŽÿõ_Xð_ÿåzÌÔЀzw¿÷Þ€ï[wó—?Ž/~÷ÌZmŸ·gÞyyÿþxÄS�J¥8pàìÍfœû<ñ©w@;‘'Ñk¨(ÂÅÃ!iî-øÉMsDxP_?óŸzëŸH{þ9L^¹“W®t-×UUãƒÀœÇ…t4nÜ´ ðÖòtDÏžÿ¨(Üú—¿´{ ›ÙŒ#ÿ÷W×ÏÚÊJ|õÌ‘öüsHX² K–´{Ε¼<|·m+ Zf­eǾCLê<”>Üã0μ³ u.âš{ŠðÉ“!R(anjDÕéÓ8õÏ·Q™“Óÿ�kiÁÞ_ý3|ãÓÒ ƒAS‡Š'ðÝöíÐ×T;V³Ûu߬&3 šZT�:…‚O?Cé‘#}z;6³ºêjT�>�>úU¹§Fìqµzõj¨Õj×´ _¿ÃiA‰ˆˆˆP³É€²Ó_£êüQ@á·ÂÿÖYH ¥ž|æ/üì3Hü1eÕ*(##a¨Õ ì»c8¾ýoÐ×Ôàøö¿!hÜ8¨Æ�‡¶ª ÚÊJdÝóSL]½c¯_EX"ôµµ¨8q9;ßl÷…ëÅ}ûPéR~v/¢f΄L¥‚ÝjE}q1.|þNïÚ{óèš-ŧ¥¥õb˜Q&//[¶lÄåçÿ‹)5©çccq÷ûïÁÜØˆ×§± ½T�ƒ¿?âãã±qãÆAy�œœìر&“ 0fÂŒ›}ë ý£|ñðG°ZLüåŽBÚêbÀúõ둜œ<¢Þ[e£™³l‘程ÞñïƒD"ÁòåË‘–6øŸ‰·ÍœÅ_yeOÃÔ+¡ÉÉ7Ň¾�¥ƒÇyƒJMák˜š6mT*•kZЪóGa5›eZP}]êKóXtQ>þøcìÝ»0÷þgY"¢QîêQ�ŒŒŒA¿i¥ëKÖÓù%+ *M1uÞ¼n×e A½ríº_B=w.ªrOá»W_Å•¼ó°Yš¡ŒŒDÒ²¥¸æÞ{÷ìa±†±˜˜<õÔSغu+ÊËËQ[” }]%&Ýœ?‰lP^3** R©”Åe¦M›6âFG9 娢áJUW‡‘‘˜�šÚíº $¨W¾}q3Ò_݆1)× ýÕm®sáË/‘Ï@bØ Á† °uëV\¸pƆjÇÍ.çß…*rÀ_oåÊ•lL‰ˆˆhÄ0iëqþË®Ÿ=5*‚h$á´ŸÔ+ —/㽟ý ¹o:nÒb5™a·Ú`¬¯GéÑ£Ø÷û§ðåïž`¡¼„\.ÇÆ�ݦ=÷y&§%"""êF°*‰'C"‘`ÅŠظq#â^â ê5C­Gþï¯nÓØ�wËÈÈ€J¥ÂÞ½{]Ó‚Ž�u gà """êÀ„1R¤Ä(`�ðŒF#ƒ¢>b AD€ôôt¨T*×´ E‡?†D](Í=P§ÜÐé:Õ…'aÑ7v»­Ðñ)�(ƒ:|̤­Ç•‹¹ÝnC$èòœuîowºz? EÐÖ\îvʰXFÆõk_º{?ž¬í@¼Ÿ�ª­'Ž•žÖv¸+<{_[O+ž8=Ò<ù×% V%ˆ…rÈårþƒHÄ@‚ˆú+55*• Û¶mƒÉdBÑá�ÐTU<(3p�e§�ÅŒënìt�üK¹¨«,îv[1c㪠íð±+ M®×êJpÄXL™>»ÓÇ�œêY#ÔÕûÑ4”ôh_g-BXRb¿ö¥»÷ãÉÚÄû¨ÚzâXéim‡Ë±Âó°÷µõô±2Øça�Þ «}pþÖ7T!02ÑA"\—±�W½1� ¢A‘œœìºÙe}}=j‹rak6aü¼Ûm"o¢V«Û-»uŠªÓõÏ}&D]¶{mœ?’“;ÞNžßîÁ6‚åÂ.÷åß=|�]mÃzIŠ‚l#!LÚï}éîýx²¶ñ~ª¶ž8VzZÛár¬ð<ì}m=}¬ æy˜ŸŸ�+eM0ù*;…Ñ:M.|û!,ú<ü«ÿÄ5 áüG�¨ R)*´Z””” &&¦Ëu}ZZZZFc‘\söœ³—†�‚„xüý��7é¾èõzlÞ¼åååi`x¯§m¨(rÝ}zýúõœeƒˆˆˆÜÃ? ˆšº1Ó È6Ks¸�Ú˜3g222ØÓ pOÃÃÄÚï�·[Öb³Á¬ÕB[Y‰ªS§Qøùç¨>sfØ¿‡m3gñ:Èårlذ;vìÀéÓ§al¨Æé=¯"é†Õƒ2-(ÑPsŽŠ06T»–-]ºéééÃre2âããQqò$„6û�ï{ê-^5Œù�"tÂLY½ wf¾ŽôW·A1fà†‹MYµ²Ã?DÎPbݺu®iA-úN JDDD#RiîœÙóª+ŒˆŠŠÂ“O>9lȉ‰ÁÆ�‘Xx2£‘= yŽ�fÚ&q>ÄJ%Tññ·ðz$/_ލ™3qgf&>X“]uu¿_/tÂ�º•‘‘µZ�wß}öf3Îìyqónç DDDäõ¼mT{ö4# GH c-6L (ÿþ{|ûâf¼³jjóó! Å�Ú4 ¯šÌ“—z&-- ÷Ýw$Ç4WE‡?BŹÃ, yµº’<¯Áž†=ÍHÂ^DWU�=�ÿ ÷¼ÿÆ\s bæÍCÉa÷†PˆikîGlj*”ð a¨Õ âä œÌÜ�úK—ÓÖÜ�9�>êzžsˆÓ—O<‰ _|Ñãít&zölLÏXƒ�¤$Åb4––!ÿ“OpúíÂnµõiŸ�§LÁÔ{îÆ˜)S!SÃj6C[U…KàÌ®,˜ÚíOøäɸæÞ{‘’I`ÌMM¨>s§Þ~'Nðàê¡ÔÔT¨ÕjlÞ¼&“ —¿ÿ úº*N JDDDÞÙ ù÷¯¾oh ‘’’ ‚=MŸzš¾¼{^ɨÑàÌ;»0ã¡·h‘ÛÉ«Ç�™¯CæöŘp$Þz+â-ÆG¿ø®üðC—¯ÑŸíØ­6Œ]°K^ø_ø®åÁñã1÷ñÇ>y>ÿ�ÿìókÅ-Z„›þûOnÛùùAU|<&Ü~;Þ¿ ô55®Ç“–-à OýÞí9Òà`Œ½~Æ.¸‡^xg³²xpõPLLŒëf—ååå¨-Ê…Y߀¤…«9-(y�è ®K€Xè‹§Ÿ~šaOÓ§ž¦/¯ÃžæG¼dà ]:x‘’â¶|æÏ‚<, 5gÏ჌ð÷ëàï×-À‡>„+çÏC(cκu€œo¸]Ûµmæ,l›9 ¾ø¢WÛi§ÅŽëþc#Îÿûßø×]waûœ9ØqãMÈ~i Zl6Ä-Z„Øù©}Úg¸ö—ká# gÇxky:¶Ï™‹×.Ä'ëme%ä¡¡¸ö—k]ëûGGáú'~8™™‰Þ~þ6/o¥ß†c¯l…ÝfCêo~ƒÀØXX}%¢¢¢Úêbœû<Í&‹CDDDÚи!)i‚ ²bOÓ¿ž¦/¯Ãž†�„Wk*+HUÁnËý££Ñ¬×cÿ3@õ™3°�°�¨:u ž}>er·ÛïÏv|ýüPuú4þé¿ÑP|v« Æúzœ~ûmä¾ù aÉ-}~-ÿÖøDf&´••°[­°èô¸|è¾øÝï`nj‚,$ĵþä+!‰ðÝ«Ûqì•­h*+ƒÍb�¶¢'33ñýßÿ_¡︃V/9§uÎÀal¨ÆÉ¶p"""vJs@§©@t�+f†"V%aQØÓ HOÓ—×aOó#^²á…š�Žo¡ý¤R·å?²¶ÓçÔ9ž#ë~H}·söÝ÷:\~aß—˜¶æ~„MœÐçת/.†*! Ÿ|Ù/m�¡¶ÖõXÍÙsx}Ñb·mDÏš Èß»·Ã×(øô3Ì^»‘38[D_C‰ŒŒ ÀÑ£Gao6ãÜç™HZx7#ãX """tK—.Ea�‚À˜v�µ�AÃR��G—ÿaD½÷’’dee¡"!QeeÃvêÏ‘ÜÓôåuFzO#3¡Œˆ€Z­f 1‰•þscc»Ç$�˜¼b%¢gÏ‚"<Ò  ø …n×õD¶£),ìpycI©ãm“öõöµ¾ú㑾u+âoº ãÓÒPsîÊ�GÙwÇQ‘“ƒ›û 3•‘‘€û>ÙÛå>ûGGóÀꇌŒ $&&bçÎ�°7›qþˈ›w;Äò@‡ˆˆˆUzz:>9£A�Öê¶¼4÷ÊNpýì'ðAmm-B®ú,êÍ  %¬ïü>’zšÞ¾ÎHïi¢ËÊ1cÉ-˜½z5‰‘($)  ­ªjw ÞñÚ?  í×öû»�fCÇ÷°šLŽƒN,îókÕæåã_+Vâš{îÁø´ÅŸ2áS¦`úÀP«Áw¯nÃù�þýã?û@‚7mBìüTˆ øPFD`îc�4.¸¶îý÷a5™0>m1Òžj5|…BÈBT˜t×OpóŸÿ11)<Øúƒ�\ާŸ~+V¬À#�<Â@‚ˆˆˆ͵ãü±~ÝϱtéR<ýôÓ¼Dƒ=�Çzš¾½{WˆÔÒÒÙŒ2yyyزe‹ãàÈχ¿N?¤û³öûã=Z¯üøq|ñ»'`jhh÷XÂ’%H{þ¹vËuUÕøà�0ç±G‘xË-®åÛfÎÂmÛ··›fßïŸêõv|…<|ô(,:¾|âIÜòÒKð¶¿«lÞîÝ8ðÇgû¼Ïo-OÇOÞØipp‡õ±™ÍØóØã¨8yÒµl|ZÒž®ÓôóJ^>~äX†ø€‚„xüý��7ò_/""""bO3Œ{š¾ì¯2"bD÷40㡇0û‘‡»]OðÌ3Ï<3OÞÚÚZ=z¤Ñ@liÒý™õ‹_t¸Üj2CW]�Ëß~‹#}Ç_ÝîJ¯VwáÌ��P«!’Ë¡¯¹‚¢¯¾Âþ§ÿ}M 4……ˆHI�4 M8³kªÏžAHB¤AA°šÌÐâÛ_ìõv„)¦gdÀÔØˆoþügTž< EX8ÄþJø¨¿t '33ñݶW]Öú²Ï'þñ >ý -V$ŠDðññ�®¦Åbÿ3Ä•~p«K}Q.ø~R $þþŠÅ°Y,Ð\¸€Óÿ|_oúS§5õ4�*Íb1‚ƒƒ‘ššÊe‰ˆˆˆˆ=Í0îiú²¿�nD÷4eÑQ¸hoAu}=&OžÜåº!�á‘&!ADDDDìihôô4¼‡‘R«ÕX¿~=âòó!3Yò: $ˆˆˆˆˆˆ¼�\.Grr2üuzív„¼ """""""ò8DDDDDDDäq $ˆˆˆˆˆˆˆÈãH‘Ç1� """""""�²DDDDDDÞG¯×£¬¬ Z…Rƒ�3m�×á """"""/TZZŠ—^z “a�IY¢Ëʱd||ŽdOCÃCQ\ŽÖ×!''§ÛuGm ‰D¨S©xÔÐ�3H¥0Éå€iÓ¦± DDDDÔ¥äädWOc”IYr h Ĺââ­?ª/ÙX¼x1 )( “‡‡cÖÿÀ„ôtäää 11±Ï#½HtB¯×cÿþýØ¿¿ÛIìkµ"X£Ah͈-Š:¥ F�J½¿Òmy||<–/_Žääd‰ˆˆˆˆ<ÞÓø™ÍPiê¤Ñ°§¡.5ø»¬ˆiÓ�´|& àMøHôð$>|ø0êëëÝ“èõÖÔ! ±‘'2¹NØÆÀ@4º�†DÑÐö4W0öÂE66²HÔiOscp0®¿ÿ~„BÃ@¢²³³qøða·{L89à …N™ÑÈb�V__蔊NC‰D‚¹sç"--�—fѰèiöïß�òòrˆE"¬ˆ‰Å¥¯¿†¾ºšÅÅ=Mc` «¯¹º§¹á†°zõêAym}P[[‹}ûö¡  åååí÷µZÐÐ…V…NÇÑ#ŒV¡€^©tü÷ªË1œ¦N�Š””¤¦¦²`DDDD4,{šüü|×çÕ+yyÈÛ½'N ®õ ؆€Øö4#T“BŽŠèh˜:¸ÿƒD"AJJ RRRút³J<‘sssqøðáà Àq�–B«…Ô`ä /ãa’É» Úž°ý¹© Ñ�7ªåå(?qÛ?ùÆÖvÑÙÓ(´:H�Fö4#€_J Ž |Ûõ4‰‰‰ûb•�Är¦ŒÈÍÍmw}V[ò&-”:üÌfžÐÃ(|0ÊdÐ+•0‹ü`”J;L �¢¢¢�˜˜ˆ””Þ‚ˆˆˆˆF½^�'žx¢ÓžÆ×j…Ô`„R§Ã˜ÊJlö4©Á�ë!Ó¦!rÆ „$%"rÆ Hüýñì³Ïº¾X�‰‰ñøþ2�D%%%(((p…]Î�Bd1Cli†\«…ÀfcP1ˆ'©E$B³X ­B‹Øñÿ»â Ôj5RRR8 ‚ˆˆˆˆF}O#iiÁ”ü|4ë �~öÚí,ä Ð*Ýö4S"#±jÕªA¹!%äòÎcIIDAT /ãAQZZŠÒÒÒoŽÙ‰^�Í¥Nç.´ZpýLí™E"XD"ؾ0Éä0‹ü`‰a”IÛݤ¥#AAAP«ÕP«ÕHLLä""""¢Ö€ÂÙÏ@¥Raݺu055A“_€ò'P[�‹V‡Êœ$ÄÃàïïÖÓøZ­��ü¶P?¾Ûž&%%eÐnHÉ@b„œÐ�¥¥¥ÈÏÏGYYY·#)®&orR£B›Ý-´i'¹stW©6� G#®µZ •JåÁÑDDDDDý÷«Ç‡Élîr_«SO�55iÛÓ8¿@í¨§‰Ëχ¿N?¹ !‰IPFF@‰�¤DÔ™ÌøûGº¶���¯ëiH SyyyÐh4®°Â`0ôxDEO §ŽFY8ÃŒÎô4äh{¢u¦í èäÍàÔ—�ájAAAP©TP«Õ�ÉdHLLDHH§â$""""DûöíƒÁ`@~~>ŒFc§<8m:¸FW@eNŽã¿cPé /¤c‡½‰CC§}JÛÞ¤?_ÚjŠ—ë•î7¿ïê¾mßOWÒ,ÀŠŸþ´ËžÑÛ{^V@AA ?? ÑhP__?*k©TêJ�ÿcè@DDDD4¼èõz”––Âh4¢´´�žžÞéúÿøÛßpüĉn·;ËfGˆ��[˜áÔ¤�£()©ÛmÈšš�XØùÁÎKPº³rÌ(#~ B’!V8B‹CgNãÀ±cÝö4£áÒq#TII ƒ[pÀ5Ú©/—ˆx*\prŽjà œËyiÑÈ–——çÖÓ8¿�urŽ$_¿~}§ |^^¶lÙÒíkÅ„‡cÍrG8"R*ÚÝ ò…^èÑÈõM›6uúÅhÛ^m´�Ê@‚Ú©­­Emmm·ë9/)éHbbb�^‹7‹$""""¢Áæ•Ñ]/£R©�ššÚévÚ† N•Ýw $ˆˆˆˆˆˆˆÈã|Y"""""""ò4DDDDDDDäq $ˆˆˆˆˆˆˆÈãH‘Ç1� """""""�ûÿ¸*�̾�ZIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2523568 ceilometer-27.0.0.0rc1/doc/source/contributor/architecture.rst0000664000175000017500000001535415250267452023171 0ustar00zuulzuul.. _architecture: =================== System Architecture =================== .. index:: single: agent; architecture double: compute agent; architecture double: data store; architecture double: database; architecture High-Level Architecture ======================= .. The source for the following diagram can be found at: https://docs.google.com/presentation/d/1XiOiaq9zI_DIpxY1tlkysg9VAEw2r8aYob0bjG71pNg/edit?usp=sharing .. figure:: ./ceilo-arch.png :width: 100% :align: center :alt: Architecture summary An overall summary of Ceilometer's logical architecture. Each of Ceilometer's services are designed to scale horizontally. Additional workers and nodes can be added depending on the expected load. Ceilometer offers two core services: 1. polling agent - daemon designed to poll OpenStack services and build Meters. 2. notification agent - daemon designed to listen to notifications on message queue, convert them to Events and Samples, and apply pipeline actions. Data normalised and collected by Ceilometer can be sent to various targets. Gnocchi_ was developed to capture measurement data in a time series format to optimise storage and querying. Gnocchi is intended to replace the existing metering database interface. Additionally, Aodh_ is the alarming service which can send alerts when user defined rules are broken. Lastly, Panko_ is the event storage project designed to capture document-oriented data such as logs and system event actions. .. _Gnocchi: https://gnocchi.osci.io/ .. _Aodh: https://docs.openstack.org/aodh/latest/ .. _Panko: https://docs.openstack.org/panko/latest/ Gathering the data ================== How is data collected? ---------------------- .. figure:: ./1-agents.png :width: 100% :align: center :alt: agents This is a representation of how the agents gather data from multiple sources. The Ceilometer project created 2 methods to collect data: 1. :term:`notification agent` which takes messages generated on the notification bus and transforms them into Ceilometer samples or events. 2. :term:`polling agent`, will poll some API or other tool to collect information at a regular interval. The polling approach may impose significant on the API services so should only be used on optimised endpoints. The first method is supported by the ceilometer-notification agent, which monitors the message queues for notifications. Polling agents can be configured either to poll the local hypervisor or remote APIs (public REST APIs exposed by services and host-level IPMI daemons). Notification Agent: Listening for data --------------------------------------- .. index:: double: notifications; architecture .. figure:: ./2-1-collection-notification.png :width: 100% :align: center :alt: Notification agent Notification agent consuming messages from services. The heart of the system is the notification daemon (agent-notification) which monitors the message queue for data sent by other OpenStack components such as Nova, Glance, Cinder, Neutron, Swift, Keystone, and Heat, as well as Ceilometer internal communication. The notification daemon loads one or more *listener* plugins, using the namespace ``ceilometer.notification``. Each plugin can listen to any topic, but by default, will listen to ``notifications.info``, ``notifications.sample``, and ``notifications.error``. The listeners grab messages off the configured topics and redistributes them to the appropriate plugins(endpoints) to be processed into Events and Samples. Sample-oriented plugins provide a method to list the event types they're interested in and a callback for processing messages accordingly. The registered name of the callback is used to enable or disable it using the pipeline of the notification daemon. The incoming messages are filtered based on their event type value before being passed to the callback so the plugin only receives events it has expressed an interest in seeing. .. _polling: Polling Agent: Asking for data ------------------------------- .. index:: double: polling; architecture .. figure:: ./2-2-collection-poll.png :width: 100% :align: center :alt: Polling agent Polling agent querying services for data. Polling for compute resources is handled by a polling agent running on the compute node (where communication with the hypervisor is more efficient), often referred to as the compute-agent. Polling via service APIs for non-compute resources is handled by an agent running on a cloud controller node, often referred to the central-agent. A single agent can fulfill both roles in an all-in-one deployment. Conversely, multiple instances of an agent may be deployed, in which case the workload is shared. The polling agent daemon is configured to run one or more *pollster* plugins using any combination of ``ceilometer.poll.compute``, ``ceilometer.poll.central``, and ``ceilometer.poll.ipmi`` namespaces The frequency of polling is controlled via the polling configuration. See :ref:`Polling-Configuration` for details. The agent framework then passes the generated samples to the notification agent for processing. Processing the data =================== .. _multi-publisher: Pipeline Manager ---------------- .. figure:: ./3-Pipeline.png :width: 100% :align: center :alt: Ceilometer pipeline The assembly of components making the Ceilometer pipeline. Ceilometer offers the ability to take data gathered by the agents, manipulate it, and publish it in various combinations via multiple pipelines. This functionality is handled by the notification agents. Publishing the data ------------------- .. figure:: ./5-multi-publish.png :width: 100% :align: center :alt: Multi-publish This figure shows how a sample can be published to multiple destinations. Currently, processed data can be published using different transport options: 1. gnocchi, which publishes samples/events to Gnocchi API; 2. notifier, a notification based publisher which pushes samples to a message queue which can be consumed by an external system; 3. udp, which publishes samples using UDP packets; 4. http, which targets a REST interface; 5. file, which publishes samples to a file with specified name and location; 6. zaqar, a multi-tenant cloud messaging and notification service for web and mobile developers; 7. https, which is http over SSL and targets a REST interface; 8. prometheus, which publishes samples to Prometheus Pushgateway; Storing/Accessing the data ========================== Ceilometer is designed solely to generate and normalise cloud data. The data created by Ceilometer can be pushed to any number of target using publishers mentioned in `pipeline-publishers` section. The recommended workflow is to push data to Gnocchi_ for efficient time-series storage and resource lifecycle tracking. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2533567 ceilometer-27.0.0.0rc1/doc/source/contributor/ceilo-arch.png0000664000175000017500000025363015250267452022472 0ustar00zuulzuul‰PNG  IHDRöâ”…ÛbKGDÿÿÿ ½§“ pHYs  šœtIMEà íf½Œ IDATxÚìÝw\SWÿð3!�€,ND÷´uÔÕ‚ÚÖ:­Úa­µ¶îö×jût÷±ŽN­mÝ­Õî½À:±‚¬ƒÖ°IHã÷G$I @@Æçýzù2¹÷æ{ï9çæÀýrï9………… """""""2#KV™DDDDDDDdvL8‘Ù1á@DDDDDDDfÇ„™�µ±iiiHKK+7€——D"Q•b¸¸¸ÀÅÅÅà:¥R‰ÄÄÄ*Å€¸¸¸rcØÙÙA*•Vk ™L†œœœrã´iÓ¦Ê1ÌÑ6eÅ0GÛÔµö5WÛ”£&¿{µ¥}ëÛw¯¶´/ûVö­ì[Ù·²oeßʾ•}+ûVö­ÕÙ¾å�ïF‘‘‘ˆˆˆ(÷gÏž __ß*ÅFHHˆÑŠZºti•bÀ’%KÊ�áãヹsçVkŒÐÐPÄÇÇ—gåÊ•UŽa޶)+†9Ú¦®µ¯¹Ú¦¬5ùÝ«-í[ß¾{µ¥}Ù·²oeßʾ•}+ûVö­ì[Ù·²o­îö]°`�ÑŠÑ„ƒB©4é$NHJB¡¥•Áu©ié&ÅHMKÇõø›†>)©Ê1L¥ÌÉ©öJ²LÌÃmSV s´M]k_sµMY1ÌÑ6u­}ëÛw¯¶´/ûVö­ì[Ù·²oeßʾ•}+ûVö­ÕݾÑÑÑF…………�/\¼x1âããáêæ†~”ÜÝÍ¡Ðຬ¬,df>(÷� ‘H ®S«T�§È«C›±J(7†P „›»{µÆH‘Ë¡R«Ê�#•zW9†9Ú¦¬æh›ºÖ¾æj›²b˜£mêZûÖ·ï^mi_ö­ì[Ù·²oeßʾ•}+ûVö­ì[«³}»tê×2©(3áàéå…ÑcÇ�ˆˆˆˆˆˆˆ¨$©§ÄFÆÖ8KU&ˆˆˆˆˆˆˆÈì˜p """""""³cÂ�ˆˆˆˆˆˆˆÌŽ """*W»6>hׯ‡uÐÀ뀈ˆ¨"¬YDDTÛäææbwD8Ž9ŠØ˜+ÈÈÈ@aa!œ��ÑÞÏ@pÈpØØØÔé2îüc8€D™ )))Ðh4ptr‚�O 6 #_xÁ`7nX�/>û ×ââdù‰ˆˆèÉ;‰khß¾=|}}™p "¢ÚïÒÅ‹˜;{’““K­KNNFrr2<€Ÿ~üK¿ý;vªse|�‘�W'Oµk×J­KMIAjJ ¢"Oc×®�ømõØÙÙém[§Û¸ªåRêr‚‡ˆˆÈÜ¢NŸÖ&¬­+–p�J¥Ðh4prvf-Q�9þ^�4 �Íš7Ǥɯ �›4�……þ½w‘‘‘X·f50qüx¬Û¸�:u®SåüfÑÿpíÚ5´nÝÓ¦¿�nÝ»ÁÙÙ………HNJ¡C‡°â§qñ¬Yµ o½ý¶~Â!6¦N·sUËODDDuƒEaaa¡¡)©iHËÈ` Q�P«ÕòÌ@Ü¿Þ}_/Z[[[ƒÛªT*Ì›3‡€‡§'öî? ÛV©T¢[çNpwoŒã§Ná÷mÛ°vÍj$&&ÂA"Á€1{î\8::Œ�µ«WáÂ… È|ð‰�:wƤɯ g¯^zÛí«y‹Ø»ÿΟ?‡å?üˆ«Wc¡R©ÐÚÇ“&¿‚�áÃõ>Ы'dd`ßÁChÖ¬™Áã8~ìþï½ùèÓ·/-^غe >ùø£RÛnܼݺwÇùóç°yã&üý÷e¤Èå(,,„««+zôì‰)SßDëÖ­K}öLTÖ¯[‹¿/_FVVš6mŠ‘Ï¿€W_�@·]ÑØ†þÊÿÉÇaë–-ÄŠ_~Ñûœ!•-eÚI¡P {—κ6ús×.¬Y½ ÿܼ‰¼¼RADDµÂ®�àþýûhÙ²U™É …øfÉŒ ÆÝ»w±gwF>ÿèžùÏÉQbÇï¿cá‚‹“))غW®ü�­Û¶—ÚÇÎ?v`á‡"??_·,-- GÆÑ#GðáÂ…?áåâ¢ÖÚ£ªœDEFbê”סÑhtëccb0îØÚÚbÈСºåy�¶q”HŒ–ñé~ýù×Y“ë/ìÏ?ñþüyxüï÷îÝCØŸâÀþýX¿q:tì¨[·níZüï«/õ>“��€ï¿û‡¦-[ËM,[²[·lA×nÝðãŠånoŽòW¤�ŠÎUNÂÃÂðþüyz±òóóqøÐ!Œ=ºÔ~Ž>Œ¼¼<õé£ÇZ™z¬è¹v`ÿ>Ìž9So{�F¥R ™L†ˆð0,ýö[ <„� Õœ¥‚ˆˆj…cG�&½2¹ÌdCɤÃÄI“‡Ö-×]`ªTøé‡ïñþâØÉ“¸ô÷|ÿÓO‹Å¸võ*ÂÃÂôâ%$$à¿iïxcê›Øwð¢¯Äààá#˜9{6¬­­ñõ—_âö­[¥ö¥P(ðÑ‚5z4Ž?�+W¯!,b·n|‰M7èíË¿CÀ  T*M®£±ãÆéýuýZ\<®ÅÅ£[÷îØµs'„B!fÍžƒ£'NârL,Î_ŠÆ¦-[áçï•J…eK—ê>�oþ÷5¬­­±ðãÿ"꯳¸xùo¬ß´ -[¶BlL ~^¾¼ÌãY³z~Yù3:t舕¿þ‘HdR9*[þÊ´SQRH¡P`Ù’ÅxqÔ(ì=pW®^ÃGÿýD{1`¿Á}د]lä¯6•­ÇÊœkß-ûùùùxý�7pèÈQ\މÅå˜Xìݯ¼úlü±c;""bÂ�ˆˆèq×®^ô4ù3�AA€Ø˜+¥Öi4<3h&MžŒÆ�›@(bРÁ˜6}ú£$ÅA½í7oÚˆÜÜ\Ìx÷]Ìš3Íš5ƒ@ €—TŠ©oNôéÓ‘——‡m¡¡ºÏXXX>|ˆî=zà£�ÿ‹¦M›ÂÚÚ>mÚ`áÇn\¿®·¯Ysæ@$áÐÁèÿT_,øðìÙ�û÷ÿ­tý­^»/ÿ�7Þ|Mš4�­­-Äb1ºvë†Ï¿øpùr´nû­›7ë.`ÿ3~<œ5‚��zôè‰ÅË–ÂÎÎ.œ7º¿ß·oÇ¢¯¿†oÛ¶øuõjØÛÛ›|¬U)EÛÉÒÒR×F­Z·Æç_~…æÍ›ÃÚÚ!ÇC àLT²²²ôö“���ÈÓ§ ðÌ ÁF�§2õX™sM&KL}s<½¼`kk [[[4oÑóß.EcÅÊ_Ø‘DDD�Ëx4nPãÆ�MþLS@zzºÁõÏøËôSO÷ÄÅÅé-ÿ+ê `ÄÈç Æ ÑŽÃpîœáÛü_ž8±Ô²Ö>>º‹Ý’:vì„Í[C„¬¬,ìØ¾sfÍB¿¾}1tð |õåˆìøª¢è8” …nYQ9†VjûvíÚkÿJ¿q“ÁxöïÃ?Zˆ–-[aÕšµFÇÃ0¦*å¯J;MšüŠÞ{{{{<ݯ4 Ž>¬·îØÑ£Ðh4xº_¿2“)•©ÇÊ”¡e«V€�,€üþ}vDDT'p ""ª¬¬¬&¦èyö¢Ï>®eËV¥–¹¹¹ÒRSõ–'%%ú?Õ·Ì}Ê .oÖ¼y©eB¡`h|fß¶m±jÍZ$%%áä‰8î,Ο;‡»wî`ýڵذnþ3~ÞÿàÝcå&mÒÓ±yÓ&œ‰ŠÂ½ï!#=�Fï¹]yµå•z{W¨�"OŸÂÜÙ³aaa�_V­*slƒ²T¶üUi'CSv‡„àÀþý8p`?F<ÿ|‰¤ŠöqŠç‚ƒËÜOeê±2eøò«¯ñêäIØ»g7öïÛ‹;¢wïô@÷=L>Gˆˆˆjïp "¢ZÁùÑ…kRR’ÉŸ¹w/Œ^ôŠÅb£Ëòòòô–›:–€¢Ä]åíËžžž;n/]†c'Oaï�ƒ˜6}:6mÜ€µkÖ˜'Q&ÃÈáÃñãßãüùsHJL„R©„F£1˜ÄQ©T`Òx%͘>�yyyغes•Û½¢å¯J;¹¸º–Zöt¿þpppÀéS§ ÈÎÖíãÔÉ�î€è_æ~*S�•)C{??„ïًצL�‡‡'.GGcåÏ+ðʤ‰èÿôSø}Û6v"DDTëL‡‡††âöíÛplÔý d-QµëС’qúäIƒÓ7 èÔ¹³Ñ‹ÁÇ2Ì~ôxÃã�ˆD"<|ø�¿I³'T·æÍ›ã�wgÂÏÏo¿5 ;wìÀëS¦”û¹Åß,‚\~žž˜5{ºtí '''ØØØÀÚÚ~mõÿºogg…B�Tè.…FÎÎøzÑ"¼?>VÿöƒXcå¯J;�çP’­­-ž4;ÿøÇ�óÏãä‰ÈÉÉÁÈç_(wÖ�ÊÔceËàææ†¹óæcî¼ù¸sçNŸ:‰}{÷âü¹sX¸àCäææâ?&°3!"¢1j̸»ºÂËÓÓøÏ^C e2nß¾�¹œµHDD5â™Aƒë×­Õý¥¹,*• 7hg<ÄðT€ wï–Z–’’ øŽŠ"ÞÍšnß¾U­åLNNƾ½{Ëý+wŸ¾ÚÛí‹n¿/Ï™3Úq~ým‚CBàéé ±X [[[Ü»w¯Ôö^R)àÖ­*tüÛ~ß�Aƒ‡àÃ…¡  ïÍŸ‡ #chTGù«£�Šf¡(zŒâà�òg§¨J=š£ Í›7Çø /cÃ¦Íøä³ÏëÖ®aGBDD5F*õFëÖ­ËL¸ó‘ ""ª† †æ-Z 99³gÍ4úèäææâÿÞ›�»wîÀ·m[£³ìÙ³»Ô²ãÇŽZ¶l©·¼è¯ôkV­2ëä‰:x¾[¶¬Jåœúúk˜õî;øuåÊ2·»xá I“&F·)96ƒ&7€áA7úá{½º€îÝ{þܹ³Ôöׯ]Cçþ;ú¥Rëœ��/¼ø"ž}.)r9>ø¿÷k¬üÕÑN½zÀÕÍ 'OžDNNN?ô(÷³•©Gs—áÙç´ãLÜç`’DDTË0á@DDµ‚µµ5–,û"‘'ŽÇðçžÅ†õëpûÖ-äää@¥RáÎ�;زy3†?÷,öíÝ ,]ö­nzÊ’lll°;"›7mÂýûÿB¥Ráà�ýøùçJß1vì8ØÙÙaÿ¾}˜7g6îܹƒÜÜ\¤È娲y3f½ûîÞ¹SjƉŠz}Ê€ŸW,Ç{óçáüùsÈÌÌD~~>²²²pýÚ5ü¼b9f¾3ðâ¨ÒýE·ù_¼pAw<Í[´,]²ééP«Õøûï˘1}:233!}ô—øƒö#77£ÇŒ�¥¥%þر+^�ôôtäääàÜÙ³˜;{Ôj5ºtéZfY>ùì3xyIqìèQ¬_·®FÊ_ídee…aÆA©P`ͪUxøð!†{Öè`¤%U¦+S†ysf# WO,þfîÞ½ µZ�¼¼<$%&bÉ7‹mÚø²#!"¢ZÅ¢ÐÀÐÙ‹/F||<<½¼0zì8ÖÕ˜«W¯bö»ïà®�Ç!Jjݺ5–}ÿƒÁñÚµñ��� >ýüsüß{ï•Zßµ[7¬ß¸©ÔåÞ={ðÞ¼¹Ðh4÷ÙÞÏk×o€ƒƒƒÞ¾àZ\¼ÁÏZÿݷ˰rÅ ƒ³W”4|Ä|ñÕ×¥f xñù‘¸«{-.áaa˜?wN©�7ÆæÐmXºø쎈Ðû̪ß~ÅâE‹ î»U«Vظe+œœœÊ,çåËј0n,--±uûv´k×¾Ü6®jù+ÚNåµQQ9ƾôDb1” 6o E—®]ËmKªÇÊ–aÏîÌŸ;×àŒ#EI¨_W­F�ž=Ù‰Q�‘zz@üØxYL8Q­–——‡ð°?qäÐa\½‹ôGc8»¸ C‡4xp™�.ya†Õ¿ý†Û·oÁA"Á Aƒ0gî<ˆíí ~6>.«W­ÂÙ¿Î %%666hѲ%ž}î9Lxyb©Ù*“p€«±±Ø¾-/^Dbb"T99 …hܤ :vì„‘Ï?oô–þsçÎâÃ÷߇\.GË–-ñÇŸa€�ÖcㆠHNJ‚³³3zà�wgÂÓÓwîÜÁœY3ì;x€ööýuk× æÊ(•J4iÒƒ‡Å›ÓÞ‚}‰:*«œ¿üü3–-]‚–-[aû”¨Ó`b© å¯h;™’p€!Ï DBB<½¼pèÈQ“Û²"õX•síüùsزi3¢/]DFFòòòàæîŽž={áÕ×_‡��;""bÂ�ˆˆ¨:™z�IDDDD•W^ÂÁšUDDDDDDDDq&*×о}{øúGˆ """""""ª�¨Ó§hþ6–pà,DDDDDDDdvïp˜;w.RRÓ�–‘Á"""""""¢ 3ù‘ µJ…”””r·“8:B"‘]Ÿ(“•C ÀÍݽZc¤ÈåP«ÕåÆñz4oyUb¸¹¹A \—••…¬ÌÌ*Å0GÛÔµö5WÛ”ÃmS×Ú·¾}÷jKûÖ§ï^]é[ÿ:Y™™å–‰}+ûVö­ì[Ù·ò÷Vö­ì[Ù·š·m*•p�§Èñ{hh¹ÛõD@P�ÑõÛC·–£¼Ù1Ìãè‘ÃHJL,7ά¹óªcÔ˜1�J½ ®‹½rg¢"«ÃmS×Ú×\mSV s´M]kßúöÝ«-í[Ÿ¾{ì[Ù·²oeßʾ•}+ûVö­ì[Ù·V9áàæê7WÝ{‹‚|“ºº8£­OkT…ÈήÚcˆììLŠcŽÞžžð5'þúµ*Ç0GÛÔµö5WÛ”ÃmS×Ú·¾}÷jKûÖ§ïûVö­ì[Ù·²oeßʾ•}+ûVö­%uîÜÙh<‹ÂÂÂBSv¬T*!3á¶WWW¸¸¸]ãÆ�ò^$‚´Œ[JÌC&“A©T–ÇØh›‰!•J!227iZZRSS«ÃmS×Ú×\mSV s´M]kßúöÝ«-í[Ÿ¾{ì[Ù·²oeßʾ•}+ûVö­ì[Ù·šú=39á@DDDDDDDd*N‹IDDDDDDDfÇ„™DDDDDDDdvL8pøðaDGG³"ˆˆˆˆˆL†O?ýÔ¤©á`Â�è1k׮ŶmÛ°bÅ DFF²BˆˆˆˆˆÊPPPŸ¶m‘›«ÁâÅ‹™t &ˆJX»v-¢¢¢tï×­[Ǥ‘¸›˜ K¼4f “¤‡ÓbV€R©DTTrrr*üY///tîÜ™•X‹•L64iÚ22 R©“&MB`` +‰ˆˆˆˆè‘ÐÐP4kÙNN�tËÔ*¶‡†bäÈèÞ½;+©�³f˜.::Û¶m«Ôgíììðí·ß²k©Ç“ SÞœ†øõçP©TX·n0é@DDDD`Íš5¸té<¼¤zËB!&LšKKK¨ÕjVVÆ„C¤¥¥é^ûøø˜ü™ôôôJÝA5ÃP²ÁÎÎvvv˜òæ4&ˆˆˆˆˆJ(J6¼4f,ÜÜÝ n£}Ô" ͼ<‘ŸŸ‘HÄŠk€˜p¨¤¹sçš´]xx8"""Xaµ”±dC‘¦L:=Vn²¡HAA¶l EÂÝ;˜;w.“ �¤«¼dC‘¢¤ƒP(À�$‰ˆˆˆ¨a*š�”dC‘.]»r ÉŒ j�LM6aÒ�ˆˆˆˆ²’³Q˜šl´c:pöІ‹ jp*šl(¤5Dááá¸qó&ÔêÜJ}¾dÒáêÕ«¬Ð„ jP*›l(¤5$kÖ¬ÁÁƒ‘š’Z¥8E³WH9C­V³bYÇܸqñññ¬ˆJ¸téT.ÙPÄÐ@’—/_†T*e%Q½Ò©S'ömDD ˜)³QTTÉÙ+8efýÇ„C³nÝ:½é9É4………°°°PµdC‘Ç“ÑÑÑˆŽŽfEQ½’��€·Þz‹ADÔmݺÕìɆ"8|ôΟý‹³WÔsL8Ô1L6T\Éd´lÕ Q§O„B;téÖÍääÃŒ \¼p^/ÖÕØXV2ÕKØ‹ˆêâïÊE�Ï–ÅÇǾ¾¾Õ"""Ê�áìì\ætë¦Ä€àà`£ë"##‘žž^á]ºt…Ÿ4mÚËúIIIºu&NªP"U.‡ƒƒ= íÏ•uk×èÖ-Y²]ºtѽ€‹‹Ëo_s´My1*Û6¦Ä0`@­Hä0áPG+¯ $'†QŽÖîXê8PÿKyêT©_¨Ÿ<ؤxÛ¶nÁ�Û·ËÜfYúïjXùDTg-v€xÛÆ¬"ª3V¬ dgfàæÕKånߨ«9šJ[\gŽu¤ÜbG\Ž“U)ÈRF×ÅÇ\€âafbÄ@•W¨·D¥VU¨}6¬_‡FnM�‘òo©u‰‰‰ºÇžàÚ?‰°wlTmmcj s´My1ªÞ6ÆcØÙÙaàÀ�L8U'i~¼ò2�hݨFö ¾ÍdQ [¾f«öO`�æîöånóÜß�:kø"Ñ 1ÀOêTnŒäô{8xnk•b”,¿!ÍÝí!XW)†»£ní éýž~ R©w…Û(æú?°Ú”»ÝŽÝ‡¡PçU[Û˜ÃmS^ s´Íã1ŶjÏ]ŠL8P½&*Ô`aæ>ƒëªò<ÞaBDDDTû(Ôyˆ•=xâ1ÔšwäÙUŽ!ÏTAž©½«Á/KU©Š\ÜMQ°}ÍÜ6�Çlë^«¾“œ“ˆˆˆˆˆˆˆÌŽ """"""2‰:'jUÅîr°Š‘Ÿ_ÈÊk€˜p """"¢:I&“aÉ’%ðóv2éYxªºÙMÈSäúŒ»·�Ñq¨~cÂ�ˆˆˆˆˆê$¥R‰¸¸88ŠlaeeÁ !ªe˜p """""""³cÂ�ˆˆˆˆˆˆªÍÃôûÚX±" &ˆˆˆˆˆˆÈ$Î�áèèT¡Ïd¦Þƒ­ /="¶:™Äѵ)$ +¢–’¥*Ð¥[´iÓ¦VDDDDDDDõ€,U�®Ý{Á××·VDDDDDDDdvœ¬–ˆˆˆê ¥R‰ÄÄÄr·sqq�‹‹KµÅ€¸¸¸rcØÙÙA*•V)€2o�•ÉdÈÉÉ©RŒ´´4¤¥¥•ÃËË "‘¨ÚbÔdûš£mÊ‹a޶©kíkjÛ”÷ý¢''Ev)òNpsw7ù3n^­¡ˆ‹bå1á@DDDTwÉd2,]º´Ü킃ƒRm1`É’%åÆðññÁܹs«V®\it]hh(âãã«#22åÆ˜={¶ÑÛxÍ£&Û×mS^ s´M]k_SÛÆÎÎß~û-;µZH�“ •ZU¡ÏDöÈ/(då1á@DT~Ï*~=JRþr""]�¿mÒv+Ö†âƒÅ« ®“ˆlàïݨJ1 °mùý;‹O?_¥ÊŒá'u‚£Ø¶J1¤®bH]ÅåÆxuæBd)5ÕÃmcj s´My1ÌÑ6u­}MmSîÚ___¬\¹²ÌòDô$Ù9'�Þí{gíòìtàÎà¯pàøV /—uU"G`ÍmÀÊš ¢jâì⊘„Œr·ËÕ]§PåU9“bäçV9FyîȳaeeQ¥)™*d*Ëÿ PåUk s´�©1ÌÑ6åÅ0GÛÔµö-¯mîȳÑÜÝž�DT/ôLû˜OÙÙCû¯ëà…9Àâ‰Àíˬ³Êê>T›l ¢j‘––†ŒôT0úWXSäVéóEjK …:¯Ê1Tš|¨4ùO<†9Ú¦6µ¯9Ú¦¾µo¶Jªã¢!Ù˜å{Ce“ˆlðï½$¤¥yÔŠqP8KQƒN6ŒælÐ&4j`ÇbàÝîÀXW`¢X2Hz4(VãÀÇa€{óªïw”¤ø_CÒ3˜çQ5ŠŒŒÄî°�&Ý.ODD•ãæÕînZÊß»v‡íDddd­8þ©�¨¡I€i?@AðõXàòáâõy¹@Ô.àÒ!àóý@ó€}#`âçÀâ ú±¼ÛÃßüûN�epý/àÏo�•ÞweÇj�¸#ÞºܤÚcOO.v}¤Ê+£#°þѲۗ�ùO¿úÿÈÓ»—;—¶vÀKµ �< ö=°«œ�ª*Ræ’å}|YÉòW$fɲ݊Þï§-[ßÑ€­xÙ“ç8™�@d�PÈŠ &ˆ¨ ý'?Fqj›~²¡$U6ðëàÝ_�˜ÀßGõ×÷Ì\XÛê'z>tü4 8¾¥êÇÛºðáÀÁYyÓÖÚý'‹Æë—#·Ä€SvÀ‹ó€�·‹—�ÿH¾©M4<=®xù„O�„«”ßO� IDATÀņ�¥:Ê\јze³ž{ žÎ󚈈ˆˆj >RAÔPuy¦øõ‰meo{ã ðV`ùtàÔïÅË]½€w~Ñ^$§Ê€�†šŸ„é÷KK`ê·€KÿÚ.ræoÖ& €u“›<€•ïjïJˆ€Ùk´w)9È¥Äðë LòŽn,^>ê=À³MéåýÆ>–Ê”ÙÐã#%—U&f~‰g ÅNÀà×€oÆk‡y­Ïo"""ªêœl¨U*V1á@Deðö+~}çJåb ›ª}Ð&®žT àÊq`ÃBír[;í U1øÀ¹©öõîå@ø@v†ök´ï‹.¼‡¼fd© „„„˜´}jj*ÂÃÃ!uÃÖ†—6Dµ Çp j¨ K̃mQÉÐ�›¿þí¦ñí„6VPiòYL8Qƒpërq¡e§Ê%rj§Ê€ñMµ²zŽU™U¼¡½v¿Æ.Ø•YÕ[oÕQ暪G""jš6²ƒÔU k+ãw0º97B�ÞF×'Å]6i_Ž"íìJÙ* þ¹÷ u ž³°¶Å® /OL†OÁÙÕ �žš‚sQ'ôÖÛÚX2áÀ„5ç÷j§ƒ€§ÆgwßÖÿ)àõ%À™?µÿŠ™¼»øBÙųôØæ’xhÛ[ûÚˈ?¯¿Þ«Ä# W«·Þª£Ì5U�Dõœ‹‹ 7ñ@Ü?wXÔ`ID6hÑØA÷ÞÁA‰£¤tÂÁÝý 4gÛVÓÆwJMI�Z­†½Ð�Z8#&!ƒƒÖs§c!XÃÊÊBoù_q;�_ ½[ÔÊÒ¢Ô�5O!þÆ5 ö,ÜÜÝ+gôØq&o{éâD�> µZ Ÿ¦DßN×]xRýTÞ�,ù…L0<Á„C×î½àëë[+އC¹5T5ðÝëÚi`öz`ü'ÚA­mµSLö,:¦M6ÚÁ×¼Wc߯Úé`Ô{@à Ú iO`Ú�ÚÇ<|Šg�¨¬k´Ü0øuà¹iÚØö�€ïC¦hץʀC몷ުRæ¬Ôâ×½B¡¸f둈ˆê½Éc‡£wï^xyÒäJ'*ªK×n9 °±‚‡³ˆ ADx‡QÃv-ød80k5àìéðvg ßx Û ™¿ö¢ÞÂRûù¢�È?€È@AAéÏGíÒŽ90b&à×ptrs´ã<\£}tÃþ¹Ì졽£¡û0ÀMª=ù]àÜn üG ;£fꬲe^û�öΑvÚ3d×j¾‰ˆ¨ÞòkÛê‰îßÝÝ·þ¹ G±-ƒˆ˜p ¢GrUÀ�UÚ•q7ø~ŠéÛ�’Tly‘¬T`ÃBí¿êÚWyÇPÙ2Ú;>YýõHDD€+Çwê^wxúùz_Þ†7n‚T*ÅìÙ³ñêÌ…P¨8CFM(9`dYã4m¬`kcü ~ŽñÀ„‘Á øÇ/t*”¸u7‡�Ga뮽P©sYa Œ_‡ȃÖ††ÕØ>E"|}}yñjfÍÝí!ZC,°.szÕ¿âRŒêæ(„ÔU\î¾d© ÈRF×K]ÅPkòuSj²­™p ª5”6X"€Dkg³Æ�u@„¨C©åª[˜¬ø‹ODD Š……$öbtöóEg?_Œ|v &¿ó!d>då4 ‰ήn¼ ¬%Ц¦,JHDÚ±5ŠÞG^—kß‹ìÐLÚàîâ 7×FHI¸‰\•¢Ü}|³ðmH¥Þ×E�>�3Q‘åÆÔ/Þ-´�¥¤¦Cž¦}DV©Ì�L&ƒ½EŽÁÏe«4È/(„B•¥:òL� ¢šgÓØìɆ²D [2á@Dô„„‡‡#""�mÝu¿HSõ)ùˆ‚ÀÖíÚ´Ä‚YSáÛº9Z5—bÆkÿÁgK9è_M‘ºŠád‘�e‹¿Á¬¹óX! Xsw{“f Yù͇pnädp]lŒe �Hõ–{yKõÞ»»Ÿ ůC‡RÛ?.5%­Zû@"yô˜¨¯þ8$2Y~ 5øY{¡6�â(²En¡5ä™ÉF÷#´±ÒÝ!Qß¶uǪ•?â~p0BBB˜p ªN�s ¾�(A ݲ€  ØÙi;a‘H„.ݺ™oôØq8îœîý½ä$\»zU÷~RöV:58êÜ\DÇ\Ç‚¯¿Çöß–ú÷éU*á`g'ĤÑÃñÌSðölkü+OÅñÈóX¹~;22³ô¶w°#r÷FÀµø[ýúôìÚoN�vmZÂÂÂÑ1×±låܸyÛäãu”8`Ã�_¢E3/Àw¿lÄo›vèÖ;;9bòØx* ;<»�§¦!ò\4ÖlÙ…{÷SØèôÄ»xnïÛî.ΰT?€J‘Uj½§—ö|/º#Al'4ž,ð÷‡Ÿ¿•ŽS"‘'Œ0vwDÉõ³æÎƒZ¥‚q_}÷›n½ƒ½ß}ñ>œPPP€ÅË×"(øeô:Ÿ.Y�¼¼<Ø øæ¿sáê܈ NÕ¦‘½-š»Û£[+tjáŒ�àì €µ•%¬­,1oÚxLu ‚õÕK6Ú»‚‚àçï©Ô»Î'ªÊÍ݃‡C{??¸º¹é­sÙBê*†¿w#4w·ç‰Ç„Ñ“O:0Ù@DD¤O`k‹®ÛáÓ÷ÞÖ- ”H(ÒΧ%ÒdB£ÉÃ÷¿nBÖÃldf=Ä¿mÖmóT@w£û�Ø‹qàX$Bwíƒ:7qÿÜÁÒŸ×éÖwòó-÷8?{oúöî ¸tåæ¶To*É—††»«vì§�¿G`]èŸÈz˜ eŽ ÛÃ`m蟺c=b¾äç"S‘ “¶OKKCDD¤®bm¬ê|ù¥®bôôqE;/'x8‹ (Q&W77tîÚÃG>_î£ T¢ï�Hàçï�!ÞÅË“&cÖÜy5f zê3€Áû¡_`wˆEv¬43â#Ôà’€öñŠ¢¤CE¯`²�ˆˆHËØ™púì%¬Ù²KoÙk³>*µ�••%2³Šg²ps){ ç]{ë½?£{ÝÄݵÌÏÎyk2B†ôÜNHÂÛï�Ü\ý[¨ûõÔ½ÞsèD©ŽFêî¤ìÑË×lå‰P‚L–€[q×!u—9ÍaYê<ÄÊ`ëÆ¹&mŸššŠððpH]ÅÈTæÖù�=›6†¥& àé%EkH½½™d0#©ÔR©7@ûØE«Ö­1èÑú³cp.:ç¢c¡Pæ@,°F~Aaƒx’ ¢'�t`²�ˆˆÈ°ÂÂBd+rë.ÂöÅÎ=‡QPPPj;ßÖ-ðú„áצš6v…µµu©D™´Iÿê½Ï(1í¦È®ìÛÆ'�Qü °•rJŒQ¤äø·KŒGQänbñ(ø-‹‚€ÄnÞ¸Z¥„CCäîêŒaû PwX[ZàØÑ#híÓ­Z·fåÔ�ÇëºgWôìª4óØés8y6ù�?ÈAbš’‰&ˆÌ›t`²�ˆˆH_Éi1Mѱ}¬þî3lmõ–ÀÒÒ´§|UêÜRŸ5UV¶ç.^ÁÀ§zCêÙ_ŽU›ÿÐÛF,*žJ0G¥.sÿå%8ˆŒ)šeÂÝÕ/…<ƒ~A=ôÖö,+© èÖ—¢ŽÜ�ìàîd‡ät%d© ÎrÁ„QÕ“L6UÝ;SÆë’ �ã‡ß6릗üûØÕÿsƸ/OCÏ®á`/Â_ŸûŽ"5=C·�B©„ÄÁ^—PP(õÇz² t¯³³•lTª±À­š:@`c�ž}ú—J4Pí$ ñÚSuú®ÆÆ<œEpwâŽ<òL+© 4’|Ò¡¬�$™l "ª;\\\и‰2¹¬ŒZ¨C»6º×_}÷’ÿ•£°°MÜjdÿñ·�•­À†íaº„Âì7'êmóÏ™îuK�L”\vóv•L&u£S gØ m`ceicÎrR—H$ ö,^{c*Úûù¬­,Ѻ©¤ÖÍn‘©ÈEã&pqqaÂ�¨6'˜l "ª[<âÄʰ2j¡’³Ah4Ń5>;°¯þ/§–ÕûëéúmaÈz¨”/xðÓèЮx6„c‘çt¯‹˜,)xpñ²£§ÏÖªú•¥*ð Ð³æÎãÉVË´nâ©«€v È~p|†:žx˜0q´ƒxz8‹ÐÖÓV–µâce<â2á@T›“L6™Ï�ƒ0Ž9 [[<ûL_L�4iéÅI¢6­šUëq(”9X»U;ƒ†……þïÝ)ºuÛÃ@žš®=ÆC1~T0DvBHì1yìHŒ9pï~ vDd£R¹¤®b¸;i×uusÄI“Ñ¥k7VLçæîŽ—'M‚«›ö-±€•„QÅ’L6™ÏúG�2Àœi“pþ`(þ·p6"ÏEãÛ_6èÖ­ûáK„­ÿ¡Z�eãŽÝÈÈÌthçƒCf+ð·_!#3 –––xÆkøkßœŽØ€9Ó&ÁÒÒié0ワJ�ï@æ!Ù °­;¦N�ZçË"´±ÒÝÙàêæ†ÑcÆrjËzD âåI“Ñ; ¯O™¡�É2á@T�¤“ DDDæ³çÐI,øêÈ’þ…F“‡¤{r¬XŠÙ !üÀ1lþc“ÿ…B™ƒY«õXrrTX½y§îýÌ©t³NÄ^¿‰‘ßÁÚ­»pûn"Tê\ää¨+¿lØŽágàÆMþn@åóz”l€Ï¿/H륀  87rÂä±ÃYp– "Ii~\òèœ›È !""z¤¢Sa>îÏ}Gðç¾#×}õݯøê»Šï³¬õe­[»u—îÑŠÇ¥?ÈÄ’ë°dÅ:6z-'•J1{öl¼:s!ª¼ZulÎöÚYY:wíÊ;€~�ÝñÓêPVÄcx‡‘sn0Ù@DDDT~: {@_Ä$dÔØ>E"|}}‘¥Ô ¿ °ÖÔ…•¥ª<4iê�Ö>><9€D™ �Ú6‡ÐÆŠ•QDDDDDTe‰ήnÈRj|]ˆ…ÖpÛâß{É<1€¬¬,lÝ 1”�ˆlX!%0á@DDDõBdd$v‡í„Ÿ·+ƒwG!쑃í¡[YD5L–� {ý¤í lëŽU+Dxxx­¨&ˆˆˆ¨^HKKÿ÷’à(²eePƒ#°±‚µE>e2VQ R«T8vT;6M,jÕ£=µ�l âlÜY T­Úh伨±#ÍRÌ“�ª�W^D…¼U™ˆ¨.Ñäæb{èVtíÖ­Z·f…Ô#j• ÛB·"W­X¢nŽBÈR¬œG˜ph–;ôÅe[/VU«N¹‰xëáÉ[þh[/¬pèË�ªÝ²ôß™t "ªCŽ9‚ÌÌL$Êdè׺tëÆJ©RärìÛ»©))¬Œ2ð‘Š@ \BÕ¯¡'µdV|fœjê\kÄJ "*ú=W©D\\$"XYZÔÊcìÙ«7àØÑ#Ø¿wÔ*¯;‰�ë×é’ íýüX)Fð‡¤½ŸÚûû³"Ȭ®ÆÄàjl,+¢„QcưȬRSRpìÈVÕjYYYÈHK…DdSé™*²”D^—ãÊñ�&m/“ɰtéRø{7BLBF­œ!ñ‘&LšŒ?wþ�Ô”\��ÅÍ›7Ñ­{wtéÒ¡�'O¢V©pñüy€@ @@PºtíÆß‡™p ‰ÄR©7+‚Ì*1�ƒS=Žß3""jˆb¯\Á¹¨Hø{7BäuŽí¤ÿ{¸£ÇŒÅ±£Gp56¹j5¢NŸÆ…óç™x¨cB!‚‚så F<ÿ$ +¥ L8ÕÀ…ê�aÏ" ¨¢NŸÒK<¸¹¹s@É:¤K×nèÒ•cq˜‚ """"""3ÊÏ/D¦"í}[B(пsA"‘èGFŠ\ÎdC-‘••…âãsþ:0©`L8™‘B�‡XÙü÷ãqF·‘H$ñüóå yéâBxI¥¼}ßÌÔ*e�%ÈðÏÍxdeeéÖÅ\¹R'™Š\´iÕ...µâx˜p ""¢z!$$�=›ãµY±2¨Á‘¥*ààÒŸÌ›ÆÊ¨cÊ»A­Ré ìêæ©ÔRo)ÜÜ3QIg¢"!KH@¢ÌðXd®nnðïСN–-Vö³g¿€ž]kÇñ3á@DDDDDT eeeA @­VÐÎZ”š’‚K/l¸»»£½Ÿ?ü8�ÉâãâtSZiÙª5Zûø@êí]©DNï€@‹<�Le+¸&ˆˆˆˆˆ¨V ¬Ñ¼±=–,Y‚9sæ4øúpswÇ[3ÞAŠ\ŽÄDî& )Q¦K@äªÕH”Éàêæ?N8¨U*¤¤¤ÀK*­—u”"—C­V#1QUŽ ))ÚR^3Öèg¼¤RH¥Þðò–še†±€  ì‹ü»VNÍÊ„5xVVpÙ"..Ž•ñXâÁÍÝ]7¦@Š\¹\¹ü>e2¸»76úYyŠ¿‡†ê]l€Ô[{‘-‘8B"‘@âèX'ÑØ¿w²²² —Ë‘û(ñRQý äIUC˜p """"¢:ÉÕÕÁÁÁX±6¹š‚S±»JÊÊÌÒ{_4nÁããôD@P�Ñ8ÛC·ÂÍÍB;ãcN88HÊ|´#6&�,!á±cÍ„J­Æôï�‘™™‰¤ÄD£û—8JàæîµJUæøÄ„Õ^ÞR´Lk‹ã‘jlŸ... Á‹W³Œ�z{cÔ˜1HMI�*G¹\µZ…¬Ì,½‹ÿ²¨U*$ÊdFY,âéåUNÂáŠÑd�©ÜÜݵåzô„—·Tï=1á@DDDDDõíÂVê�,¥ÃN4øº�ˆlàïÝ˃QcÆ<Ñ‹a‰D‰DRæ1Èd ptt2º>++ ž^^Ú×HT#àêæöØ2!Ü%ÊÂÇ!ê&ˆˆˆ¨^ˆŒŒÄþ‡àçí„Ø„¬jp¸B‹\œ‰ŠDï€@VUHy 7wwŒ;®Êû1G *›Ÿ·v‡íDžê!Ÿ|_`É&!""¢ú -- ÿÞK‚£È–•A Ž£ÈBä"êôiVQï þ½—„´´´ZqRAµF»6>€kqñz¯_GDµëûZÒ­þÁÿ÷>®ÆÆÂÚÚ999Oì»Ë~ƒˆˆˆèÉâT¡_Þÿç×Ö}zcæ;3põêÕQWþþ[WþÈHÞ¶HµëûyêäÉ2×W·Ï>ý—££ñõ¢Eˆ:{Ž CDDÕJ©T"..‘ ¬,-X!DL8P]w-.^÷ïì…‹øzÑ7¸ò÷ßûÒ(\8¾Z÷YD„‡A,C,cwxx­m§å?þX#˜T»,[º………Õ~NûN^¿~0hð‚ýî>~Îצ~ƒˆ¨!ÈÊÊBFZ*$"›ÊÇPjy]Ž•+Wš´½L&Ã’%KàïÝb!oÞ&bÂ�ê±XŒ>}ûbáÇC£Ñà»o—ÕëòæççcÏîÝêÓ}úöÅÁ�››[+�õØÑ£Z°O÷郎~íÑ7(ïÍŸ‡¤¤$“ã?~;¸R©D»6>òÌ@ܽ{S§¼Ž]» ¨w/Ì|g233õ>}é&Œ‡®�:¢wϘ7g6Rärô @»6>&' ÎDE!55ƒ‡ Áà!CñðáC?~Ìà¶ÙgVV¾üü3 ì÷4:úµÇÓ}úà¿-DFzz…Ë|áüy´kãƒ+WþÖÕ]·Î��››‹U¿ýŠ!ÁèÞ¥3ztí‚Q/<�ß·mã \OXXXàÅQ£ðýwß"??ߤϘòý4tN=þ�\±ü'´kã¥RYjCwÚ¤ÈåxoÞ\öê‰n]:cô‹/bïž=¥¶;°^{e2zõ„»¶èÕ½&O|ÇŽ)óøºuîd|ß&”¹¢} 1á@Õ¤  `ee¥·üáÇ?v <€¯þ÷5¢þ:‹o¿ûQ‘‘7f4ÒK\PW„­­vʳôôt|ðþ{˜6ým=qC† Ãþ}û°tñbݶwn߯«“'ánÂ]¬Y¿ÇNœD�ž=ñÚ«¯àÁƒzñÊ‘H„þ¢ÿ€y¬¢"ûT©T˜8a<¶…†bÞ{ïãô™¿ð~ˆ?wíÂÄ ãuƒî™ZænÝ»ëÝF~-.¢/ãóO?ÅâE‹ðÔÓOãØÉSÛ½NNNX¸àC¬]½š'q= Ñh0mút$'%açŽånoê÷ÓÐ9õ¸ioM/µ�±ÇÙÙøÏØ±ˆ�‰Á–mÛqøè1xxz`öÌwö矺í6o܈wgÌ€••¶l ÅÅËcë¶í°²²Â´©S±gwD™ç|UÊ\‘>†ˆˆˆˆ˜p jtéâÅG¿ø÷Ð[¾~íZÈd2L›>�A} ¶·G·îÝñÖô·‘"—cÝš5•ÚŸµµö½ììlŒ3�;w†½½=^{} àø±â[«×¯_‡œœ¼5ýmtêÔB¡£ÇŒE×®]‘——gò>U*< M4ˆD°³³Ã3Ï<ƒcG�B‘�­_î ìsÓÆ ¸qý:&¿ò*† 6 ¯M™‚›7obËæM.³!Ego¾9 öööhÚ´),ü"±'NœàI\¢qã&?áeüôãP?z¼Á˜êú~–gãÆ HL”á­·g Y³fprr¼÷Þ‡­­-¶mݪÛnÿþ}°³³ÃÂ�?Fó-`kk‹-[bÁÂ�+–/¯ð¾M-sU¿oDDÔ°�E1kîºí9(¨�Þò^½zNž¬ú…n@@€îu“&M©©©ºeçΞ-µ<R¡ý9| …Ï.8d8Ôj5< ·mEöypÿ~Àà¡Cõ–0P[‡GŽT¸Ìe%i.\¸ [Ö¼E \¸�Õk×òd®G¦¼ñ²³³±eÓ¦2·«‰ï§!‡têÜY·ÌÓÓí³T‘ IDAT—cb±qËݲu6ââå¿Ký¢æÝ¬àî�;ÞweÊ\™ï=!!!xmêÛˆ¼.gePƒ“’©Bv¡F�ÃÊ jÀ"¯ËñÚÔ·RÁk�ê¡\©Â&ÚÑÉ =zôÄÛ3fÀ·m[½uEM=<ô–7iÚ KH¨òñ¸¸ºê^=ÒQòùõääd@ãG EÚ´ñ­Ð~vG„ÃÑÑA}Š/Táââ‚Ýùü •Úç­[·´QÞúUÍš7׋U‘2òêk¯cÙÒ%xëÍ© B¿~ý1à™gtPT85j„W^} ¿¬ü£F�†½½½ÁíjâûiHÑ9ïææVî¶—.^Ćõëpãú ¤¦¦@­VC£Ñ€îÿЍL™+ó}#"ªi*M>ò`Å¿¦=!ž^^¸#K†ZÃߘp *©È4sEã½åEï‹ÖWÅããF”úühB¡Po¹±‹0C´lÕJoÝ�;w0lð ìÛ»ã'¼\á}ŠÅbdeeáBôåj¯�¡Ã†aè°aHIIÁÁ°ê·_±wÏnXZZ`ñÒe‰ˆˆÊø½ÍÆRW1ÂÃÙp ª8†U«-Z(ýX@Ñû¢õÕ©qãÆ´Óð•grŒˆ°pxxx”J6@óæÍáéå…Ý•Úg«Ö­òû÷k¬]ÜÜÜðŸñã±jµv€¼Ç�ód­‡Æ�‰ƒV®Xnðn‚'õýlÚDûø‚\^ösöwnßtêÔYo¹¡éwëRŸDDDæ#‰àããƒLE.òó Y!DL8PC2ð™g'Oè_ÐFž> è?p@µC§.ÚG.>šIC—Dˆ7éóÉÉɸpá<ûô1ºMŸ>}}é’+¼ÏÁƒ‡<<²HÌ•+èÿT_|öÉ'•*wÑfѳæ3ß™�€^=q÷î]Ý6N�émKõ‹­­-¦¿=[K ÄX•ïçãçTeõ}ú)ÀÙ³é–%%%¡£_{Œ5J·¬è¥’w4äççã—•?ëÞ—œõŔ㫠}Q}%qt„“³ 2¹5¶O©Tйsç"Vö u�ˆ jH&Lœˆæ-Zà—•+qö¯¿ P(p&* +W,G‹–-ñòÄIÕ ^†••~úá{ÄÆÄ ''ÛB·âúµk&}~wx8 K�j_RPŸ>(,,ÔÝåP‘}Ž?íÚ·ÇŠå?áð¡CP«Õˆ¹rïÏŸ‡û÷ïë�ä_ÞÞÚ‘ü/^¸…Bÿx�‘�/?ÿ÷ïÿ Ev6V,ÿI{¼/OäÉZO�|á4õð08ÀbE¿Ÿ%Ï©ªxyâ$xxxà—•+qëŸð #‹¾þ �/�­Û®hæ–¿ÿÙÙÙHJLĬwß…“S#Ý ªG�Ann®Ás¾¶öIDDõ•Ÿ¿?z=�XÙƒ_b�5ü¼�°=tk©;^‰˜p 2‘H„�›·`РÁ˜;g6zöÀûóçaÈСظy ÄbqµC‡ŽñÝ?Bhg‡qcFcð3‹ŸùUû%°,ûkKKKݦw@ ¬­­uw0TdŸÿÏÞ}‡5u¶ÿ&!ƒF bQA+®PëDquYµ¶Ö®·¶¶}jû¶Õ.ÛÚ*¶µÓNºA;Á½­‚V´‚ ‚"Q“0HÈ ðû#‰L‘‘„ûs]½jNNÎ9¹ÏyBÎ�ç¹>Ÿ�ïü ÷ÍŸ�÷V¾ƒC£ðàóá+•âãO?ÃÌÛoo×û~é•Wл7~pâ'ÇáÑÇÃ+Ë—Cyù2¦N™‚ÛF�ÂáCÿ`ÙòWñø¢Et±º(777ü÷ÿžé�öÙ𚺉?þ’„¨¨(Ì»w.&Œ‡’’¬^³³$þûÏàþù`ßÞ=ˆ½õ<òðC1k>øK–¾<ûßÿbLlL“×¼£~&‘Γ››‹¬Œ#PHé<’žG,äBÀ2áÐÁt †àpX�y(fŒ é2A^Ø”òÒÓã³€úQ“6»žÙ)òõõÅ+V\×ö¯ÝWSûnîxšZ>aâD[WêzõÕè}||Z<®”�›Z=vOOOœ8yªÝûôððÀâ%K±xÉÒv�ƒ¦–ÇŽ…]{öÚ-»ïþù¶Â–¤ç´Ï©Ó¦aê´i7Ô>›»¦Z;ŽæŽËßß¿ÕB¥B¡//[†——-kS›nËñµ÷3©#> IçËËËCæÑ �ŠÀ¨uÒ£H„<`ÂÁ´4ÜC!f@¿0úÛçdŸ—.^@ii©Cõp .ïõW—cÖ]w¢˜aì–9|0bäH—Ø'!„B!×Ë`0`ÆÔøëšÁ�4öþêUðbUQ/»kPÂ�¸¼›BB�“��ÿ½øÎÀ`0àèÑ#xëÍ7 ‰°è©§]bŸ„B!„\¯_7¬‡¾ºÉI¿P0H‡£„qyóX€?úó‘ÆâùgžÅà›oƆ_Cè•i)�}Ÿ„B!„\³ÙŒo¿ù+ß{ß}û­­sK¦Ï=ó ¢o‰A`ÂØÛðæë¯£ªª &“ £c¢‘™‘�)“&bpÄ@ë²Ø9rwÌœ�›##?9ééi8tð î˜9QƒaÖ]w"÷ôi»}�;{�>üFŠáQC°`þý�Ö!Ž�j8�!nòÄMžâòû$„Bqf:C ²‹ÊñÍo¶iýÜÜ\¬Y³1ýåÈ.*‡Vo¦ ^‡¿þø¡¡¡1b$ÂûÀï¿ý†¹÷ÞÛâk–<ÿ<¦Ï˜�対 ‘‡JJJðÅçŸaÅoà�wß…Á`À÷ß%â«o¾…ÜÏ<U••øôã�±juzâ«/¿ÀË/¾ˆ >}°*a z÷î�¯¾üo¿µßýð£m_/.]Š;îº }ò)j-üöÛoxuù2$­ß@'ÏIPB!„BÈ ;˜–†í©¿#¦¿¼ÝÛ°ÔÖA«7#<<œÚÉ, ¾übyô?€ÿ,| _ù%jjjZ|]Òú ¸þðòö—ËEŸ>}°è©§±oß^°X,TUUaöܹT(ÀãñXëD<ùôÓë×B¡<°—.]Âÿ=ó,ÂÂÂlËN�ÿ’6üJ™“ „!„�&�ø÷_Ìžu7àëÄDÄÄÄRPˆC‹‰‰A-‹‹÷>ù†‚q ±�k÷X"ä5ZG£7µXp/"È KtÆæÇwÍ(5 x7Pi ðö•âÁ¹3)¤U_|þ9V¼ýîž5«ÑsSS±îóÏ›M8ìßÿ7Þ]½B¡Ð¶l÷®]�~Ì EŒF#Μ¡¡=-É.*ÇÒ'FLôJ8Bq\SS ‰›RS6áðéÇ㣵ÒØb___øôî1Uê9lD7pØ,T-0˜-M®çãÉGÿÞ’V·'«ó†<È}} “z7zþBÞqÛöšc®cC©¹ÔìóÁrˆnv‰‹*ƒ–Ú:è 5°ÔÖÑ…ÜN³5à@¡¢`�Ø¿eee˜yûíM>?u*>þh-ìß�Q£G7z¾oß›ðןâλîByy9öîÙƒíÛ¶Âß߿Ç;ÌŸ7S¦ÆãŽ;î„—‹ä¤$ <Ø!ãÚ;0…L ŒÍ|w­Þ ÿ€Þðõõ¥„!„Çd±X°yÓ&ÄŽ‹…Û·ãÕ×ß°U›v${÷ì¡F\>© â»Á�ÆXȇ͂‡À¾Ç‚¾Ž®PŒ`E@èAÖë«48°§õ_§L…¨¡Ãš}~}R©Ýc£ÑµJeŸPP`é’çí–�c.@¯·özØ»c+L}‹‰‹*ƒÿ–ÓÉï¡\d6n܈éÓ§S@:ÁºÏ>Ã#�þ\.·éÏÿyl!Ö}öY“ ‡·V®Ä›¯¿†Uﮄ@ À˜ÛÆbÍû`ýúdÜ1£cÏÙ‹/½„·V¼‰„U«àææ†aÇã­wV:d\gϽ¯®ú¬Å¤kOD B!�:xjµq“'ƒÅbcÛÖ­Ø·o/&MŠk´î±¬,¬~ï=œ<™Ÿ�Ñ£Gcé /â®;n‡Z­Æñì[¢B«Õâãµb×Î�P©TðööÁ¸ñãðÏ< oëÍ‘^�aCnFPP¾øú¼½âMdfd€ÇãaÄÈ‘xýÍ�H$È8z÷Ï»:Wø€~Öiµ>‚¾ÿ)ý… ÅÅ`±X茹sïŬٳéä§à#„B*ju½ñ1ÃÛt/$£Á€^r©í±\&ÿʘëëý2Ý}½mÿV^è•R P«T0��Ö—ˆÜqÏÌáÈÉ-@µÞ€sL‰Ýór‰fKm‹½:ˆóâqÙPHEHMM¥„C'ùáçŸ[]çîY³lÃ-®íA8pà@ü’¼¾Ñk>þ>þD“Ûkªb[–EDFâç¤d:i”p „âJRSR  1nü°X,E"lJMm”p(Ž. ©‡¦C«ÕÒ…Ez,êá@!ÄΦ�©�H$ˆ5êê�L |}}±iãFÜqç]¶å%%Ö±Õ~W’õúõ o´Ý³gÏ‚‚ì»v÷ ¶ÛVC¾Ò«ãÎ9kAË–<üÈ£xM=¾1±±;vÆOœhKXו››‹¬Œ#PHE`Ôºn?žæf‡�ÊdT(Ô���”Lh™\™\ŽD°ö)..FQÑy3 BCÃðÈ£ðOVþN?Ú¨„«ÞàºÁ‚b†A BÑ!IµZ é•Ïaš èúÔ×¢8˜–†Þ��-{"¤#)¤"de�DÄCxx8%!„8ŽŠŠ ìÿûo˜ÍfÜÑèùC¢´´Ô6Õ’¡ÚÚeYpÍ “‡Gã_õzkeúC£šþb{Mµû†I†ëñØã�#¨O|ÿ]"ìß�ýÿ�·V¼‰ÉSâñÆŠMq yyyÈ<ú�C$äRÄO…ÂS™0›L舰~ýF7�€/ $4!¡¡v˧O�é“FC¥.Ã?Y9زë´ }…(.Õ»TÑI™DV56$'áÙÅKÚ½�ò²2lß¾ [6oƑÇ‘s:—.0Bœ,á�yôô’QÂ�BˆcÙ¶u Ìf36mÙŠ›BBìž+,,D|Ü$lݲ÷Ý?€µ ¤Á`€ÉdŸÏo”\hH$A«Õ"ãØq[mˆÎ2%>Sâã¡R©°cûv|ý՗زyØlV¯yŸN4é4#¢"0mâhD„[ÛÓ¿�K×_pš›q©�-ùð므)̇ÜËÊŠj—K<´GEEvîØ�-›7áð?ÿ ¦¦, ƒ¦‹‡�6Ú�œTC.@©1P@(á@!äZSRÐ(ÙÁÁÁèˆM7Ú~~~8þ"‘¨Û�Mg¨AvQ9ž{î¹6­Ï0 äÕcF„PÂ�BˆÓIMM›ÍFtLL³ëÜ777[†AƒãÃ�>†ÀÝ÷Ι�¸‰�“�ƒÏ¿øÒúG†}õÏ ŸÏÇ÷?þ„ûæÏÇ{+ßÁˆ¡Qxð�ùð•Jññ§Ÿaæí··ë¸_zåôî�‡\€øÉqxô±ÇðÊòåP^¾Œ©S¦à¶Ñ£pøÐ?X¶üU<¾h�hrÃɹ—;ÀÓSŒYsæ :6–ã䉇¸)ñàó­Ó•†ú‹]>é0zÌ”••âõW—cܘÑXöÊËØ³{7 †wž“��#é#"È«ÝÛ°ÔÖA«7·¹Ø�^¯G^^$B8]Ô„8JB¤lÜÔê:žžž8qÒ¾÷„‰1aâD»eÕWf¯ðñ±ïbîáá�ÅK–bñ’¥-î§¹éךZ;jvíÙk·ì¾ûçÛêLÒQä�-Ù •É0{Î\*é"""#!—Ëñ×€ÅaÃP\éÒï÷‹¯¾¶ŠÜ¾[¶lÆŸ¿ÿŽ_ׯ‡»»;¢cb0nüŒ7Î6%f[i5”—ª!òè¢"„PÂ�BÈ�yýÕå8qâ>øp­]ÑÈ#‡FŒIA".�Ãf!Xn�RÕÓSLÉ$“Ë1Á€xû~¬OÙîÒï×ËË ³fÏÆ¬Ù³Q^V†;¶cËæÍØ·w/vïÚ6›�Aƒ#iýº8!íFC*!„´ÛM!!ÈÉÎÆÿ^|g `0pôè¼õæŠDXôÔÓ$âü¼Üm5&O�§dƒ‹â à ¸gæ$È{PPoÌž3ß~÷=öHÃò×^Çð#pâßé¢h§úZ³æÌ�\&§€�‹z8Bi·ù,€ŸŸ~þé'Ì¿o*++áí탑·ŒÄ‹žlrzMB:K¿~ý5lR¶ííðm‹Ý¹¬C)hªËž!"ü&(ÕeNs¼½ ²:oŒ�~CÛñõõŽóæáÞyó V«éBh§úZôyAºZvQ9–>ù0b¢GPÂ�Bˆó‹›<q“§P H· ‡FgÂ'?mêðmsØÖbtr9ýRÙädgCÀ2C.@©18Å1kõfÀksS“É„¤_~Á¶­[P�Ÿ�N¡Pˆ¾7Ý„¸É“1ï¾û¯»†!Ä1> üzÃ×××!އ†TB!„´‘X,¡ ô™G¡Q_„ØE‹ꪪpß½sñÎ[+�™‘�FƒššhµZ?v «Þ}óæÎAee×Ïä°Y ¹ÈËË£ ‘8•g/AE�µŽ‚Ñ%!„B¹‚aŠ V©zc�K¾ÇÏ>ûùgÎà™çžCêæ-8šu p$3 ¿ýù>þÎàã�Övù±‰nˆ òFBB]Œ„¸RA!„rŒk{*éšçvû–-¶Ç|®kþ6·cÛ6¬^ó~£)�=<<0pà@ 8ý Àª÷ÞÅÿ^z™. BH»QB!„�6ª©©Áúä$¬ON‚J©¤€¸£Á€õÉIÐjµ¶eBžkþ6wñâEŒ=ºÅubG�rŠk<<<ëÖ­Cúi%´z3]Ȥ[±X, Â5(á@!„Òæµ¨U*¨U*¬ONBA~>Å0L~üþ;ÛP ©LæÒï—Çã!ÿÌ™×IOKƒ·÷õM ‹¸w!ý4%ã@.`ëÖmô9у¨Ôå„kPÂ�B!.¡´´—.^€XÈí´}(Aˆ›0�HùólÛ²ÙîWqâ\¦¥á×ädÛ9�ÐÐ0§{.n° ˜aZ]7rÐ ¼¸t ²Oœ°[®×ëqêÔI|öé'øß K1vÜ8º@n€¿·§²ÿEfÆQ F ,-wˆ©tÅB..]¼€ÒÒRJ8B!„t”ôôtlJù‘AÞ�ºŸˆÈHÌš3|>p2'_±ŽNŽÏçcæwbrüT§<~™DV56$'µºîÃ�<Šüü|<ýä"»åÆ܌»n¿k?øþþø¿gŸ¥ 㨴Öz/Å ƒ¬Ì ˆ«žg¥ S„_S¶;ÄñDycSÊHOOwˆã¡¢‘„B!×I¡Âý ÄÁ´8™“cK<äçç#&6QC‡Q�œDtl, Fbbb{L!Ð1·Ý†·W®Ä‰=¸\.d2zcìøq˜{ï<ˆD"º@n@I™2±"�öîÞ ��ˆÈH Œ a˜"¤üù'LF#M‡I B!„�Ž#‹19~*¢cGÙ&£†jÇ� hµZÈäòf×7~B�‹Ë�wÝ�;ïºÛöøßœ“t±t‚ü‹ZDöñ‡ÍÂö­[PY©Å­Ñ1�“��í[¯Îjc4[((M „!„BH+TZ )=wmâ!bÐ ˜$ �&ùù‹Åxä±…˜ëd2™Àãñ(7@g¬Aöùr[Òá`ZÎäåaìøñP(‚(@NH«Õbïî]vÅ@ó/j¡ÔP²™„B!í Ô öíÕâ B}â¡%ÓÒ�Ÿ EA „8aqBGM03`Š0L‘m¶‰†7ùù ¥`Яí×Ý©¼3]zl&s-µO<8§Më3 ƒõë×#"È …—« 3Ö8dÒáh¾ý{K ñ V©ðkr2=õt�Æã*Ÿ3{÷ì¶ £KmNWД¬”p „Bé~ùùglÓjÖ‘ Ë »ä IDATT( “É¡R@&÷ƒX,¦@]‡¬Ì ìݽ»Éç<=Å Edä ‡T�Æ„"¼¼¼®ë59ÙÙ8rä"‚¼�STÑ®ýÌ0jf̘Ѧõõz=òòò òÀá°6ž–Ú:ä0ðB!¡º†�‚ó%~]lN@¥Tb}rLF£m™RcÀ¹Ë•°ÔÖQ€(á@!„Òý" ™¼<\(.¶-+f»*ö<>O>ý_ VƒøÈd²f æñøv�o EPŸ „„†µ;y1hò‹.ãø�#.Óæz-X,¨Õ*:x’“ñö»ï^×vµ ÊKÕ�iFsJÊô¸\Q øwÕgˆè‚±1Ã16f8Ç�é 50™­½g4:˜Rõj „!„Bˆc‰:Ì6ƒÃ¡¸Áã•_Îø×Ü@_+';••ZëM8_€@…©cb4 R©`4Zÿo¨6@¥RB«Ñئ�5gN³ÃYär9n�ŽA`�¢ÃÆÄ‹Åbð…Ù=¿3q8øùõÂíwÜ�>ÁÁxëÍ7°î˯¨áv°†¿ˆçœ.@ÎélHÙ�Q˜:q4T/ àÊЫ�°0êõÔMôÕÎÊÆÞô£È9]¹DƒÙB‰J8B!„8>…" E¢ ÀÚeW©T¶úº†Uѯ½IK$Ömuk%|•RiK ´Ô;á`Zl}®xµJÕl2A&—Óp‰NŠÌŒ DQªË°iÇ~lÚ±CCý p«CA~>öîÙ ©L†°~ý¨pú££Q)•(fäçŸA1ÃàÁGÃñ“gp$+‡³²íÏ…¤„!„Bz®~ýú!jؤlÛë”ÇßÖgOO1*+µ�–kµZ[�€ºººë“~±Ö!•É h")pÏœ¹Ín#';™Gh¼VK½î‚&ߟX"†L.‡——¤2$/—¹Ný½Ý¡Ô:|Ü·Ñl�¹Ž�`EÀ mÇb±àÒ¥Køæ«¯h†ŠnR¦ÕA"äA$°ÞªÕ×}©¨Pt{RÑY3 Š‹(/_Ã0v5àÅ×W¡¬ÒèÔï‘Që0sòXôë×�„B!%<<� Ÿü´©Ã·-â»ÁX]­VÛíÝ›]¸Ð–`Ðh* ¾2 Á`4@u¥‡Ÿ}•ï[K4E«Ñ´ëu …„†�ÇãC,C"ñrù®ãý½àãÁƒÎXÓd·ì'š�O¾]ß®m+5H{ 1{î½­®ÛÖY*&ÅM¦–nP¨¬¸øxòá-âA"âÙÝ4_ºx ƒ‡ …Ð�f¹h‹–ŠËÖ ½ zƒó£bÔ: ~ ÂÃÃ)á@!„â ‚åP ç„¢ccâ˜Äb1Äbq»êDDjòuJ¥FcÛº )p+ÿÂ*–H –\M´Ô;A,#"2²G\Cý½àç-‹ÃƒÅRÖèy…T%“�'š�’ÿè¶qâR©£ÆŒÁâ%K»~ÿlD7äåå9Ì/´ÝÅ`¶ ¤L�’2½µ½¹�y»s˜°àéeè«@pPú(zÛþ-t`}Ò/`±Xàóù�ûùA,–@,·8ÄÉ3Œ­þËÀˆÈFIË¢—àL^^Ó c J+�4Ó%!„BHG鈛üú¤u"¡;†GE"&:{‡îعf× òCd�7ò/j;mÌxs³T8D¬nˆ òFBBÖ­[GOZ½¹Q"êS‚sL €£v×Û …'ØWf -ÈÏo´­úš/- ›r S„ ÅÅÐTT@«ÕÂ`04Ù»Š)Q\rò  R—C©¾šØp9�IÐèMTô‘„B!„tï NeÙeÈ%‚¾é Ýñê’…è«èݦõŠ ÄM‰®íÎBuG�bØpš®ÑÙèôÕ¸Pʆˆï›e7Ö¼ÐjQU¥Ã†”ˆH¥Þ�ûzÛÖiXë…ÇçCÞL�™–’õÉåå˶B²×2 ;~|“ Ì¢ž+BÖ‘ƒ­¾ïƒGŽ�QëšÞ‡ÙÒìs„„B!„t™â"ÚÒK�‰o,áà-áÏþ§ÍɆz¶(Ýœt¸Þ½Ý°ÖnY·nÝv']¸ oô¯¹¹p9àqÙpã\MDÀ�“Û›ÝÆ°P9øWîMF#ЦÉõ6¤ì°ý[&õF°"z½ÊÒ2äŸ>‰Ê²Ë­obr ,WnM ‹J ÓWÛž ¹ˆ ò†Fgkja4[`©­C•Á “¹³…N:%ˆ+©Ÿ3ÛZ[Cq®:~Ï¥¿ô^ùã^\ÌP0œŸÇ‡L.ŸÏ§©úéÁúz¡·Ü«Q²¡²ì2\N«7HõI‡ 3ç�´éï=¶´ðÎ[+pþüyÔÔ´\$oöÝwcì¸qXôÔS­n7:6bi/¼¶êsºŒÁl±]sm�…�QiÁçr®þ}ãrÀwcÛ{yy¡—Ÿu¨CŸÀôU@.óÁ·I)ؼs?@.@&nþ;g}ò@¥)m¶Mhõf¤ŸVÒI¤„é Næä 'ûD³Nâ<¤2"#!jØ0 †ƒa˜"œÌÎF~~~£éšˆs‹Å ÃÐáÃ]¾ú>!Ä>Ùàç-ÂݳîiôœF}CC|m�«êÜQëMݵ3LDDF""2¾ò^íž½¢)Ë—½‚’ Ú´î‰ÿâĉÛ”p ®¥aÏšˆðôQøCæëƒ¾ŠDôiò5{Ó�Ú’ õÛèÎaA=ò»‡�‹K/ ´4¾¾¾”p Ρ ?{wï²ÍïMœŸZ¥ÂÞ=»‘™qѱ£00"‚‚ÒÍ´Z-¶mÙL =;§Y™ÈÊÌ@ÔÐaˆŽ‰¡ÞE�(557nDL9ýFº=Ù0çÞyMörš5gŽÝc¹LÞêçÂØØ€¯ü­Ù_�R¼XUxõ*<»xIË7’—/ãûÂÐaÃÀápš]o@¿0‡RA:^°"#¢"ìz-´EaQ >ù&™ØÍ"ƒ¼±)å°j͘1c%ˆãÛ¶e3Næäس¼d`„�-W€-§êÔΦVY„Z%ƒÚ’3¨«PÙnr™¢ó˜?•ÔM òó±uËæ«=¸|°{‡‚Ó»XÞr°„ôë¸3©«P¢¶B‰Úâ3¨-±VÏÊÌÃaJüTjAˆ‹j8Esí¼½3{ ެß™½ÂËÛGÆO?þ€Ö~d[þÕ_à«/¿€ÅbÁÔiÓáîîN'µR•–cÚÄ1 Ûž WªËñÚjJC(á@®ƒÑ`@Ê_^ýµU(†[d,8Á‘'Æ–YE‘±¨½�sÖ.@¯ÅÉœ(•JÌž3—~�íb9ÙÙØ~¥0pÆ€ÓoX<:Ίå%ÇKNp$êôZÔdîBmI>Ô*Ö''aöœ¹”t Ä“ ×3Åõ‹Å6{ÅãO<�o¼/ï«3lß¶ «WÙ¯ONÂâ¥KéÄö@:}56ïÜ�{fNjãú¬ú$Ñ®Ð#!¶{ iζ­[lÉ–4¼¸”lpµ€Þ¡àÅ-; €u˜EÊ_R`º+ÙÀ僷n‘±”lp¥äƒP î¨;á62€µÒ÷úä$¨”Ôåß™¨´xúø!0HAÁ �x‹E�šl¨‰¸)ñõC.iÿ߉_×oÀè1cðKòÕºß%&‚Ãá`Ý—_áhfùÏ�ò×_tr{¨õ)Û¡T—·°F�í_Ÿ~“ŒB¦„‚F(á@ÚnÏî](È·vfG€7þ^ºrÕ›!žÜQw‚l­áPÌ0س{¦+n`”Jìݳûj²aòƒ`yѯޮŠ nì�¶¤ÃÖ-›a4P!-g¡Ô öíÕîîðÄuõôÂÐPY§'®M:ŒܯÝÛ(bŠðÆŠ·lý{¤R!+31±±sÛmyxàñ'áBqq—ÇÓb©ƒFgBXXX›Ö/--ÅÆ�¡�Š àrè‚ì@¿oÚÙÒ7HÖ)1˦`J8�¶Ójµ8–™iý(ñ’ÁmÈx JÀ9,i àXf&ýúÚöîÙm­ÙÀåƒ7n.Õiè t{‡Úz:¨U*dfdPPqòdCs³QtvÒaÁ‚ùxò¡Ùíz= €§‡‡íñÎ;PWW‡±cÇÙ–UVV‚Åb]×v¦góNC!µû½éŒ5Èa*°xñâ6­¯V«‘šš …T—nm:‚HèŽ'š�Ç´|]ïM?Šõ)Û)`„ä:o‚üºÍuõlèII‡Qw\¾íf˜tž‚ü|Û�%NØ0êÙЃp‚#mɽCÓiöBPÄ A�öA¡²ªù›2¾|Ï7¥¦"÷ôiäœo_²cî¼y¨¨¨À�?|“É„1·�Å+VØž¿ùæ!=f4î™3‡N²‹“K}°`ö Œzµ0|aQ ×§ çt�õzÛ¹S'ކH(°&V}FÉB Ò>E独_†½dÔ»¡§Þñ`yÉPW¡Â™¼so”¯¯/üz ¯ �‚AºÔ'ß$†Föëô¶\Ÿlø·°Ì®†Ñl�¹Ž�`E@ëŸ?l6=õ=õT“Ï“˜H'Õʼn„î˜:qfÏŒ³-ÓW�˜ôö¤µ[W§¯FbÒ_xòá9HLú‹f¤ppŒZ‡™“Ç¢_¿~q<”p v´Z�õ¦“n‚zö�P@,*¨®ü O:¸�i4¶Sí×U§×¢N§ia…«SŠåŸÉƒ\î×ìª ‡7»yá:ë¸YoÆkuðµè\:æ111pxâ‘g—ÓHºÜ×?üЬ_ÄM‰GDdçL#^^¡Á‰œÓ8Í”5î¡Ô í%Äì¹÷ÒÉ -3³gN‚¬Aý�M;`CÊöf{.ìM? ½Þ@3R8IÂaèð[N âxê»y³¸T(’X§î#Ï`4\I6È(®Ü~¶%æ¶µ¡}{ö´y»ëEíïuä^kBx�³õ™.Ÿ|èh /\8sÓDˆŽ¥º+¤‰Ïv³ùµÀÖ-Ök¦ƒ“újÞùðœc¨¾iŸ¾Š,˜;á!¶e'sÏ"1é/œkC¯J6J8B:ÛÛ C§±ýZíÆ§`¸2³ã%ìªÙ<ã"×MŽÙúLÄÏÑy"¤)5Ö„rKI•RÙh&(©L†qã'4»Ýò Í•dƒëwe ¹ˆ òÆÂ… ±nÝ:º¨:€HèŽY3&aú¤ÑW¯ÕÒr|—”‚ÃY”D ”p „t5.�b@H™={6»?ÙP]�ÜÜ\ìÞ½Õl¾ó¸Õl&TçÒI"äZ­Æê*ˆøn-ÎTÑRÒÁ'#«É„îGp8ë´Ý2£9Ÿþ¼¾ZÕhýM©©8‘sšz6�v™6qî™g›yB_mÀ¦û±iç~*üH(á@!„8»ÀÀ@‡G9dÈÄÄÄ 11ÅÅÅHDbˆ©˜†W¸ˆýÂZ]çTÞ™N?ŽGz_›èÔ±Ì9qêâË=�ÃT\÷ë•6îÿ}ÂŽ6š½‚ãÆ£n[›«/yš)£ ¼ …Ï=÷~ft† ÈýC°`Î ôUô¶-;’•ƒÄä”vM¥J%!„Ò¦/ç>ø P] $ŠnÁóÚÝÑ …ÖÔÏxE®Î^=4|AËõ±äöìÞeZÑÜl}“;hjÀAbrJ—íS("<<Z½™.8´mšKBº›B@!„ô¼¤ÃŒ3y\?èY\ J¡R©Ñ?………vË 9 ?JKKñç¿ãŽ™35x¦ÄMŸü0™Lƒ#GÛž¿çî»�{úêð€›##P\Ì`@¿0¼óÖ ÛòsgÏâчÂÈaC1û‡ó”וl�Kð@56$'µºnþ™3HX½ Ç�Åüûæa}r***¨�8©‘Q‘XµüÌnP«aÓÎXôÂÛØ´c?¨‡‰é/Ç×ë>Fjj*%!„Ò=Ö•(æzS@z�I“'cïÞÆ ‡é3fà»ÄoñâK/!vÔ(…BŒ9Ï-^‚¤_~ ü÷™gÐ/<B¡<°§NžluŸù˜9s&ÜÝÝ!òðÀ  iý†s¥Æ€ü‹Zl¿2{E³É K-^{ï³vÍFÁçràÆ² ˜aZ]7íÐ?XóÁ‡¸kÖ,\(.ƫ˖aLl žZô¶nÙ#M‹ÝíDBw+Z\§¯"¯-yKž\™Ô€ušË¥¯¿�Ĥ¿¨($qTÃ�Béáô !®¢¥Â‘õõ&ÅÅ!aÕ{¨ªª‚‡‡t:²Oü‹�>þo¯X�蘻×Ýrë­x?aµíqDÄÕ±á//TVV¶z\£ÇŒÁ»+Wâæ!C ÿwnêg¯õÃb©kræBI—ÌFáåå…ø©S?u*kBhz|<Ž9‚];wB$aRÜd̘9·Ü ‡C�«‹-zhd¾^XúÆM&#hšKB B!„¡3Ö@æë ±DâÐÇÙ–¢‘èßìß�)ññHOOCLl,DÐëõˆ9¢Ñkx¼«Ó%»»»ÛþÍb±Út\o¯|ß~ý5¾üb–½üF��W–¿Š   —tÐêÍ0˜-�ž+)ÓCu%)ÑÕBBB§¥ãÀþýؘšŠm[·àÏ?~‡T&ÃÔiÓ0}ú <˜> ºÀƒsoÇȨÀØ˜áØ›~ÔöÜØ˜áxpîLÛÐ Ø�²ƒ¦¹$”p „BqV…Ê*Œ¸%‘‘.ñ~â&OÆÞ={0%>ûöîÅ´éÓžžØ¹{<<<:tîîîXôÔSXôÔS`˜"Tj+±ôùç�´á×w-5•l¬µ,µ–n=6�‡ñ&`ü„ Ðëõس{¶oÛ†ß6lÀ÷‰‰]2 ŠÅR�΄áC"Ú´~ii)<…T•ÆÐl|�ÅØ˜á˜6q”íñ‚¹3qäX‚ƒhšKB B!„G â»Á`¶ÀR[GÁh¤ɓ±öÃpûw`Ï®]Xþêk€Aƒ!3#cn»­Óö­PÁh4âÌ™3t"‹Å‹Å›Ýö’o S„³y§¡�ŠÀ¨uíÚ¯ÎXƒ¦I?.nÓújµ©©©PHEÐèMN�pVàɇçØ-óºcÅ‹O"0À϶LYZŽO¿M¦™'%!„BºÃÍ}}Pc©ENQE§L+èìúô郛BB°zÕ{;nœmÈÄ Ä›¯¿†·ÞY‰AƒC«ÑàçŸ~ÂéÓ§ñù_´iÛ~~~ÈÉÎÆM!!¶áóçÍÔ©ñ¸ãŽ;áÆå"9)É¡»ç)à™ë‡ü‹=+)b±Xp0=©©)عcô:<==7y2¦_™J·%ÅE òsOÞP¡§’I}ðê’Ç,©`²TŸlÐW°>e;Ío½³Òaã¨PAìÛ J͉qÝœ?pÛ¨X”––‚Ïçã¶±c1}Æ Ü6vœ] ÒñDBw,Yô<êë2Ô¸¦>J…¦Ï,[Eu%!„B(éÐõ®w|}}M…kM�6 S§Mkó>®];jö8`·,"2?'%Óé *++qèÐA¤8€û ¸˜‡ÃAÿ0}Æ Lœ×áuF³PJ8B!„t9___øõ Àù"§G&i‹˜[F¢¦ÆÚ†DEáÁ‡”ø©ðõõ¥àt±†3R´fÁœ”p ­ÒèLèì0í™M§„B!.qƒé·ß•öêÔ‚ ,p5é âÓo-Ä51j*ê<ðìâ%­®Û÷¦›ðÌsÏaçî=ø%y=î»>%ºÁµ3R´F.óÁì™q8Ò¢¦Óo¿ 111q<ôW—B!.«þ ×wß}wC="^¸pæ8¦‰K�%N-eã& B7kjFŠ–(ÕåP—–£Šj8'C B!„4éÒ¥KHHHpªcÖVê —ì–uTÒ�ÒùD|7ûy !!Ï?ÿ¼k¾G¡û53RØ;™{ÊÒ2œ+*Áy¦…L ‹$N‹„BiRee%*++�î¸ù\N£e”t ×C«ÕÂX]ß�®‘.Æá° ò�——çTÇ,÷€HÐú­U¡ÚˆW/„‡ÐÝÖk!'·…E%¨1VÁ¨«°[_g¨�ˆ[›Km�SŸÛˆ ¯V×ÑjP¨¬º®8kõfTÌ(¯2Q¢„!„Bœ‰»»;�òØ£¢¢(é@Ú%çÄ ¨‹ ,÷@SárﯵéS›s½3¡t©TŠéÓ§ã³Äd˜Ìµ]ºï¾Š ¿RðQU”“A×êkf̓ÂâËÉ…Ù|õ³§O�?´êK¨¬ÓÛ­/Z§$UH-8sQ ­ÞìT×Úˆ¨+òŽ·º¾ÌÇ#n�nöù¦â\#�Þ„‚‹•0˜-ô!F B!„8ƒÀÀ@,^¼ØeÞ%éÄÏ‹ n*í�}é]¶O___̘1/­þ¦Ëö)ºcÉ“ b[¶g7*¥²Õ×NŸ8ºÙçr²³‘“}Ân™V£Ee¥|.‘AÞ8}Aƒ²J£Ã_ }XòäȤ>¶eë“J[O8Èå7~B³Ï7gµJ£Ñ‰�‡ÁÁÞô™N B:—ÍÂ’‘ ¬>ÌÀÔ�]ÍEàÓ¬’Æ”¸lÜ&Å-þbüwW>�0Bm­Úµ3BIBÚ®¹ž ú…µø\kŠ hõfü˜ò·ËÆÎZa!ú*zÛ-oé&¹­""#ÙhyA~>¶mÙ £ÑˆÐ^žÈЙzxE_E^]ò8DBw»å³çÞ{ÃÛn.ÎÓÒpè`:Ü8l„ø{âßÂrj肦Å$N/:@ ‡�Q�’n=™;¯Ñ²[ÄX<"ç5F:Q„ÚZ'µ5jg¤½Iš2“¸¹DTcCr£“(¤"Ü9å¶FɆΊ{æÌµ}^õõótÈø¸È%Ì»;¾Q²¡Ó¿£ÄÆbìøñ>žû÷­¿_¯û©©©”p ä†/`0¾�7~ȹ„±A^pc±š\ïf¹Ëbú a\þoX ž|„ú‹‘¶g;ŒC—ï?jè0He²+1¢ÏrGAg‚8µ[üŸ¤3¡ Â€ U&Ü FÚ57Cä˜â‹Ä— Ô›!ááAþpc³Pß-R*ÂŒ)~:yg+ª$`~¤Ê 58S^ KàîÆÆ´_|—} ¥ÕfL öÆÝýdø(óžÛS€µBuåþúß‹t’µµ+míFÛ€&Ûµ3r£IÀ~xEFA©ÓWƒ'¤½ª««áînÿë´N§›Ý³§äó®Þä‹Åân9¹ŸNæäÀCÀuÈqج+±âƒ/tOŒär¨U*‡MÊôDÔÃ�8-€IÁÞØyÞ:Fkga&öñûš^Çyá÷<˜J#Œ–:d^®ÂßL…]ï†qWÖ9]¦‡©¶ùÕH9SŠþ°b IDATÑ ºŽsÙ,l*(ÅE� ¦Ú:ìe*èɧA¨­µ±­Q;#ŽjÈ�!ðõõ`íé hbZM�±<�b‰„F\–ÄË Ç�k´üXV¼½½{tlö~ê®›éú_ï�X"î¾}‹­ŸÑŽš”¡„!Nd¨Ÿ'tæZä—W * Кj0ìšqm2w. 5öÝºŽ«ìç÷ ôä#·Ì~¢¼r=úˆíÿ i¯Ž×›káîFMˆP[kk[£vF‘^¯GBBJK­ÕÓó/j›,Y¨¬‚LÚdA7B\Åðáñìå—±g÷nh4TVVbßÞ½xuù2 >ÜaÛp^^ÄB®ívW%—É1kÎdQAÄæD „áÑ£)F„†T§××›ÏÚO¯³½° ·‡JqäR¥m™ˆË�¡Æ~^æJ“ýü¼|+o»©Ñ>j®éRk¦.¶„ÚZ»Ûµ3â¨É†ââb[²A©1P`H�õÔÓÿŽsfcÑã í–s¹\|¸ö£V_¯ÕjQ^ª†XÈ…Von×1hõf¤ŸVâľ?Ú´>Ã0X³f "ƒ¼‘]TÞîý:¾@`› „4M,ÃG*£QÂ��3X&‚¿ˆ‡Gù7ùüÍ2Ž«t¶›‡£åêMŒÇ5Ýe 5µx-­K-—�NjkÔÎ%ˆ³ RÀ3×ùÏô¨÷ÝÀüøó/øàý5ÈÌÌD­Å‚ÈAƒðÏ>Û¦Þ=9'NàÈÁtDy#ý´’.$B%ˆsŠ öÁϧ”8T¢môÜ0?OÄõõ±Ý©ô&KÈ-«¶­3´—}WðóZúz pªTOÁ%¤“Úµ3BÉâ,Š ˆ}{A©9ÑãÞ{Dd$¾üúº!”p =S!Øy¾ÍÝgÔØq¾“ûú\¹ ҨÓQ½áîÆÆYM5¾=q/ÝÚÇöšS¥z¤”âžþ2È…<˜,µÈ-ÓcCnÛ»þ|R‰Çn‡ ¼ô÷9ÀÚ ¡vë4||íš„¸z[ëˆvÖT[£vֹЋ‹‘��à´ ‡·ß~›’ „B%i›3Š[]çP‰ÖÖ<Г�ƒ%Z¤]¸ú+­\ÈE™Á¾˜LÖå*d]®jv›Mݸ4\vºL�eεúBzr[»ÑvÖT[£vFšS?óDG%X,µq¶óͨuðôõÃëKžhuÝýÂÚµ�Sygzìõ Ñ› «óÆØ˜áÔ8ZˆÑ@Ÿ›9ˆ‚Ñ�ÒO+ñõûo`äPÇ8”p .ïöP)Δ뱿X“¥~"f…˰»¨‚‚Cµ5Ò‚°°0,^¼Ø©Žùpæ ¼öÖ»PHEšlPHE(9w_q�<¶�.Ž@UZFA 6Z½ðÛ}Ç ÕâdN6RµÎ!c$öíÕ­Ó3LÎæ�vØõD”p ./1û"î“á•è>pc³�[¦Ç¦‚RTP7ZB¨­W×QÃ(Œf jÍ&hÍ&0LŠ ®+_7ÕÎÊqÙ÷çÈ=D|7ûy !!Ï?ÿ<]Œ h48˜–F7Ó-(.b�Ÿ{’bD BºŽÎ\‹ŸN^¦@Bm�ô@U³¡´Òˆ¾~uà°YHùóOÌ_ð Äb1ØÅ¨”Jìݳf¶:}5¤p8,H„<§-VK±Ç¦B!ÄUuT�HKmN[‡™ŒFlHú*¥’ìBr²³±>9 Å ƒ gOAÀåPPœ€T*ÅôéÓÁ¨u0™k) „8J8B!Ä¥uÔlZ½ùW¦‰ÕjµXŸœ„“99`�•‘�í[·Àd4 •U0˜-˜ë¤ÀMýúwiWv___̘1ŒZGçŒDC*!„âR, æríè©/ë·ê/†ÉhĶ-›‘“}cÇ�‡L.§à;†)Âö-[ Õjm×ι˕4]j;)AÐêÍø1åo !%!„â"ÒÓÓ±mûNøŠùPj ÐêM�rã¨Ô 3Ô Ô_ ‘À Å ƒ‘nP� ÃáPz:ŠƶÌ`¶ ·X�±Æ)ß“XÈ…€e¡ƒé¸5:†N2!=TD�6¥ü�C%bbºÿ³€†TB!Ä%”––âÒÅ ðp;¼gõtÆ/,£Ö¡¬Òˆ¯“7ãH ¯pÛ6o±%,µu`Ô:d”:m²$B0á`Z�àN �ŠàŪÂû«WQ0ZˆÑ…3DZ>é F7\ºx¥¥¥q<ÔÃ�B!¤�®ŽU× çtäRÜ3sFFEBè.`�ù€†[8ët—ÙÈ/)‡TÄFI™%ez»a8„B:%!„B:ˆR]†O¾IF¢0#£"pKTþÞ¹b±!¡aP)F�êdÅ ±Db›ºôHVŽËÆá¬èôÕà°Y((Ñ@!�Œ„B!L§¯Æž´£È=uÁrhµZdef +3<> … E êýÐTJ%ŠùùglC%nê7J«�“{Ju™Ýú”h ÎH.“cÖœ9xmÕçŒfD „p�˜œBÁ „!„Bˆk»\Q�K-|<ø�ˆxà°Y0�(ÈÏGA~>€ÇçcJüT„„†RÀÚÀh0 ¸˜�J¥STdWø±¡ÿ=Ž Ë)`NF«7#ý´'öýѦõsss±fÍÄô—#»¨Z½ÙecÃl3��¦‰ÅbøHe#J8B!„¸>Km”ƒ­€¥�'bw.$BDë×0“ш²NÂha#8(ÀVû¡á ¶J¥B BA`4™�òçŸM>§3Ô@£7¡¬ÊH7Ýà`ZLGL9ÒO+) „J8B!„t•²J#Ê*�›‰ˆ±;é§÷ã�mûr©‚¶ÿÔœ>y€µ7„\.‡X,†ÄË b±µNŸÏwÊ¡*¥F£Z­Z­ÊË—a4!‹19~* °¨ªÒr2%¶ÿúúpÀa³ Ñ™ ­6Cg¬�Fg¢¡„B B!„b©­³K@ÔSªË T—ápV6 Ô_ ¹ÄÚëÁd46;„`Öœ9P(‚š|N«ÕZ )^)¢XO&“�/Üð{ÑjµÐj4`W¬±)?|—µJÕâöÌulì:œ‹sLIÓûÓp`0[è""„J8B!„�ö*Vë ÔTÃCÀ‡Í‚Ø� 7Û6$£^br ,pƒÜ×2_@°""¡;òsO¢ ïT«ûj)iÁ0Eø59¹Õm܃èØXœÌ=k[¦,-ƒJm­§ÐR²Ag¨A�¥Újsƒ)G£d!„PÂ�B!¤ËÄÄÄ –ÅÅ{Ÿ|ãRïË`¶À`¶4Y“€ÃfÙ:ƒ –Ú:ä4± …T…TÔê¾ê“Má m8Þß7íÄšoÿlöùP1Œf ,µu¨2˜m‰qcTþŸ½ûkòÜß~‡�A ÀEp1,`뮀u᪭Ú:zÚcÇé:�®ÓÏiû«�§­vOkÕNÑö´œµn‚Š[° ¸.†�hBHHøý�FQ A2îÏuõjÈûæIr?yÁ÷›ç}ž U0œ2–a´€J‹–jtVw`2Kå\y§•iaáûsÑ¿wˆˆˆˆœE¥R¡}‡Ž^5Y ÕVݨ÷«-1@[b€\â ¡PPûƒBÈ%¾—NZKë=  ¢”Öy’qõc¬VÇ…ƒü3z~X[€ÉbE„õŽP¡æ)Ò™ÜN†IS¦¶Þ ý¥ d2‘Kþž+Ò™0X�¡Ã’Zí5èõz”•–¸lFÞˆ"ºžÅÌ ˆˆ<�¡²ªÎû¯�G¢¾ZG—99ÃåÂVzz:’““ÈÕ'ôÅEø)5qáA\¤9"+SÃŒ\ˆ# ¢kÙÊÎ1„ty¨aµ¥’a]E,ò�:XŽ´´4†AäXp Z‚CB.�™A"‘0„ •\’¬+fDDDÍ “ÉY³,ª•󀹨…B °i†³žÊp¥EN>ΔJûíêr÷#""ÏùûØF�áæ]š©V«1kÖ,ähËë½dˆˆZ TKx§K éŠQmä¤NÞ¨Úl²óÅ@Z@DD„ý¶­˜Å=""ò ±qqèŸ89Úr†ADXp kt‹ˆ´ß¶;È@¼�5w·ývX˜š�´‰Tj=bÍÝÅ@ˆœäÈ‘#Ø»;«QË?y…L©ÀŒí™†AäÅÔÁrìÝ�…#GޏÄëaÁ�jÿ±R(е[Í·¯ÖCŽrð2Õf¬y5‡Žaa\G¹%$¬ÉÜ GUv!r‚ÜÜ\ìÙµƒòJJ™R˜‘™Á¿)-u(¸ˆ÷ß{—a8ÈèTÞ~,[ú#Ãhå~سkrssYp ×44éÊÚ¹æ­ÿc ^Ä’ñ+piå„Ö\CÙt‹ˆ@ǰ0€5o7çr """"�Â]G¡P gïÞ5?èŠaÙ¹Š¡xËÎU¨¾4Ÿ@ÏÞ½9ºá&:,©f%K%Ì—rDy¨Þ¡Ë—VØNäÔœ ™¹T¦'ª6›`Éø¶9j.¥à膛#$4ƒ‡«ùÁR óÚÅé@DDDuR*1 >ÚèGX¸]£º3#‚Õkô�wÚ'¶«.ÖÂòûûI)yÛ©üš~=• fÌqw�g07Ql\F޾³æK%Ìë–ÔÌŸÂy!ƒ© Ùe˜1cF£ö?räüq$t…B&òèl ây2í€ZŽˆèfäB|ÕG"•â�ibíêU8”“ƒjƒ¾æòŠ#YðUGA¢†OW1p»"C±ÕeE¨:~оü%ÄÄÆbÈÐa�H¥ ©ŠR©kW¯BeeeÍ$’GvÁ§c|:FÂ'¨-2ƒr#¼<†ˆ¼QfF¶gj�Ð=šÃ76bÏj«†ÞhAtt4%bÁ�¼Á¨;Ç LŽÌŒ \¸ tŨºêD•Ü[@@Mµ<6.Ža´¢n¸ÚƒX³j%N–JØNäpT¹-¨Qbã⇜ìläd¬9!"·‚¸=ËQ .B¡P`Ò”©Ðj p(;GóóQYYÉ`ˆˆˆˆÈ-±à@Mr¹ðPi2¡¨¸z�z�ŽÁ¸Ë ­R …R�Ð�P\˜Zµ: Õ ´ ÆMHý¤ ÁO©© ƒˆˆˆ¼ tC$RiÍ §p jQ— —ÿODõKHH€M Â;Ÿ~Í0ÈëèŒf„TaHB_†AäŲ Êð�§FB|?—x=\¥‚ˆˆˆ<‚J¥Bû¡7Zy½ÑĈOLd-@[b@yµ?¦ÏšÝj¯¡ÒdB¡V벫qhK èy&M™ÚzÇ�^�²Ò�_±¤¡ßí;t„J¥r‰×¹©Hu°ééé ãEÅEXžºqáA £9"K³…¹ˆˆˆˆˆÈ%ˆE>PË‘––Æ0ˆ< DDDDDä–d2"##¡3˜aµV3"Â5›B©D`tóM{NµZ�Y³f!G[Ce;�ÈÅp• ºa…Úšeúôz=ôz.�éòÿP(¡P(aj./â.ôú+KÏriLW'KÊ ˆÈ+ÅÆÅ"?¬z÷ †ADXp &:”“mÁIäççÃ\YÉ@Ü”X"�Z­FDdbbc½: �¤ 2%]0ɰjk™K¼&½^�=»váh~ôz=?°D�TZZгgNA!q¥ ò:R‘¾°¢P«å D^L,ÇÞÝYPÊÅˆŽŽnõ×ÃK*¨Q´Ú,O]е«WáPN‹ nÎ\Y‰£ùùX»z¾]²Zm�÷Ä]�+j‹yŠaÐ [wFc½^�µ«Waá`ïžÝ,65õxÖh°rÅ/œ�œ¼RˆR A–§.e- T)…?˜oCæcã†?F+öìÚ�ÜÜ\—x=á@ Úž©AfFÆ•;d ø„ª!ìˆÄð gHnÂVTX̰�„íT>`Ô£¤¸?¥¦¢Wï>2l˜×fSá#Æ<Å0ÌÔoh•‘ÅEEX–º´V1ϧC|Â"!�) ð„@¦à‡Ø T—¡Úl‚eS*à "ò‘¾«ý’bª;#s…ÅEE ƒXp Æ¹<¢ ’À7.ÂÈ> ÆM].ùtŒz%Áz"U{7–ÊKߨë0jô��H¥,:ÜÄ¢CNv6Ö­Y}¥Ÿ:ÇÂ7n nJ c """â%Ô¸bƒ 0â»c±ÁÃ;ÇA|×c†ŽæçcÓÆ ^™…ŸŸ€+E‡›uy…V[p¥Ø ’@”8¢þcXl ""¢Z”Ê@ ˆO€¶ÄÀ0ê®FרîÌÈ…p„ÕiïžÝöbƒO‡øö¿±”Áx �X ñÈaÙ¹ ¶98”“…B‰øÄÄ›òü¹¢Ö�Ñ¿ÂGTó*, X²dÉMéP\T„¿þj/6ˆ‡N� �+‘÷2[lЖð䃓µ¿V«Å²eˈç.zôÒ˜ …ñ‰‰˜¿èW~Pê¡V‡Co´à»[ äªôz=6m¨ù–[Âbƒ·ü2è9 –ò"T—c{¦±=zØ—Ñl)¯)G¡Ð·�Ëd���7­è°iãûœ ¢Ä»Yl ""·–“��¬¬,Ć"§ ü†Ú0Y¬Ð–�’’Ò¨ý�F#rss¡”‰!ò‚6"WÃK*è:™Ûì·E'°Øà%b)D'\9¾ 3 »R±¡W¯^ö¢Ã´iÓ´ìåZm�}â)aL'_%""·§×éPVZ¥LÌ0ˆG8е(ôú+—RtŽåuä^F S@“ë! ŽæçC¯×·ø(ˆ��G|||«½ïàà`¨T*ûÏ7c¤Ãž]»knˆ$Fqn""""ò<,8P-Ú‚‚+ޏ� Ä »ö€õ�p4?½z·üɰJ¥Btt´KåÐ’E‡J“ ÇŽæ×äÙ‡£ˆˆˆˆˆÈ#ñ’ ª%?/¯æ†LÁÑ ^J SØW­ÈËÍõê,ZêòŠüü|ûmaX$?tDN…^}úqvròJ:£¦j1Ä'0 "/–]P†1)ãí_žµ6¨½^WóÁèÁ0¼˜O‡š“àââb¯Ï¢%Šz�Î~›E9Ott4z÷½�òÎÃ-0A|ÓV™ò6ÚÊ«ý1}Öl†á £Ž‘·aÒ”© £•´ïбÖåÂ,8�Ë(¹t‚)qˆ7Á¾‚‚·KHH°Ï1Qá#Fª¬—SÚ½<’„ˆˆˆj}PÈDÈõòQ–užÐk ðþ{ï"¡;¿¬¨OfFÖ¥ý�¹ˆèúa%C¸ŠF£Aff&À¯Ú‚ÉÆ½ÍþÀWÂp‰ˆˆ®"—ú".<óæÍcD€"ºçï¸B£Ñ`É’%jŠ 3u8uµ """§ýý¹ˆˆêÁb¹“â’Öý¥P*áë+º©Ï� @s¸z£…"Ãe1‰Èm´Ôõœ~~~P«Õµîc±�ˆˆÜMQÉyœ(8�ÎáZåùcãâ�•s ›¶d²3ˆ Dä&>ûì3ìß¿¿ÅÚŸ4i’’’°Ø@ä®JKKqöÌ)(d"~ÓI^ë§kÑ6@€Þ}ú"6.î¦>÷‰‚ÓXùûVvQ+RÈD8{æJK;¸ÄJ,8‘[hÉbìÝ»III,6¹1�Fƒ•ééˆ ‚æp!¯#ôÀráJLB¬[³Góó V‡#$ôúûîÙwµâ¢"T6b¥ª«Û(*-Ãg‹R=ú$N*0c{¦âøa«'£ çÏ!';û¦»èЏð ¬\ñ 6 RRRXp "jŠøÊãˆ7sZ{Ëä½Qèd?Ya±�ˆˆÜ•ÕV�ýÇÏ#.<r©/Žæçãh~~�ûNŸ5»Þv6nø§ |¾é³fãDÁidíËÁÊõ[a0Vxl¶J™R˜‘™‘Á‚ƒƒŒô¥g‘“}�bÁ�ˆÜ“ÊzÑUÎûæÒÏf²Ø@DDQtÈ.(ƒ:X¥L ¹´îîßû·ú ±ê@(åâŸËQDD DD€ŠŠšoeXl ""wgµUãDÑEûÏ YÓVŽ8QtB!—Øt5R‰ÃÂpèÈ1†Q…R‰À6*œÔža.‚"¢KXl ""OÔÔIT •U­öZÍ´%<ùàäFí¯Õj±lÙ2ĆâĹ‹­úÚ[ZHh(&M™Ê‘%ÄÆÅ"?¬z÷ †á"|‹ DDD®Àd±B[bhôdwF£¹¹¹PÊÄ•Aä‚8Â�ˆ¼ZJE6ÒL6îe±�ˆˆˆˆÈ‰Xp "¯]U„è ‘“ñ’ """""""r:ˆˆˆÈ#DEE¡WŸ~Жy%m‰½úôCTT”K¼ˆˆˆÈ#DGG£wßÛYp "§+Ö™p±Ú÷LžÌ0dܱ†Kb­H[b@ï¾·#::Ú%^ DDDDDD˜,VTAµ:¼õN$µxÿ½w‘Ð=Ôe3’ÈüÚz¯/3#ëÒþç²y#ˆˆˆˆˆÈ%(d"$tÅã�?Î0ˆ< DDDDDDDät,8‘[R«Õ˜1c² Ê`0U1"¹%™L†èèhè�XmÕ „ZÝÏ‹>ÀCSÇ£]ˆŠa€ò"æ ‡ØGЪ¯ãï½:ÔùÚÆF¨ðrB'ÌÚ /Üޏ`9;�x¬9ñXãqFDDD®"ªk'Ìxâ¯X·üK,úðuL¸k8üoÞ¿K2Ξ9…ÒÒR—È× rwñ }00L‰ å­ö:BüÄ×Ý÷@l[œÔ›ðáîBÌ6¨ü|11:þb!¶ŸÖ³óˆÇšŽ5gtYZZÒÓÓœ�<»  z£¥Îmê`9Ô�(X9jC!!.<¨Á6´%†z—ðtF5S»Î`F޶¼Ym€æpQ½ÛbÕ�PÊÅÍjÃ}ãný댾i¨ gôÍÍìßÖ¢�‰ ˜±=Sƒñ ü…[OFΟCNv6bãâ¼>�@€¾=cÑ·g,þõücغ}7Ò߂͙Y°XZîòŸ¸ð ¬\ñ 6 RRRXp j0¬S¾Í9‹iqí°E«CUõõÃén •clD0%¾(ÐWbù‘"Ìì§Æ¬�Gqyïþí0,<!2ÊMUX{â|”�MÚrü/·°ûìä”í¯éœÑ‚af5O„È+�µægê<ÖxœÑe2™¬Qû½ðôÃhß!¬Îm»³v`ïî�ÍjãÌéB¬\ñKƒmŒ5}úÝÞbmÀW_|Ü`Ñ�1sæÄfµ_ðZ½ÛÒûgÏœnVÎèwë_gôMCm8£onfÿ¶¥L )ÌÈÌÈ`ÁÁAFúÒ³ÈÉ>èÕ‡gç¼…QÃ1$¡ä2?€X,BÒ H4.ñûf Òߌ]ûrP]íÙ—±à@níöö œ5˜q´Ü„S͸½ƒ§tµöéê�»º©°øàY͈ –ãáíáë#°â‚åHéŒï�ñò „+¤x ®-ÊLUÈ+«€µðóõÁ]ÝTX’}¥ Q!øxÏ)ÌØx%EàÙ?òk=÷þâë¿Ù¨´ÚØqä•ÇZs�3uk<Îè²øøx€Ñht¸_€~P©ê¾¶V!¡cû†¿5vÔFix{l–ÛˆŠŠBttt‹µg““lC¥R¡_¯¸fµÀa– }£†÷:jÃ}ãný댾i¨ gôMKôoTT©‘[Ú˜±3vB,a`ÿÞ94w dßà/Ä»†cÂ]Ãq¶¸«×oAúï[�{ô D®D`Dç üðgÍð¼õ'Îãþ˜vÈ<­ÃÕs Äÿr‹¡½P Øsî"üEBÜbßgè¥}Ÿ¯ùGj~yVä•âŽ0%òÊ*Ô\'¾òh)Îj¾iݤ-Ç`u`“_÷ˆÎAØÁo]É �5gÔÒd2’’’šÕFtt´ÃøÆP©TÍÆêŒ6¸L ÍÿFØ}Ãþm™¾q•þ%r%f³¶íÀ†m;ì‡o—§aÄ�û„’íBTxhêx<4u<ò�`åï[ðÕ÷?{Tœ4’ÜVï¶0XlÈ¿t¢r´Ü½¹ }ÚÔÚ/ÄO„:S­ûö_¬õsX€GÎ×þF,·ÌˆN i­û ô•öÛF‹ ~¾M;„îSB­�bõñóì@òºc�Ç9[ii)ÒÓÓ¡–C*zô{U«Ã1}Öl‡sax»øÄDŒL™à²½óÉ×qÏßpÿß_Ä·ËÓp¶¨Ä¾-¢K8ž{ì~�ëŽp ·5²Kë À IDATV«=|o݉󌬳ì÷ÉEB˜ªj¯¾`¶ÖúY"ôÁÛƒ»^÷U×,¯diÆrKàÁûN_×.‘7k<ΈˆÈÙJJJ�––u°:£&‹•¡�ËÛŸsûsŽà�O¾FòˆÁxî±ûÑ.4Ø#ß+ ä–n ‘£½\ŒGz´¯sûm!rûuÝ[5ÄB*­WN>ü¯©€›ªlø¿Œ0µÀuß~¾>¸':§/šñMÎ9vyí±ÆãŒˆˆˆ¨féÌ‘C1rp<ºt óè÷Ê‚¹¥‘�Ûà‡?‹êœ�¾OÛŒìÒÆ~Tl4£³RŠ#ç+ìûônW{(øI½ ]¥ø³ÔèÔ×) ðT¯ŽXsü<²,[Fä Ç�3"""òfÏ>z?F‰G§°×m;W\Š5¶yÜ{æävº·‘Á_,ÄÎ3uO·çÜH…>èÞ¦fy´ŒSzŒ�F;¹b¡½Býq{{E­ÇlÒ–ãÞèDúAä#€B,Dr7¿­C£_WyeÔˆ|öûRºcka9O‚ˆÇš“Ž³ºŽ5gDDDäj|}}1(¾/Þ˜ó¬ý¾GïŸX«Ø ¿p?§ÿއŸ #î}ï}¶ØóràG�Üͨ.m°þdê»<»Àï'Ë0ªK>o¼´t_5žêÕ~¾>8¦«À¢ƒg0g@'ûcþ,5"íh)îí‚P™f« GαüHã'œùáP»­„>Àœ-ǃÔJ Ä_bÚ^·ÿ[Û ì3ñyñæŒã¬®c�ǹŠ!‰ý0rp†$öG€¿ìºí¦J36i²°ê÷-غcªªªœúü:ƒQÝ:×»´/ D øpwaƒûl?­· � ó´§®|K*Ἡöú×{Ï]ÄÞsëmóÙ?òÞwø¼/m;^kûsu<†È›�µæguk<Έˆ¨¥ëLRãÁ)c†ƒŒzÝv+†èÕ9|üæœëZ±}×~¬Z¿ë·î@E…©Åž?G[Ž3& ï,8Ý ã"‚‘WfÄÖBÌVÚÊŸ': Ê�5""¢™,VTAµ:¼õN苊°iãĆ"Çÿ¶š,VHdþ mµ×�“��¬¬,—ȨººûsŽ`åú-X»!e:½W;,8�Ç[œ}wG†àßñ�àë#À‘óF¬·Æà�Icñì¿ÞBÎaÏžüÚ‡""""""¢æ R*ðÅ{/_Wl¸Vhp|ñÎËR*<:ˆˆˆˆˆˆ�Š„ð……Z-Ãp�QeÅEy÷D‰ß7AJl6–ý¶<õOÄ�ù n:î¼Sÿ¾ÿy%l6•˜6eœSŸ?¡{(.øiii.‘ DDDDDD„(¥ðT`yêR†á £’£ظá¯Îap|_À{Ÿ-Ækó`_öa\4a³Ù`0V ûpÞþè+¼¿àÛZû{*ˆˆˆˆˆˆˆœ }»PÀÿV®w¸ßåí/íï©Xp """"""r‚j›  îWUuiU�À£ó`Á�ˆˆˆˆˆÜ’ÑhDnn.2„>ž}â¦V‡cú¬ÙÐ.bÇ×#>1#S&´jF…gjž{˜$‡ûÝ5b ¨¤Ô£û„Ëb‘[Òjµ˜?>âƒ�]PÆå1©ÕmÊØ‰È®á˜þÄ_Ñ®mÒ×mÆñ‚B+L�J%èÖ£†&âÁÉ5“Effí÷è1±õ^ƒ^�홨ƒå.›‘BÕ±qq­ö´ÚË=ì²y#^RAD׳˜™¹%£ÑˆÂÂÂ÷S©Tõ7uF�››Û`~~~P«ÕÍj¢¢¢ü\‹ŠŠŠfµQZZŠÒ҆׊ ƒL&k±6nfÿ:£ojÃ}ãnýÛØ¾qô\ÞJ§+GfFÔÁrhK ¤…Zä9ä%öï…1Ãï@\÷H´ †D,†©²§ÏãàŸ¹H[» »öç°à@DÞÇVvŽ!´ �ÐPœ*,Dµ¥’a9™V«ÅüùóÜ/99)))-ÖÌ›7¯Á6"##1kÖ¬fµ ,¨w[jj*òòòšÕ†F£Azzzƒm̘1ÑÑÑ-ÖÆÍì_gôMCm8£oÜ­Û7~~~øàƒøK�ÜŽD,Æü¹³1(¾ïuÛä2?Dv Gd×pL¸k8V®ß‚½ù!¬V ä¸pAÏ ˆZ�T"­¹¡+fDNv8ïx£öû|q*æ¼÷u�Û2⃚Õ$tm°�]ûrÐcðøfµÀa±ê@(åâfµ¡–7jˆòÃÏ¿½ÑÒbm8£oÛ†3ú¦¡6œÑ7îÖ¿�í›ÆŒÚ€àà`$''ãóÅ©0[l jmÏ=v�ņºÜ5|NœÂK–±à@ÞA¡¬)8XOåÁ7.‘�x)[Q cXÃh‘ãLi¿]mÔC S0"'i£ FvAÃËŒ9:11˜ªšÝ€FµaµV7»�†œ(º¡PЬ6Šu&èŒ _ng0UµhÎè›Æ¶áŒ¾i¨ gô�»õoC}s¢è":‡ú7úµ«T*¤¤¤8,þÝLc’îè/ðå7˱uÇœ>[S¥R‰íÛ†àŽ}ðØ÷@à�»ïLbÁ�¼GdTNºbžy©j³ Õ%5×OªÕá ¤¨Ã¯äj;•ad†Bä$‰¤Þo`Ëj«nv\¦ CeU³Û0Y¬0Y¬­Þ†3úÆ•ú×}ãiý{Ñd‘;ó÷¯™{äù½�¬}Ùµ¶U˜*qìd!Ž�,ÄŸyǰðý¹Qyt\¥‚jéyå—þ±ƒ Ä YswÛoGDF2� P(r)ï] „ÈIÒÒÒ°pÁ'�¾ �ˆ¨±¤"!|aE¡VË0dTYqÅEE^�Cá隹в�ä;Üï࡚‰gOŸsî%¶±á�X¹âh4—ȃºîDèò0zkÞnT›M Å‹T›M°æÕ:†…!$”ÿho)½ûÔ\ÛWmÐÃz"›�¹°¥þ‚ ,O]Ê0dTRx7üáÕ9,O[’ÐÏá~ñ}{–ý¶Æ©Ï¯”‰qöÌ©F­s3°à@×:,©æ†¥–Œ_ˆ©Ú·¸´rBk®3í bãâPsÉRÕÞ ¨./b(DDDDnîûŸÒñùâT¼2ëILü¯ˆ��~R ¢ºuÆ“NÆ›ÿzßÿ”ŽŸÒÖÁÏOZë?OÂ9è:!¡¡èÙ»7öíÙƒêb-,;WAÔ ƒñôbCvl'jÖމ�åü 7Á¨1wâ§ÔTÀR óŽU��XÊ`ˆˆˆˆÜÔ¦_!P©€P胇ï�‡ï«…˜¿Ü“Œ¿Ü“|ÝýŽV•q7á@u:,É~�¹íD,;W1fÙ¹ ÖC5×y‡„`ÈÐa å&P«Ã1rô�5?èŠaÞ¸”#ˆˆÈ«Y­ÕÐ̈lä²!8$“&O�DÊoÙoÚƸ8ÄÄÆÖ*:\=q',[¶ ÿøÇ?ì? ¼þúë7n&Nœˆï¿ÿÕÕÕ ŠˆÜž¡² 9ÚrÌš5«QûkµZÌ›7qáA�K={ð¶D*…Zî”R<•B¡@›àfäBxI9ü¥6iò¬Y½ÇŽæºbX6¥B¢†o—„ª¹l¦ª6êQ]¤EÕñƒ¨.¾2Órǰ0Œ»{<‹ ­`Ô�c¦Ǻ5«K%ªöm€5o|:DBØ%‚@NÞéÍÒÒÒðßÿþÒ«ŽÍùóçcóæÍöŸ-Z¥R‰äädFDDÔŠ“@e¥f‹…_°à@�):Œ?{÷ìFfF*++kæu¸êD" OˆÜ¡ÐP^dŸ²VK$èÕ»'‰le±qqP(ÈÌÈÀ©ÂšÕ+òvÛW (C8ǃ«gf sîòV+W®Dß¾}ñôÓOŠ‹‹±eË´k×o¿ý6 ¾ýö[¬\¹’""¢V¦¿p‘!°à@MÕ«wÄÄÄbÏîÝÈÉÎÆ… ú+-•µ¾)'÷ @l\z÷éÃQ .B­‡zJ8r²³‘Ÿ—W3²èjºb°Nî}N�:…7Þx*•  ÑhP]]�)S¦ ìÒ2Æ=ô¦N�ʰˆˆ\À¸”d¤¤ŒÅ˜ädtèÐ�� D�!‘JŸ˜ˆøÄDèõzh  ×錛Q(•P‡‡C¡àå0®*6.±qq¨4™PT\„ÂôÜ�ÔOŠM68¯½«Š‚™™™€Ä«F% Øl6¯Ï]¥R¡m»È=z‚B"j5ùyy˜÷Þ»˜?ï=ôéÛ)cÇbä¨Ñ d8Ä‚Q£NZ5ߎQ˹<9—(uOÎ*8´mÛK—.ÅÔ©S‘���Ý»w#&&AAAjF@|úé§èر£×gž��_i™þ2?€DäT•+,Õ>è¬nxÄBÆöÈÔh�‘± šmÛðÊK/áõ¹s1hð`$§ŒÅÐaà ‘H<2#±TŽ�P^jÝš4‡‹°ðý¹èß» DDDäXRR.\ˆüÑ~ßĉí·Ÿyæèõz<÷Üs ‹ˆ¨…éLn'ä) _¾ˆ;ÇŒÁ�cÆŽÍGò�wbWVþX¿r¹#FŽBÊØÜ> B¡°Q¯A¯×ãPN6ÔÁrhK .™Ñ`u†Kjµ× ÕàXîáVÍèàæ_šÝF�Áã=æØaÁ�ˆˆÈ…ÝsÏ=8}ú46mÚ¡PˆI“&aðàÁöí={öDtt4RRR¹=…L„¸ð <þøãX°`�G¼§nÝ"[24ضu+ÒÓÒ°vÍjüúËÿ‚1wÝ…ääô¸õV‡íètåÈÌÈpÙ‚ƒ+(,Ð"ÿÈ!fÄ‚5†H$ÂÌ™31sæÌ:·¿ü2/ "rb±Ã’’0,) F£7ü�uk×âçåËñÍâÅø37�!y‘Š >>�JÄý>Yp """"jÀÕä›2ÜùFG�Œääd|¾8f‹ûLž+ øÀÇLJ�èaç~~Rt k�q£‡aø xmþlÉÜåÑy°à@DDäâL&V¬X�ÌÌLœ;weeeX½z5à£�>BLL †Π¨Y'?CûcHb?Ü U�r¹ f³çŠJ�“{¶îÀ[wpEr)*• )))˜óÞ×.ÿZ­V+25¤¥­Àú߇Ñ`@@@FŽ…d^ç**L8œw‡ó¢ ð >|ãE<6ãÿ�µ/›"""ºùt:f̘�“'OÖ¹]£Ñ`ÅŠð÷÷Ç€5Ù-‘]ñŸ—¦£K§°ëÿ¡è'D—NaèÒ) É##ÿxf¿:ùÇ Q#]þý=x`"JKK!‘H0xÈ$§¤`ð�¡‹Å É ýoåzÌyþQ<1m DDDÔ:-Z„³gÏbÚ´i0`ÂÂÂjMùꫯâí·ßÆòå˽¾à�––†ôôt$t…æp?<�p[l4~0’K'<¶íD꯫‘sä( #‚ÛbÄ�<|ßx· BD—p,úðuLy|N�aÆ�ÁË(¼Ï… °}{&4Û¶aÛÖm(,ÔB(¢û-· 9%ÃGŒ„¿¿¿Wg4.%))c1&9:tðÊ ÂÃÚb¢»9µÝØð@¬\ñ ªL���À‚ÕO£Ñà…^À AƒêÜ�_|³gÏfXÔ$b±ï½:Û^l˜ÿÅ7XôcíåÜΗâÛåiX·IƒÅ½Ž°í¨ ÀœçÃS/¼ð—C³ò;ÀŸyÇ0éo3Ñ¿w<ñ×I¸%ª+öeÆû ¾Å‘üãu¾–6�J<8eÅ÷E‡¶!€@€¢’Rh²öaÑ�¿â̹âz߇ºc;<4?/óÞ{ó罇>}û"eìXŒ5����‘@ €Rá�>·Æ`Öß´ßçLJ™gÏœBii©K¼gˆˆˆ\˜N§CŸ>}îÓ¥K˜Íf†EM2vÔP´ ©9Ú±çÀuņ«�+.ÅÜy_`Îó�a“& ›¶í´o3UVÚoûËd’Øï¿ö|…Bûý‰ý{¡g\wLzt& ÏÔj;®{$>ç%*jÝß)¬:…uÀÝw&áù¿ MÖ¾ëOìúõćoü³Ö,ï"‘/zÆuGϸî9$Oþã5ûÉß�>æj•f 'ôů¿Øè÷HÞ¥K×®¸+9ÉÉ)èÆ@ê�±}25ddlƒfÛ6¼òÒKx}î\ <É)c1tØ0H$·|oWã@Î�îkN‹JDDä‚‚‚°gχûdee!44”aQ“ Ièg¿½ì·µ k?Rî ó>[ŒÝÙï·X®œ˜+•ø×ó�áí�¾Bß“1ñáé8}¶ft‚\懿ýeb­6üåøð�¨ €ÍfÃ{Ÿ-Fbò¸}ôTÌ�÷9ªªªà'•àÝÿ›…à6Aµ� ¥ï¼<R‰Æ žþçè3bîžö,Ê ès+½b³s-«ÕŠO¼þ÷xß~¸¼ÜŠô•xü‰'Ylp 00wŽƒ×ßx6oAúêÕ°X,Ø••…éÏ=‹Ä·ãŸ/¼MÆ6X­V�ÍÁ`¬Àüß°à@DDD­#!!o½õ¾þúkäççÃ`0¨Y¹¢°°?üðÞxã�z/¹ ªO÷È.öÛÎú†Má/ǺM¤þº•f3r�žÀü/–Ø·ß]kÿ{ÇŽDhpÀw?¥cIêoÐ_¸c… ËW¬ÃâÔßìíN7êšÇŽ‚RQ3*âËoÂfÍ.˜Í=¡Å¿ßþKÊu�Яg³s-¹ÌÏñ{ŒëÎW3X­ÕÐ̈ŒŒd× Å=“'#» ÌãÞ[·n€-|úù4xÖ®Y�GzCÝ�·Þ|h°�Ø=Ð7þ—Ψ¢Â„ã' ñËÊõ˜ôèLÎ;îÑŸ[^RADDäÂzè!8p?üð~øáûýÉÉÉöÛQQQ¸ï¾û5I�Ra¿]R¦sZ»¿®þ£ÖÏ»®š}½]hp­mCûÛo¯Z¿åº¶ÖmÔØGE$ôë‰Ï-µo»#þÊ¥F›3³j=îøÉBô~ïuíÝÈcšû©i •UÈÑ–céw³µÿ‘#G0þ|$tEvAôFK«¿‡[¢n¼Xògn^½Û$R)Ôêp—x�-E,cXR†%%Áh4bã†?°níZü¼|9¾Y¼Øa> P(Ð&8¤U3â\-,8¹�€€|òÉ'øí·ß°uëVhµZTTTÀÏÏ�:u Aƒ0nÜ8ˆD"†EMb»4ù!øøÔ=i™£k‘ëûGµöÔÙZ?—é.ØoËü¤µ¶u½j)Îã§®këdáiûík—íìÞÑ~»ðô¹F½çyL]®}¬£÷HD7F @ ð��å³à@DDD-F*•bòäɘƒï~ø={õÂÁ°sLJcä¨Ñ^™Ñ… °}{&4Û¶aÛÖm(,ÔB(¢û-· 9%ÃGŒ„¿¿??L€Ä¹°�?þ«W¯Fii)‡m7 !!Éã& G[Î0aåú-öå{ÆFãñ¿:ž01ª[g§¿†Mš+7¦ŒrÝöä‘WîÛ˜±³Ö¶Ì]ûì·‡ ¼½Ö¶.�°÷�åÈXùR¿|¯Y�!€J‹–jŸF-u¹níZLš<[¶e`v¾ùî{´oßß,Y ø-=w „Ô¥K=.#±TŽ�F,Óœp{<ûÔSXúã� Æ¿_~›·e૯áîñXlhÍá"<òøÓHq‘KP8Â�ˆˆÈ…eeeaìØ±˜ùòõEçÎ�ñØObXR’×fÄQ Þƒ"""–žž‘H¡PÈ0 Ñh°vÝzĆ"§€KcQë8wî>úôS : ¾¾<ݪKEE>ùè#¬Y½ ¥¥¥èÒ¥+žzæ 10í�û1jÔhÜwÿý «‰ÔÁrìÝ�¥\ŒèèèV=¼”ˆˆÈ…I¥R©´´gÏœ‚R&fDäT¡J)üQ�å©KÜ×ß߃qXl°Ùl0™L—Qqa>6nø£Á}ߟ÷¾^øNŸ>�ÊÊJ>ü'žöìÛ[³$íÁðÚÜW±ní~øn à°g×—™x•%7""""""$"!|Vjµ î«Óép[\l£Úõ¤KΠX> IDAT $"!Ì5¼庵k1iò<ýÌ3Pbÿ¾}˜óâ‹øfÉbôìÕ ¿¥§ãµW_EêÒ¥9j4?€nŒ"""2|øpW&€¼üscpÒH""×'‰ P(àïïïµ”••aö /Ø3è׿?¦Ïœ�O?þ V‡cοþ�©S&óãæXp """""r’úF-”—•!/?ß,ZŒ6*^œ3§Ií*•�Ÿ€åi¿»}Fí;t€N§«UtéÕ»JJŠí?wèØôú&µ®F×ÒîØ¬ÙÍ¢‹`Á�ˆˆÈ…\;J�£ˆÈ›è�háàæ_µÿ‘#G0þ|$tEvAôF‹Ë¾·À  ôë×½{÷Á#=ˆW_yo¿óN£¯P(Ÿ˜ˆù‹~uû~¾{üx¼÷Î0ïýàãS3­``` *++íûœ>u r¹¼IíªÕáÐ-ønÅL,8ѵšr ŵXœ "r}B¡#G�ÆG~àµ<ò·G1sút<òЃøÛ£�¡Ç­·B.—Ãb±Àf³¡°°o¼þn‰‰áÆÍ±à@DDDDDt3UWÃh0xíÛß²y3rs�à䉨ž™Yk[l÷+K9~úùü¬¸9ˆˆˆ\G)¹·[¢"BÞ|ý5œ>}ºîT__tîÜ�=ñ$†%%1,7Ç‚ÑM$“ËèµïÿܹsøèÓO1tè0øúò”Ô“±w‰ˆˆ\œÉdŠ+�™™‰sçΡ¬¬ «W¯|ôÑGˆ‰‰iÖÜž"%%m;vÆ#Ó_懆ˆZM}«TX­V””c{æv,OMÅ›ÿù�×fäïï�Áƒ‡8,6Øl6˜ÍfH¥R~¨š »  ÿxêa$Ä÷cÁ�ˆˆˆÓét˜1cNž#""°g÷î&=¦¨¸ËS—".<Èã?"‘*• �:ujÒãrD–f‹Wdä.8Â�ˆˆÈ…i4¼ð 4hP�Û£££ñâ‹/böìÙ ‹ˆÜžP(€R&Fnn®G¾?›Í†¢¢"|õßÿÂV]íµý\ߨ…ò²2äåçá›E‹ÑF¥Â‹sæð ps,8¹0�N‡>}ú8ܧK—.0›Í ‹ˆÈ4v•Š�wÜÁ°®„~ýú£wï>xä¡ñê+¯àíwÞa0nŒ—T¹°   ìÙ³Çá>YYY eXDäu¢££±`Áh¹Õ5û·õ쉹¯¿Á¬‡P(ÄÈQ£±iÓF†áæ8Â�ˆˆÈ…%$$à­·ÞB^^ „öíÛ¨Y¹¢¤¤[¶lÁwß}‡ &0,""àh•ŠÒ’ìܹ_}ù_èÊËí¿Ó©ÕÕ¨0™ƒ›cÁ�è*&“ %ÅÅ(Ôj!–HŠ0µšÁñ8k5=ô8€~ø?üðƒýþäädûí¨¨(Üwß}^Ÿ•F£ÁÚu눜‚r~xˆÈ¥…B„¶m‹ä”„©Õx÷�ÿ`á¢Å ¦÷Ý?î™4‰A4‘:X޽»³ ”‹Í‚‘«Ð`ÝÚ5ÐëtµîS«1jô�P(• ‰ˆÇÙM€O>ù¿ýö¶nÝ ­V‹ŠŠ øùù¡S§N4hÆ�‘HäõY•––âì™SPÊÄüà‘S…*¥ðG–§.Ž“§4»½ˆˆØ¿ßã2*.ÌÇÆ B –äp߯Îs4mYLª)8ìÙµÚ…°à@ä*ŠŠÎá§e©un+Ôj±â×_pÿ´Q g÷Lž©Tʰ®!•J1yòdLž<™aµ‰H_�…Zm³Ú¹z• ±Xìq™+ (.*rJ{2¹���üð¹9ˆ¬[½P}iy"�@Pk{qq125ˆOHdXD7hÓ† ·cïžÝ<ΨÕHEBøI„ð—֌іêÝW!5j$E±Î“ÅÚ¬6tFs½“áIEB„(¥Íj¨ùF¬!•+Št¦m#T)…D$l°G}ÓØ6œÑ7ŽÚpF߸[ÿÀéóFXmÕ^ý»¤±ßÞ�5Êk3r4ÏEII1¶gnÇòÔT¼ùŸÿð� DîÍd2¡¸¸¨ÎBÃå"„@ €¶ €'BDÍpª°°Þm<Î;qâ~þùg:teee0 @›6mpë­·bâĉœx¬„>¨ƒåèÐFVëþ õ>F_rΟk°í^=o…Dæß¬6bÚt…"¸]Ý'‰Æ‹()<Ú¬6àTnÃC»ÅR9‡G´hÅù0› ¶ã¨oÛ†3úÆQÎèwë_Àq1ˆjæqÆÀAƒ0kö?šôX©DŠŽaa8tä˜GçÓ¶m;Œ»ûntêÜo¼6 ¾üªÑ�W(•l£ÂIí~ØXp r Wûº|ÒsµË?—3,¢¤-(hÔqæ¨(á­öíÛ‡þóŸ°Xj{Y^^Žòòr;v kÖ¬Á;#˜˜vņØð@û¨�H$ Á¤±#ë}\Nv6r²6ØþÐaRÏ’¥�m#6®bãâêý¶qCe³Ú€eKKl#$4ÔáuÙÎhcãa£†c;ê›Æ¶áŒ¾qÔ†3úÆ�ú·²² …�ýýi|¶hvî;¡cRo´@s¸7ÿÒ¸¿/Z-–-[†Øð@œ8w†ÊªVÿ½ÒRs„„†bÒ”©¸÷o³½â÷sDDöìÞݤÇÄÆÅ"?¬z÷ þ�cÁ�È5¨ÃÃn¿|r°ˆZè8»¬cXúƗ_~‰ÀÀ@L›6 ·Ýv üüüPQQ�²²2ìß¿ß|ó ,X€?üУ³(--EII‰S'Áêêo/6tí�¡IIP(�úG­£¼Æþø¹m\>i.Wi£¡‰ænV®Ò7îÚ¿³Ÿš†å+~DzëZü÷‚ÑhDnn.”21„Bÿhx€«ç¹°UW3ˆÜ_ǰ0œ*,„@ ¸îÛ×Ë·{ÂDDŽ�3 îQ<ÎêvìØ1|üñLjˆ¨=ÔY.—C.—#,, ˜>}ºGç`41gÎÀ´iÓ�à`H}c)d"„úzöîí”U"ªqïØÈÚ›�ãÚÓ ƒ®ÓØy.ÞqÃrs>Œ€}çH$’Z†«…„„òºr"'g�€ÇYøúú"¬�‘jµÚã—ÅÔ^53ü’%K ÑhšÝæåI% øÙ#rº1#x²H7î¶ž=1÷õ7„»ÿ;†ÕL0sÏäÉX·z�}É˺ED`äè;�³V‡ƒ¢_¿~õîsðàAÜzë­^•Ë’%K Y#~5Ešà�H¸+‘Ó…ªÚ0ª“£U*JKJ°sçN|õå¡+/ç¤È,8yÈÅж¸Ú4h P\\ ‰DŒ�ÐP„†¶e8D<ÎZÍ“O>‰¹sç"?? @ûöí!‹a±XpîÜ9deeaÍš5xùå—½.›k‹ ° DxçÓ¯ùÁ!jez½úóg¡–så j4¡PˆÐ¶m‘œ’‚0µï¾ó,\´˜Á4�¶Ä€±£† **Š"W¤çuäD<Î\ÆsÏ=ƒÁ€… báÂ…uîãããƒGy6›­Öýëׯ÷ÈL&Mš„´´4TTTÔ*:¨T*´ïÐz£…¢V¦Ó•#3#Ãc ÚTmñêì'Ù¹2Ÿ€IãF:¥½ˆˆØ¿ŸÁÞ@?ôî{»S'XnÎá@DDäÂ.\¸p]!áZ6›­Á}©Hu°ééé ãÚImÞï]$tõè÷i³ÙpöìY|0>Äbq“›™‘�uiÿóøŒÜ G8¹°U«V5ù\ÎRXXˆyóæ¹DF£±ÖÏjµ3gÎļyój�tð•ðCã¦.ÏZ_ßµÝäüý|¡–#-- ÉÉÉnýYn*WøìKEBX¬6XmÕ.‘ÑÈQ£xP¸9ˆˆˆ\Xk ¢¢¹¹¹.›M]E‡AC¸´eSþ±ÿ÷§ŸÆ3Ï>çpßo¿Y‚7_Ý+ŠÀ¤{&.^\ïê%u�,ùøø ((}ûõÃcO<‰˜˜˜zÇ¢JÝB•RD´W0ˆVÔ9Ôþ2 ö-nñ¢C}„B!‚ƒƒ1pÐ Ìšýv DDDä©Ú´i•Jå2¯'88¸Öu©×¶lú¡J)Št&v^#,KMÅOþ½ÞeU«««ñÃ÷ß7ë9>ûä|üч žd»ÂIxzÚ ÈårÀÊ´´—K½ú5 ìݳ¯¼ôoL¹÷,Zò úôíËY ¢££±`Áô<Þ¥ÞO}Ÿç[¢"ëÜv££"œ~²ï#€XôêÒ¢EÝXp ""¢V0|øpW&|¼üsc´Ä$‘ HIIqéÌ®-:\>iaÑ¡� ¡%ÅÅø}ÝZŒ¹«î¡ëÛ¶áÄñã …°Z­7ô<›6nt‹<¬V+V­\‰Ä�!ðûºuxåÕ¹�e$—Ë1ðŽ;ðÒ+¯àÉÇLJ¼�o¾ûž´FêÑ­=î4Gµçp4giE={÷FÁÉôZu¤yNIDDDnïrÑáòD’íUJŒ"‘1±±øþ»ïêÝçûï¾Et÷î×�€Ðëõxóõ×�4d0n��Áà�ñ/¿„²óçíûìÞµ ·DEâàÁj¾Á½%*£GŽ€ÑhÄ ³ga@ÿ~¸-.¶Ööká…Ù³�p{ôéÕ“&NÄêU«ìÛÍf3~õ%Æ¥$£o¯žè×»î™0?-[Ö¤<¶gf¢¤¤#G�ÂÈQ£qáÂlÞ¼©É¹öîS3ªáà�ü�5ÒSMÂÿ;bã↠�J¤˜2u*Ú…#¦3—­v7 ™gÏœBii©K¼Žp ""r!׎RðÔ¥-[²èÀ‘�SUõÿìÝy\Tåþðϳ0À 0̨ÀL( *¸/ h)îÞ¬4S\JSoVÚæµn·[ݺսmZYVö«[šå’¥‰»¥f™»€ ‚ 3l3l3 0¿?�”U0f†ÏûõꕜóÌ™3ßsž�ç{ž¥ãÆ�ǻ˗á÷ßÏ gϺsdiµ8øóÏxüÉ'ñþ»ïÚ¶›L&<8{.^¸€7ÞzÑÆ!þÐ!üãïÏâØÑ£ØøÝ÷pwwÇÀAƒð{Ú9[áTr úF„£¤¤oüç?ÈÉÉÁO>X~zZR\Œ™±±‹EX·ñ[Èår¼üÒ‹XòÔ“°X,¸ûž{ðÚ¿ÿ�o7nÀ‚‡ÆÂGEQQ^üçóxñ…¢¸¸sçÍkV<â¶n…T*ÅȘQ�zx`{\ÆŒiÙ5+Ƹººò&kf²aDô`‡hÄIåø51C#£œþºˆ%ÕI‡Š*+^yû\Ð^nVŒŠòs�’œÌäQ;ŠÐø`ûÖÍTY좇"{8‘Ó%šÓÓ᢮~ÁÓ1'𬍍À]wß �@Pï< ë¾ùp÷=÷ÖÙþõÚ¯�zö,æ>4ã'L€——ÆO˜€ùý+ÒÓÓ±î›ú‡Ôô’0¢ ?Ÿñ%fΚ…™³g7xŽk×~…¬,-[ü8n»í6x{{㙿?‘H„�ë×vl¯^>ñ‘G…§§'ºté‚^| R}ðÐC!$$„ÁifÒ!,, 'Ož¬þƒYì†ü"3S�{'OÁ¯‰‰øþ»MxhÞüê†^\ ……˜rß}7”?þ<@£ÑÔÙ~[PPuƒñrÓ]¯CC»7ëÜjÞK©T6XfÞüxwù2<öÈBDEGcĈ‘ˆ=�;wnv ¶o‹ƒ\.Gô°a¶m‘QQP(ؾmî�<¥Þ×]?ç„ÜÛƒÁâÇGX�ú¾K$P«50–ZêM6Œ?¡Þn÷½ÂÃmɯã'N@îãkK êu:?q¢Ny¥J_¥ ß~ö6 %9ù†'ì#cFÕI,n\¿î†cÔîéTûf³¹z=´¹%íŸdKˆ3©ç!º@íç�€À@Àòضrz�û÷ýÔègÜ¿ï'èuºFãT['•]T `µë‰$er9¼}¸¤½Â/w&ˆˆˆ¨>éééxæ™gPQQaÛvøðaœÿý ž�L*lð³4'N-JV3§ÃúuëÚdõŠ›]´©ø…GDBwìxûV8&ˆˆˆ¨>_ý5|}}ñÄO ""UUUHNNƇ~ˆÕ«Wã•W^a�šH6$&&Ú’ éWŒ J#ÜÝÝ1~Â|·i~9x^^^øýÌL��…DrcWr�F;y R©ô–ž›�¯/ô:ÊJKáÑHâ`ü„ ?aôz=öîÙƒÏ?û?ìܱ..¼³üÝFßc÷®�°X,ؾsº×ÙwñâEL;»vîÀ¬Ùðfi¥GæÜ�È�½!–8Î 2b‰•-l2ÝLR¡¥¯)1U`päz,j’}¬œHϹ¥ï'  aµZa±XX�çp ""²3IIIX²d † OOOÈd2DEEaéÒ¥8Í¥ö˜l¸î�R=d`çŽØúÃÀäz†S@ðÕa=ºœœ[~^]:w©~¯ëº~7D©Tbæ¬Yøü_þüs“¯Ù¶5þþþ7$ ((��ؾmo’VªY�‘’ ·ZnnîM¿¶²Ê _?åŸ~Î5I‡G.hÖD’ ù=í\“ÿ8ø‹mXW¯ðp|úÙç¼i˜p ""¢Ö2 ˆ¨ols¯^(**b€˜lhsƒ †F£Áá_qð矌¾}ûÕ[vìØq€=W'�¬‘œ”„‘w Ç«×õÀ©QYYÙâó~gõÜß~;lÛ–���>á½0}êTÀSO<ŽÈÛ‡àÒ¥K¶2Þ>>uÞ»!—/_ƱcGUkî†ë 6 'Oœ@vVo”› ’Kp×ðÞ±ô埡 ?Ö¯ÃÜÀ�âò3ˆ%øúx7{õŠ–*,(ÀÛo¾‰qcFã×_Å›o¿ƒMßoÆð;îà ä€8¤‚ˆˆÈÎX­Öz»²KÚáÉ`ZZ¶ÙÑÓÝÀÀ@ôëׯÑd€f%<Än0—C¯Óuè•*jÜ;y V¼ÿ`é³Ï6XnƬYغõ|üÑJ‡„`Øðá8—–†çž}999uV”�æ6œ?Ÿ�ãµV‘h®œƒÍß}‡OW­ÂàÁCàëë‹·Þø/, îŸ6 Ñ»wíÂ^{ ÿ~íUxzxâã�Vf?ð`£Ç߫Պèè†ÑÆaÃúõؾm~ä~Aµ@Jr2BºÈ0hà‡ý Zm&ä(BkÒ—………øqï^ìܱ¿>ŒŠŠ ôéÓç¦�éê"@~ž@ûÍéã!uÇsO<„�?ù It­>^II Ö|ù%þ÷ùgðôôÄóÿ|S¦NmÖŠ6Ä„9 ´´4¤¥¥ÙÕ9½ð P«Õõ&5Ø�pªYÇ Ry"7+û÷™›œ`­#¸gòd|øÁ Ü}Ͻ –‹ÅX³ök|òñGxë�ÿâé'Ÿ€H$BxD–,}1£êNF÷ü /à¥_À¼¹sZ|Nr¹k׭Dz·ßÂ̱°X,èÖ­ÞY¾Ü¶„傇†»Ô›6~‹‰ãÇC@­ÖàÅ—þ…Ø™3=~\ÜV¸¸¸ 2*ªÁ2C#£àææ†mÛâ˜ph� .`Ï®�Óä$ŽÎÈh4â§÷b×Î�HLH€Åb�H$BTt4bF�B̨Ñ�®¾Òdc_â†# 1,êövýœî¨|<ÐÃâ ciùM£¼¼ë¾þŸ®úUV+[¼3g͆X,fEº ÚÜÜ=nºwïnçÄ9”šU®O6DFF¢WŸ�øöÇ£ Rê› Îßß)gS›UÞÓÓKŸyKŸy¶É÷Š6 ?í?pSç]ºtirâÇY³¸©I·nÛÞd///$�ù½Yçz³ŸÑUUUB  óÒ%ô0Ðá?�Þ`‚�Âscïn²ì£ Fü¡C°X,�Ëå?abF�Æð;‡‡S]çôôtCo(ƒ±Ô‚þ}û`d̰f½¶¢¢[¾ÿ­ü£sæÎżù š\Y†šN8 t;˜p ""¢ú�=ºÅû~üñÇ6=‡ÐÐP,]ºÔ.⑚šŠåË—×Ùv}²aîܹøíxo"; ‰`µZ‘�•…‹™—¤ñwèÏc²T¢®P«5M–=°?TªNxtÑ"L½ÿþ&çi.½N‡û÷!\ãmKýjµ™Ø³k'Ò¯¡3˜b©g³†¨íؾ¬x—³³;c>ú|}}[}N)ÉÉ8räÂ5ÞHÉ,dEdÂ�ˆˆˆ¨åêK6‘ý1›Íxù�OðòÒG2é K`±¶lžýáwÜ�Ä„¼ò¯—°òƒƒ˜˜QˆŒŠjÕ\<&³ YZ-äR´hÿ„CÜ�‰HÎ,€±´åKVþíé§B¡ë×­ÃWkÖÀjµ6ùº¦z  äåB.±ò1á@DDDõiëž ÎfãÆ�Ⱥºb“ Dö¯¤´ o®øQ½»bÚôéµ4¦R¥B© eà >ýìóê‰"÷ìÁÎ�;°åûï±iãF¸»»#2* #cFaÄÈ‘ðóó»é󪬴ÂÇׯÝâòѰ?¾õÃ×, +DDDDöƒÉ"û—«××ù¹ÀX mö¬ýê+Ì~à‡J:Ü oooL�6 S§MCA~>öî݃�;vàç°ï§Ÿàââ‚Þ}ú`ýÆooêø%æ ‰¾óOÿ\)ÉÉØwè7<ú{«ŽÓ‘æ3aÂ�ˆˆˆÈÁ´E²¡Ä\¹‡EF#JÔÆt99ÕõÌT¨¬²âôÅ|ôM:diµªWC©=€Ñh„Ñ`¨SV&—C&“ÝðÚÚk­hc6™ ¿.rý1ô:Ìfs›ÆÂÇ×Ó¦ÇbÚôXäååaÏîÝØµsŽ9Òêc×w¾J¥²Nl¯�KS±½þõµ“ 5s61á@tL&rõzdiµ‰ÅP©Tu~Qëµ�Ú]�J6äååá�+Ù�I…ÍS\j®°ý±}âø1§˜IŸÈèu:¤§§ µ–J¬�tضm;î›zß ¯ývÃzÕ“N¾ùÖ[¶í»vžÆîÝ»ë”7nÆO˜˜÷Ô‹�£è†ãàeû·+*à‰²º¿“ ‚×–_”ZK TÕ)s1³¨EŸ¿¬¬ ®X�];w //]»vâÇÇè1c P(°kçŒ7ËÞ}¯Uqþrýð“VÝp¾ÅpGe­fÞõq±X]ê à ®]§¡‘QˆŒŽn0ÙP3A$D- ÍÌĞݻnÈžªÕ7~dr9ƒDÄzFíD¡P`Ú´i(--ŤI“ê-“��€íÛ¶!Bツ³º&�©3˜ÐÅG ‰ 9›9Q[Ù¸a=ÊÍfTVY¡¿®qZ“tÈÐ�ÂÀ!‘ N$Y^^©»»íç)S¦`Ê”) ¾gÊÙŒ†jz›|&™T‰ ¿&&`hdT£eß]ö¾Z³ÆöóÙ³¿ã©'ÇÚoÖ¡_ÿþH:}¿> ?¥ÆŽÓçtA{ÚøÚ©¾É„Þݻ1mz,?þ8äÞÞ8uò$žî9¬Yý%úõï�¶m민‚ ë×·*áp+\Ôßp¯7w5 ¹TcÞHINj2á�ŸŸ�gÍDFFÝDQQQ’’N#)é46ÿ¾^¿J¥’¿DöìܶåxA�ýz½‰ ñˆŒŠf°ˆnÒ�}û«×ëqâø1Ö3úS•˜+�~ňô+¿àȉ îŽðîÁð�^{²šßäq||ý JÜä·#(ÈÏmò82ßηôé©g�‘ÖôdoU®ðõS¶êÁÝ{¢¨´¢þÆF®Gã[uŒ¶º6Žv}ÛâÚ4vŒÖ\›”Ô ¤¤e ål|½ÄÈ/j|.„’Ò2¼ºüLÖwÝuÄ ‚à #å¸C¯à™¿ÿÝöt~ð�!xúoK°òƒjµÏÿó̈�n÷Ÿ¥­V£¸Þ{Ë—#7/Ï>÷† ¿�H$0›Í¸|9‡~9„O>þ¼ÿ>þýÚküeÅ„‘cÓëuõ&j’�ÚÌL6„ˆZ¡±§n¬gdt¹ùؾ÷lßûK�íQ=TM¾ö¢ö2¶¿ýqƒûÃÕÞ�{4½.ü¿nñ1Ô~Pû5½Äß~hðifs�q á(¾Úúsý o©ŸV£­®�£]ß¶¸6�£­®MSɆkIRÓÒQxu"IgÐÅ߃¡Îp€þ"7÷Ú„•þv=amJr2$žÀ�çoÉñØ�÷W|€Û‡­³ÝÝÝÁÁ!Ahhþñ÷çøË‰ "ǦÍ̼¡ÑS[ÍÏ×/ïDDm_ÏšÓ˜èÏÖœù šüã][hÇÐæ–@›[ÒîÇ0–ZÚ$®ör gº¾mumšëúÕ+¼UhsKðèÜæ=ýÏËËCbb"Ô~ÐL0Y*Ûý;ãÞÉ“ñÎ[obÙ»ïÁÅÅ@õ2™µ{ù]ÎΆ‡‡G‹Ž«VkðôÒgpÿ‚gnÙ¹«äØ·'Ž»¥«Qäç#¢wïFËôíÛù-:ndt4d~�ñòÛŸð——�pa¨£Sk474zj«fáÇñcDmRÏÈ`Q“<ÄnÍz’OŽ¡&é�£ÏÃÅôè ¦fOv—››‹¸¸8¨ý< ÚGÓfþ‚¿¢²² óš‹øC‡`4!‰`±XPUU…ÌÌL¼þÚ«èÙ«—Ý] ¥LbK6ÜÊÕ( ’“’-“šš 9'“vxìá@tµ‘“�•Õè“׿6˜ˆ¨ñz&ê­k¬gDÔ¡+úvSV+Än.Hÿ£ˆAq¢¤ƒ¿¯Ô.z)´ÆÁŸFZZ*.]¼ˆ_ëì ïfû÷Ê�íï)¼Þh•²f‰¹Y#cb°ä©'ñè¢EˆŽ†Î]º@,Ãb± ''‡±òÃÍa–L89�ñ&bíšÕ Nh§Tª8®œ¨ ëY}ÉÖ3"jŽ¡ƒûcÄ�p¨T*¬ûúk`ÒÁ‰’­bþóÚ«¸|ùrý�/77ááGEÌ-Xy§µne¯†Ú?ñ$ðú«¯6XF©Rá‰'ŸbÅ`Â�ÈñÉärL�>{vî²M Y#8$cÇO`�ˆXψ¨��ˆ„Eó®�íŸ1kÆFúöƒ Ù�œœ¬X¹#GÆÀÍ�Í­ú( lÚ¼_­^�ýûöáÂ…ó(++ƒX"�Z­Æ°áÃ1oþøúú2XL89•ªfÏ™mf&ôz=Äb”*TªN ë9€îÝ»£ÿÀÁغûƒád<ÄnÔ¯W�dPýô¾É“à«êŒ�¾ØÈ@‘]ðôôÄ�wŽ`²¡qztÑ"<ºhƒÑ† %åè…BÁ„‘=Rk4GNÄzF(,, †’r¬üz;ƒáD<ÄnèÛM�ð`ÿËŒŒ ØôýÖ?­K8u°F\i9”VŒˆÔtYƒ}#Âoê}~O;çÐ1êåÛ á½Ûì˜ÇŽÅÀAƒx¶@жK–LÁ�½íâ|¸JÙ¥šdC¯^½02¦ññî{‡!,Ð!�½8æê"€L*DZZš]�—±ÔDˆlÇI õ:¾Ý°áo»¼vÆR dŠÎ�ˆh³cΞ9£e�íädI8h·1êˆØÃ�ˆˆˆˆìNídø ›,/“É0cÖ,N$éè×]â†�–-[†U«V9äg¸U½Lf²´ZÈ¥"‡¿Îñ‡`¿†K—.¡¢¢¢Ñ²Óî»#FŽÄc‹7y\£Á€‚¼\§ˆ‘³`""""²;ƒôÁС‘ÍJ6ÔPªT˜1kº(¼ØÓ�ÈŽ½ôâ ÈÈÈh2ÙII§ñÁŠ÷4ÅDDDDdW®_�¢%j’\½¢cP«ÕX²d æ=õ"JLvqNF£o¿õ&øyy¹¨¬¬l°¬#ÏÙкœ¬Yû5 WW×ËõìÚacä,˜p """"»ÐÐj-ÅÕ+:©Tа°0K-vsN¯¿úolýá^œFxûø Wxx£É†šëKŽ� """r yyyøãJ6dR¡]5>¨yš³EKÕ¬^±vÃfÞô§ùåàAÌœ5ó,@çÎ�›lTwD¹z=õï×¢×ôì ìñà`8‡9…„„lߺÃÁ´d5Š–ŠØ}»*9§µŠDè 7T"K«m²¬¹¼OÿíoèPÉ‰Ðæ²bèu:Þ0í(ª‡ Ÿ¯úqqqvq>ìá@DDDDí¦&Ùàçç‡ðˆÖ¯¯×é`6›¨VÄ &O½›6l@E•uÅ :µ˜R.�§  ßnX�§—>ÓhÙ~ýú!==ýúõëp1ÊÍÊÀþ}fL‹m|9Ë›é¥Àž މ """"j×F ¬VäêõøvÃz¬×z"ô¸ÍÇ�…L&köñvîØ�¼\}�㸢žÀO&aÂ�n¹_úþùü?;c&† …Ÿ\\رœ:&&ˆˆˆˆ¨Ý\Ô_—¸Ö[.(‚ÁPØ¢„CU•ÚÜhsÙ­›ÚÇ¿.@~~>ž]ú·&Ëò©}ã>þh%}láÀ˜j#"""""»Pn©‚6·wÝuW³Ê—––"-- 2©®.»ø ™™™(.nûž4jµO/} g;N2mÅ{ﵨ|dt4ÆNšÒ¡bdïØÃ�ˆˆˆˆˆì‚ÉR mn &MšÔ¬òZ­Ë—/G„ÆÉ™v±ɱ“§ ‰àæÆ¦VCjVœhŽ�ýúB$£g�žxéå—Ôµ+è@ØÃ�ˆˆˆˆœÆ�è;¡Í-a ¨ÝH¥R&ÚPii) �˜˜€<÷wÄÁ°&‘]òòí¹Ü›� ‡RVV†W¬À®�;�——‡®]»aÑã�cô˜1€9ÌÆ¸qã1söì£æÎ]ѳ{(~O;‡òòrìÚ¹/<ÿÞ`† """"j7j?tQÔ�²Ì*F%\Ö³EF€»»ýÂÔ¡¢Îör¸áœ6%¥e :ÝRï.{_­YcûùìÙßñÔ�cí7ëЯ$�>�߆ŸÒcÇ�gÀšA$aÜøñxù¥ Ä9…îÝ»£ÿÀÁغûƒá@ ¥åR{`p¿Û¶ðÞ½[œh¨ÑU€cïEV¦¶Îö\c)N¦îaÀé–Û³{7¦M�ÅâLJÜÛ§NžÄóÏ=‡5«¿D¿þýñömxõ•W°aýú›phÉ5úõŽàÍÕœïÔ’rt‚B¡°‹óáDDDäÂÂÂ0`Ðí¿ï`Œ¥ür"ÇS³�Èèè›N6ÔP«5¶c…„†â²>k¾c²�Zш+-‡É*ÂÐȨ&Ëà™¿ÿJ• "‘ƒ‡ ÁÓ[‚Ô³©¶ûóù¾€3gÎ8]Œ¼|;!<¢7o˜v”¢-Ä]÷LATT”]œ{8Q»;�pVk&Ü9Á!!mrL½N‡o¾^‹?ò™„¢Ö1–Z`‚‘ÑÑM–íâïƒÁOOOÛ¶þ"7WoûÙ? EFcËÎÁhÄ™”d¨ý<ì2±j,µ@¦èŒðˆ¦{"4w‡–¾F«ÍÄù´³v£Žˆ=ˆˆˆˆÈ.$%§`ë–ÍHINnÓdCú#ƒë dR!¢z¨°páB‡ý ÷NžŒwÞzUUU¶mÞÞÞ0›Í¶Ÿ/ggÃÃãEÇ5 ‘µŸo”dej‘žz†1²#L8‘]È/2#ýŠ{vílUÒÁh42Ù@ífþ‚¿¢²² óš‹øC‡`4!‰`±XPUU…ÌÌL¼þÚ«èÙ«W‡ŽSyy9Ö¬^�Y3b1tð ôîÕ·ˆØi÷㟓ÉÄ›É pHÙ �áj#c×NhV÷ìëONCZVÁµc‘ÓòóóÃ]wÝ…�¿Ü€rK•]œÓÁŸFZZ*.]¼ˆ_ëì ïfû÷Ê�?é°×­¤¸sç<ˆä¤¤:Û�F#N�<‰S'Ob[\Vµ^^^¼Ñ{8‘]ÑLH¿bÄþÄ-~í�„£Xù¿ L6t …“&M‚6·&K¥]œÓ^{—.^¬wŸ››BBBðÖ;Ë3jT‡½nüÒÏ�ÃSK– nÇN=qpäø |·å,|äQœÏÈÀ‡¬àMîàØÃ�¨“É„\½YZ-Db1Õ�P©:10D·°ž©T*ªÕ Ý�tÐ> �ë,š7½Éòz�GO�Áÿ6lgð¨]åää`ÅÊ•92nnlnÕgïîÝxgù»5zt�ížžžèÕ«zõê…={âí·ÞÄ?žÿ'Æ„‘ãÓffbÏî]0 u¶‡„„bÌøñ�H$ Ñ-ªg�j5Æ�Ÿ™\Î ÑM‹‹‹Ã¶mÛÕC…„³:ÄIH8 ¸{L$ÔjMƒÉ†o¾^‹,}FíÎÓÓwÞ9¢Édñ£G1pР£+W®`Øðá�–‰6 Ï.ýo¨ ×xcûÖͨ0ÙÅÒ˜RA@§ËAÜ[nh@zú9lÚ°žA"º…õ,K«ÅÖ-›$"ª×ñ“§°iÆz'’¬½ÅE]1ƒE·„ÚÏÞ‚b¼ûÎÛM–ýõÈQˆD¢&ËÍž9Ãéb”}î6®_×dY‘H„ôs�/s™_Þ|-$—ŠðÇ•läååÙÅù°‡€=;wÁl6ÃjµÀjµB @¯×#1!‘QÑ ÑM:°o_½õÌÖh`=#¢K-Õ«M\7‘$W£ {èþûúk¸té***-;í¾û0bäH<¶xq‡ŠQDïÞxîÙgðß7ßBDïÞ¶í¥¥¥¸té"ìß�ÿ[µ “7”ƒc"z½Î֪ݶƑ63“�"j…쬬:u«6Ö3"jJÍD’µ—̬Y�‚ɲ'/½ø222šL6@RÒi|°âý£yó ==�/z¬Îö�ýúbÊ=÷`Å{ï¡K<ùôÓ¼¡{8P‡W»�SÓ£¡¶šŸsõz‹¨ êY}jêYí¤Q}I&ž€ÞhÂÊÿm`PœLe¥†’r êÞ¬ò¥¥¥ÈÊÊ‚L*D‰©•UÖö¿Osr°fí×0p \]],׳{(~O;×ì㪔*L�>/¿íøËiÞqç�øÏo ©Ö²˜B¡J¥þþ3±3fÂÃãEÇ ïÝpÅ—¶²21á@dÔÍ �žÚj’~J%ƒEÔõ¬1�� 5™tÐ>�ŸŸf0œP‰¹)ÚB¬_»´YåµZ-–/_Ž�’3 `,µ´ûgðöñA¯ððF“ •J[t\±DµZcŸ±-Lžr&O¹Ïöóé”3­>¦L&ƒ¯ŸÒibÄ„‘“ DvVV£=šÛ`"¢ÆëYÍP¥ú|¬gDDäè~‰O@YY>ÿìÿðãÞ½8Ÿ‘�’’H¥RuÅȘ<0gŽ�<Å`5¢ªª ååå\)Ž "Ç7"&›6l€Ùl®w¿R©âDvD­4~ÂD¬]³f³¹Þdë‘ósuÀCâ±Ü\ëN%&“ mÿÖL¶áõ ×x7ú>5]ëµ¹% :ýéòóóñଙÈÈȨ³½¨¨II§‘”t›¿ÿ_¯ßeíAÛ³{h³Ë¶dØ 1á@d—TªN˜tϽؽk'ŠŒu'ž  ÁØñ$¢V’Éå˜:}:öìÜe›¨•õŒÈñI„® ] ºB,t…¡´¼ÁîÌ7Dh|š}n>¶mÝ‚Âüth�íîîîAppBCCð�¿?×acÔP¯…‚œK?‡5_| _…Ï=ÿΤž·íó÷•ÂÕE€b“eæJ˜,•¼Qœ”ÙR ‹ÕAjÿ&Ëäç#¢wïFËôíÛùN#‘ÄJ•ê¦�áíãƒÁƒ‡`À€�˜ÿÐ\¼ò¯á�·Þâ Ø guøüÝcÈ€ÞL8Qûru@&Bá)†Îh²=ÍïÖ ]ÕþRûCéç‹�ÛðчØz$ÈårÈdr¨:u‚H,‚J©‚¸“»µ6ÙÐVÂ#"ܧÕV/囕©Å�2 nY}n>.d^Æ‘„ƒ(7•Ôip•˜*lÃJ8$Ãyè &øu–bZìŒ&Ë* $'%ÝÐá¶ÔÔTÈåò�ƒÑhÄ™”d¨ý<ìr~�Á„;Õ!�öNjö÷’«+ÆŽ�ï¿×¢×iµ™8ŸvÖncÔ1á@DDDÔÁøxŠ —Š “ ë ) 銑£bÐUPïë[üx³’ ŽN­ÖÔùÿõ”~¾PúùâbZ²³®5jÄW‡™Ô Í0[*a()Ç]1*«¬¼ñšA&"Bヅ bÕªUùFÆÄ`ÉSOâÑE‹= �»t�X,†ÅbANN'&bå‡ *ºe% …HŒ�ï8�i«e¥¥-zIV¦é©g˜p`Â�ˆˆˆˆþì$ƒÂS _/ñ +DÕs)tÕø7˜lÐ!’ -Qó´[¯ÓA§Ó!K› m¦EEF[Båë‰+ÆŠ:V’s[üÄ“HLHÀ믾Ú`¥J…'ž|ŠÁjÄÌÙ³1sölÂÁ1á@DDDää$BWô ô¾!Á¨†æ6 Õ­wÝÑ)U*(U*Û0 £Ñmf&²´™�ÉäxzI4.h³ñsüQ9yºÜ|­�H¥R„††âèÉTVÚG/…B�M›·à«Õ«±ß>\¸peeeK$P«Õ6|8æÍ____^@bÂ�ˆˆˆˆ×àþáÒ/ùW.á�+—‚�Ðî apn™L†ðˆˆ:óDtU klæÆÞƒ ÚlìØ{ÇO§ ÈO‚? Ê 3˜¸› V«±téRô¾s²]�—§§']´�.ZÄ‹DDDDDNhDÔ L»{ ”~ÕOQ�Æ0»™°±£ëªÀ¢yÓqâøqØ÷BºmcΙx說ªP^^ ‡)‘“cÂ�ˆˆˆœB\\¶mÛ†¨*$œÕu¨Ï.º"H剋ºbŒº3÷ß=R÷:e˜l°?2™ ^^2!º"¤‹ j?\Ð#¿ÈÌ9¨žÝC›]ö÷´s µ©p�7¶oÝŒ S¢¢¢Úý|\xIˆˆˆˆ—¯—}‚|àë%Æ=#`nì=7$È>‡„`ÁÂ…;~¼¼ªBb¡+zÈÑŇ×О¨ý<à-(Æ»ï¼Ýêc …B( ÜvÛmN£ìs§°qý:Þ0íH.á�+ÙÈË˳‹óa""""ÒÙ *ïk S…Ÿ‚Aq@5ó=¤$'ãçýû`6›Ñµ“<Än\RÓ5Ôk¡° çÒÏaÍ_ÂW¡ÀsÏ?Ï`‘Óc""""T;Ùà§Tböƒs02fãÀÂ#"0{Î\ø)••·;ºvòb`œ„·�‚÷>ø—.]Ä+ÿúƒBN� """"£’Kê$¦M�å²–NB&“aÚôXô ‡¯ÂUnêóWVZa()Ghh¨Ó~FWWWŒ7ìoÑëäro �Œ‚6·„¥�5ºuïÁÙ&ˆˆˆˆ©±â"€Ú¯ºêåUÝ8s¦{§"–H0nÂDÄΘ�GæMïPŸ½Ä\�m!–.]Ú¬ò©©©X¸p!¢z¨ “ çƒZ­(+-mÑKd2"££Ù˜n„Z­AHX/ÆÈŽ0á@DDDä@^bˆ…®€q'0ÙàÄÄ ºª02zƒádfΞ�SÉ) 9=&ˆˆˆˆ©z5Ù ‹¡Vk�`p¿�ˆDDDDÄÕE±»'z†‡3@Jr2RNþ†p�7ƒAD‡ """"â!vƒ¹¬z�ŽÁèR’“P�— peL"r@L8‘S˜4iæ/\Œ„³lˆ“sÐëtÈÎÊP=™"µ³¥« ŒFb$’xpÅœv–œY€‰“&#**Ê.· """"GýßdbO'¾¶»vîxJÜ”v¤3˜P)¦ÅÎh×{"K«µÛÕ8t”ꌌÕnç`4Q�—ëX+–´u J-èâ…BaçÄ‘ªªªÂÆ ë±vÍj¤$'3 NÄl2aã†õÈÕëV«2©¨C|v±Â5ÞX¶lo„ëôz¾Ý°£)II8’p�1²#L89 ‹Å‚"£°g×Nì¾ú4œ[Fz:>û¿OmÉ?¥²C}~WWäRÒÒÒx39&ˆˆˆˆ�X,ÆýÓcm Ò3))øüÓU8“’Âà8¨Äøxlݲåf3 ß€ e`!•J CI9*+9³&‘½aÂ�ˆˆˆÈA)U*L›k›ÈÎh4b÷ÎL<8¨�Ðêä‚—— S§Oo×±ðŽB­VcéÒ¥HÑrbM";ÄÙgˆˆˆˆ˜X"Á´ØHINFb|<ŠŠŒ¶ÄCrÒév�äŽZF©Ráî{'#00b‰„!"‡ÇDDDä°}ëf„k¼;äç�ˆÀ‚… 1vüxyÉr¹œ7†ÑëtMö<  a²�ˆnšÚÏ'ŽAjjª]œ{8‘SÈËËÃW²!wòÙü/êŠ1jØ`ŒŒVïþðˆ„GD %9j�¦Áã˜M&6lÿz�)ÉÉÈH?ãÕI>ÕjÈd2Ç�¨äx¢ ßnX�û§Ç2 ÄHŸ•Žýû\9¨�Ç�†g%˜p """¢æ+1W@,õ„R¥j´\xDD£ûÓÓÓ±g×NªÕPk4 T#P­f€Û Á�¥ÕB«Í„V«µMY›63³ÉëCöE,t…› YZ-ƒÑHŒÊËJ ×é ²aÂ�nZÍnV¿x…R©„X,á”Äh4Âh0@¯ÓÁ\nf@€L&ç“KréçÎÙ~Ÿ×nDªÕP*UPkÔPª:ñ~n�Ç�áÀ¾}õîë‚�ÐP„pÈ1á@T?­6g’“‘žž^oÆžƒH,†Z­Fÿ�¡Vk;L2?z´N÷["¢¶ •J…ôôsÈÕëmÛk'ŽÌ~pN“½):½N£ÑÐh&0ðZBßËK†àÐh4·µjÈÈèhÈü:ãå·?áMÛÁÉåÞ…oãö2 Ô¨Ñ-¯~N8Æ`0á@ŽöKöÀþ}ìFæ$ÊÍfd¤§##=�j5†FE1ñ`Ì&Nœ8ŽÄøxƒˆn9¥J¥J…Èèh˜M&deeA—“­6¹z=ÌW,4–lØ¿ï'�Puê±H ¥J±Xì� Ššef³ z½¦2ôzŒC�äô0°Á˜Ž?j�†ÈÝÁ’ IDAT=Cnö:”Z�pV‡¤Ÿ77«¼V«ÅÆ�®ñÆÅœb§^S&“!2:Ë¿ØÂ¥jµÆR Ön=È`0á@Ž"%9{ví¼¶A(†K@\ºC  „@Än�ŽÂZn‚UŸ…JÝ%Te§¥FdiµØ´aÆŽŸÀñ¤íH¯Óa×Îuž2ºø‡À%0.Þ*¼ùtÑ‘Té2a9°�� ‡!–H‚à�D"Úö½d2›šüîÊÎÊBFzz½ûý”JH$’F'Ù«ièËäm3ÉlªNÔÙv5‰Ò`2D¯Óaë–¦¹………�îçïÒ?Wii)ÒÒÒ —Šàê*`@ˆ˜p Gr`ß>[—JÅp ×î™dpP‘‚€¸„À�Ê´c¨á 7šàåÛ á½yщþ$*¹ž(÷Ö3�ÄHŸ•Žýû~b0ÚQ„ÆÛ·nFBB‚]œDDDDDg0A¦èŒðˆƒèO"ºÂMP‰,­–Áh$Fåe%Ðë8]ĵ9&ˆˆˆˆˆìTxïÞ9É™ F'KCI9ƒÑ™\o_cdG˜p """"²×”L_?e‡Yò±ÄT�äÌ,Y²¤YåµZ-–-[†p�7<ÄnN¥J…i±3�¢-dÅh@xD†DßÉÙ&ˆˆˆˆˆÈ.TVYa,µ ,,¬YåKKK‘––¹TWWHdg˜p """"""¢6Ç„‘ñ»Á\VÌ™à‰ˆÈî1á@DDDä@‚TžÈÍÊàZ÷DDd÷˜p """§…‰“&s6""ê°´¹%è?p0ºwïnçÄ9…B�.þf6ª«g÷Pçlí,Ô~¼ÈíHo0¡Øêީӧ3�ÄÈ/ #cF1íù�‘[‚ƒnoöÄ«·DDDDÔ& þMß~Ûh™÷îmqb`þCs›Uî÷´sN׬L-ÒSÏt˜„ƒDè µŸ¶mÛfWçe²T¢®P«5í×�ÔfâÝwÞFT•]^;“¥b©'”ªö;¿Äøxì‰ûÞncÔ1á@DDDD­& ñÕšÕ¨¨¨¨w¿ÕjÅgÿ÷)D"Q‹Ž›y)“Áí@DB¨ý<Ç`9&ˆˆˆˆ¨M‡à‡-[êÝ÷ãÞ=P©:¡²²ò†}[6�{ïž„þ}zcüØ1زù{@߈pdeiѳ{(þûúk€òòr �ŠÄñcÇ0~Ìhô ï þ!›¿ÿ#†ÃÀþýð×ùópéÒ%Û¾ çÏcÁ¼‡0dà êßs˜�Ô³gy‰ˆ˜p """"{RQQ�)÷݇UtC/«ÕŠ�>\‰û¦N½!á°ß>¼»l9žyöïˆÿõ0^}íu¬xï=üš˜ˆSÉ)ª‡KüãŸ/¨îIa2™°fõ—øì_à艓õžÏO?þˆ ëÖá˯Ö">ñWŒ3Ï=ûŒmÿsÏ>‹Q£ÇàçCñøù—C5z þõÒ‹¼�&,, «V­BÂYço!bÂ�ˆˆˆˆœ‘ÕjŰáÃá.•bë?ÔÙ·wÏnTVUâŽ;ï¼áu«¿üÏ=ÿ<¢‡ ƒT*Åà!C°dé3X¿î›zßG  ¸¸Óbc¨V78Dã«5«ñôß–"((‰Ócg`݆�¶ýé¸ûî»áîîOO<8gÖoü–’ˆˆ """¢ºRSSqâØÎæßÎüõá:½¬V+>Z¹}x!Á åϤ¤ 2*ªÎ¶Û‡EÒéÓ�¾OïÞ}ÝŸ–šŠ={6¸øwàÍ7ÞÀw›6áÊ•+¼pDädR!þ¸’�¼¼<»8&ˆˆˆÈ)¤¥¥áøÑÃNŸpÐMðòí„ðˆÞvy~&NDeUâ¶V÷rس{7Š‹‹1aâÄzË—––"rÈ`ôìjûïŽè(äææ6ú>^^^�î/..†‡GÃ÷ÂÞx�;wÆÿ}º £F܉‡ÌGf&'¨¤†qA9~ML`0‰QQ~R’“Œv¡ñÁö­›‘�`÷*DDDDDg0A¦èŒðˆ»þUUUøø£•˜7ÜÜÜê-ïéå…#ÇOà÷´suþ«™¿áfɽ½QRRÒà~www<¶x1víÙ‹Ý?þˆ§ž^‚gÿ¶„7Õ?IE� ‰ññ F#12æý�”ä$ƒ˜p """¢[cÊÔ©(**Âÿ>û y¹¹¸oêÔËöîÝÇ�kósèÖ­N�<Ù¬²jµÁ!!8wî/QbÂ�ˆˆˆˆÚ”D"ÁìÄÊ?ÀƒsæB,7XöÁ9sñÚ¿_Á‘ß~ƒÉd‚.'ï-_ŽG~ЩS'¤$'£¬¬¬Eçðׇbù²w��� “É„M7bê”ɶýÌœ‰¯×~…’âb˜ÍflX¿½ûô±»X†÷î�A‘ÑœYÀ«£×+±��0””3 �ÉåðöU0FL8µ ¡‹ÏÕ@ä"h×óx¬¿ÿ ÛÞ‚ž iƒ¯Y1*„�X×XψœÖÌY³áíí�Ø™3-7üŽ;ðÔ’¿áÕ¿‚!`Ò_&ââÅ xñ¥—¯ÿ÷ <öÈ#=rD‹ÞØðá˜ûÐC˜1}¢‡ÞŽÛ·ãíw–Ùö?÷üóؾm†GGaxT$âñúß°¿”L_?e‡Yò±ÄT�äÌ,YÒ¼á-yyyضmÔ~�]�:6J• Óbg E[È/˜„GD`Hô�Œ‘qcÈ‘EúË ruÁ°@9öe¶ß‹ÒýÆ%¹,•VŒïê‹ì"3Œå•¼XĺÆzFäÔ~O«;A.—cÿÁ_š,ÿòLüË_ê=nô°aøùС&�Qß¶{'OÁ½“§4Ø0ùfý^8;SYe…±Ô‚°°°f•ÏÍÍE\\Ô~0”–Ãdáï"&ˆÚ€‹ˆ¹Í_¥ü�9�qPk@…ÕzC¹¾*Üâo±2�f|›ªÃß«±tjJéâ…�”R! MØ}1¿])‚«xeXW|‘t÷‡© ” q¥¸ßüžƒËÅÕ]µ–� †›‹+F…à€¶ß§UϪ-›Rõ˜Þ Ÿ¸ +/uðºÖP=Ðd]c=#"""bÂ�èOs{þ()GF¡ ÙÅå¸Ý_†ølC�2ýTžøK°_&ý]i9Âý<0¯w¸¹l “?L öÃ×grp¾° ™DtB�©ç Êàîæ‚¿+°:ùä•Y0:È÷uWâƒãÙ€%û3°bTžø)½når@[dÆïy¥˜ÐÍ;Îçó¢Q‡­kMÕ³J+­k¬gDDDDއs8�Cäƒ/UO ôãÅ|Œ¾Í×/¡ñÆ÷izh‹Ì0WZq<§µ…¨]läÕ2góKQ^eEza¶žËÃð@9€ê±ëÛ3òp¥¤åUVÐ"ÐKܬs€ý™ÕåC}Üyá¨ÃÖµ¦êÙÍÖ5Ö3êˆÂÕÞÈ>w ׯc0ˆˆˆ ¢¶6 “J,UH/¨ž±:£ÐcyvòªSNé.ÄEƒ©Î¶Súâ:?z‰‘š_Zg[ZA)n“Il?gͶ—ZªàîÖ²ªóÍî õƒ§“OfD¬k ÕµæÔ³ÖÖ5Ö3ŠŠŠÂÄI“9›?uXÚÜô8Ý»w·‹óá� rHc»ú`Çù¼:Ûö\ÌÇ=!~8òG‘m›‡Ð¦Šª:劮›XNìê‚7îìvÃ{TT] n©jÝÈðbK%¶œËÅá�ðñÉ˼€ÔáêZsêYkëë) tñè0³ùSÇ`4Q�— ™TÈ{»é &ø(ü07ön£‘õïÛ#c†1íœp0èöfO¼Ê„Ñuú(=ÐÅC„ù½»Ô»¿¯Ò§ô%¶Æ‹ÈUsåµFÌõO?MUx9þ"L•U·ô¼Ï”¡»¯cƒ|°çb'·£U×XψˆnNJRŽ$& Bツ³:§ÿ¼®.xHÜ�––f7OhÀd©D\¡VkÚ¯A¯ÓáÀþ}×x#%Óþ–}4Y*!–zB©Rµ_}INÆ‘#Gì6FäpÆùâ›ßuøõ²ñ†};yalW_[#H_ZŽ ¹©ùe¶2:×í ~ÉhBWo ~Ï+½åç¾ó|@zAY«{M9R]c=#"¢æð�¸!BãƒeË–aÕªU Hí½Ù„,­r©ˆÁh€Ñ`@A^.cdG8‡9”¾RxŠ\ñÛc½û�çAâꂾR@|¶w‡ø¡³‡"Wú«RAã„ÉAÛã0uÖdøvëŠ;Uî¸ðø“¨òÀ£ÌÄVO9î–UàÂÛË!¨¨À¢þ�-ybܦÓ"yÅ&2/Â{ù{P{I0± ñë (?�ÿ÷müv¥ã/Gü–$t++ÄãÿxÙÅft?¸ ûS³ÐϘ5s Ûë±/·»È1kâÔêm_­Â~³+&ôÐ@Òw†tñBå«/ãO_Ü7|ÜÕ½1¤‹*ž]‚£* ¦vëøâðe#/,Ù•ÜJl9}w%�Ƭ)³!ß®Æ>C©0kô$xŠQùÎøÅÕ÷ê IÈ éâ…ÿoïÎÚ:Óþ�Ö € q#H¡ãV¬][µu«t±Rœnú¾­ejõšiÕÎL÷½¿·Vk§vQ»¨í´Š¶N«­Ë(V[Á¥V­àBU\BÂIH€p~PR"�„ïçºzUÎòä9÷“rnž¥ìÇÙ>pã­©øU‘€´ÁrÔ/x9ÑñHŸu;úÄ À¸0àÌs/B�J1oÁ<|*Ãb#μ³ ~•xìÿ^þŠzL¸¬AÎû[0èâ9,xëºpÓ.žÄþ/"¶J‹/¾Òø¹-ÈÅ_ŸFœÕˆO<‰Òj âw~�}ç´­�â¾»îÿNò‹ù–³Új\c2ÆO˜È`u“¨p BE5ظa=_´˜i%F:M1öd[pϬ?1 Ä„yŸÍ·Í�uËfŒŸ<Å/‚•÷= ˜»î_˜‘6E«¿Âêé�yåu̼?…ßþ«ošÕ¸í‰¿bÚ#ÿ‹vÀ–›îFX] ùÇbL¹}:öæ—`[ÊPT–aÎë¯ ášøF[‡#©·£Xw3¿ø�†`³dN$_�’3y¸óÛ�h(/Ǧ«®ÇûDã|^.nÇ÷¸üÑÏø$ù.”ËúÀ|d¦¸´l#VΘ£ê¾þ 3ï¼å‘Jœ®¨aÃ’G92l ,¥…¸{ó¿€úzl’'"XJ Žàö][P]T„uCÇ£L15'ÄÑa”½»Nšƒ*I,߇i7_ûïÿÏ3¨©o@Ýçë1>m"Š–¬Çª»4~nßyw§ß‰Ók·bõ¤Ù€Ì¿=…›ÿç~ü¸%_OÈ€TÔ€??¾Ó¼ûäaëØ»a¬Âÿ>ÿ ¦Œ‡ïK*±=ùv(ËJñà²7‹¯k$8–zÎþv 3Òþ„¯ ulX""""&ˆÚ¶%õè~:„#wüÙ¶íã±é˜pü8öÜòÀïÛ&ÏÆ„c'±+e†mÛÚ»æáÚ#ùÈMI\ Æ'îÈ5ö޼ ‹ˆÂº‘· ¿A�ãC¯œT ¦¢ú:ä8~ÕpX5°ŠÃûD7>¨ OF]Ñq\5å²>€ïFÝ‚ê3'�Ë}0ú7® ü͸(Ë9ˆKU@¿h6*yœ¼� h¸èAä‡ÿö½ p4q4êÏ�Ä¥„±(S4.g¹mèX=†ü?¦£JÒ8|hë�ÓpùØA¿{>jê?·7ÏDÙáÃøiêÿØ^ã“ ÷aâ/yØuó¬ß·Ýö0FÎÃ�›?·&ÁŸ¤=‚‘ù§±wô$@eH8ÖŽ¹ÃÊtØwu2@5ŸÕ'#ªÎ„C†Ú®¡îr8Bþý-0ôF6*y¥èâôWco΃ˆ˜p êj‚Ÿr†Ž±Ûf Â⯷ÛV‚�Wl³„Ê�;4Ùn[y¸{ÃåvÛ´Q¡�h·í¤2¶åCYtb‹m¿Ä_ÓbÛ¾˜á€Hd·-÷j>‘g;Ùop‹m' mùþŽKj±mo¢ýû»Á?9Ãì?{õb vÄ]g·­6$¹W|¾+B#°7Áþ³¬—÷Ç>¹ý’—ûÇââõ(ë íËÆ$"¯¥RÅÀ`ªÃgßìë×[[׵ΈGçd8u¼^¯Gnn.TŠ”U™a®³úô{áñE‹‘þ0‡t´&952E?¼°äÃCpÒH¢îpE²�ˆˆˆˆZ2×Y¡Ö‘––æÔñ:�[¶l�J‚ @>Úy~*‰ˆˆˆˆˆˆÈí˜p """ò2&³W:ê-D`OI"òNL8ye¸ê|´j%“½Äõy�ˆ¼DDDä0jôõPëŒ>}�S�íßG~:̆ï¾Ûõƒ@DN©2Ö¢o¿�ËåQ&ˆˆˆÈ'$&&âÚënôù„ƒ¹ÎŠòËÀ±ŸF~^ß•iµx÷�⓵ŸC«+g@zú!ÎT ³„1É) F1 ëÓÆ�`0zP¾ºÓï˜�”Ïx¯2á@DDDäeN_0Ø–ÿÛ¹}Ž9 øÒC^>[·µ *ËJ!“öšk÷÷A& Daa¡GÕË`ªƒAHNMí±:”iµØ¸a=†ÅDxdÛLu�ÉûaØðá=úÙ9œ³ÏccÔ1á@DDDäe¬  4U°6€ï÷dã›o†Ålfp¼˜ÅlÆ�ÜìܾͶ­è‚Án�¯ ‘`xL$–.]Ê7ÄÌ34j5Â¥A F+ UU¨Ðë#„‘2ZêqüL9Œæz@qQ>\½ŠC,¼ÔÉü||¶n-r÷ïИT:UZm“HDä½""""ïd®³"¯¤*Eô‘¢Öb�È�Oò&ju vnÛf·âH•±gµÕ0Zê v( LŸ>ï¯Ù€Úº„ÈÃð7‘³68«­Æñ3å8_nÂ¬ÇÆoþ­¾‚Áñ'óòlɆ¦¶ÌWW2Ùà$¹\Ž´´4¨uFÛ¼&Dä9ØÃ�ˆˆˆ|‚^¯ÇÅ �ìõ¦1ïMŒ–zµÕ€¬ov"ë›�Ÿz¦Mü#bchv¥TB&“ñ Óôú ìÝÿrý‚2?œ/7á|¹É6/‘/`Â�ˆˆ¨ i4$&&2Ý ''ßnÝŠá1‘È9¥e@ìÙÿöìÿ JEŒ»qΟ(¢¢0$!ƒÇ#J©d ºˆÁ`€F­F˜, *U L5f:š‡ÃGóqèèïsmœ×Š˜hðp’@À �Z�h•Ši%F–šj”iµ¼¯ô ”«•øhå \š>iiiL8ù$© 0 Óé êqZ]9²÷ýˆ«†teeЕ•!wÿ~‰ÅP©TP©b­RñA¡“ †2í%¨KÔ(.:m*©€Þìg—dhŽÉÏ.A¨¨7¬Çã‹3 ­ÄH§)Æžl î™õ'„˜p "Ç´j¡+!+•(Õhç¡DƒÉ€ãÇ�###ƒ¡W~Ù‚Ÿ‹õè&F”L‚Iã×ÀZ‹ÅEE(.*$]{-ÆO˜È€9éè‘#P«KP¦ÕÚMüØÜ%í%ü\¬g°ˆ¨×aÂ�ì„…Épù²�� êB± è˜tðe~ÑCÐp¾z½[¶lñˆn�Dæ:«m®?äabȤA�I! ôÉ+†®ºÃ#6f¤ÁÛùƒŧO7Îι ìÿñÔÕÙÏbmPe¬…¡¦å—-œÌ�ˆ˜p Yxc¡A[ •é¥„ÊÆ±Ï££Œ®H84ûò.˜ Iù…ÝùLJõ×tlݺR©'ò¯Æä9¬ ´Ufh«Ì�÷¦@ÿß&ÜÔãà/§mÇ)}«€XÕ„Iüñß_ŽØ•Ó4ž]ˆŠŠB”²¯×$#,f3ÊÊÊeZ-,µh/]‚Åb�V«…L&óç@«¯€NW�ü‚b”éË¡ÕW ÿT1†©"àS-Œæz®0Ñ µu PëŒxtŽs=ÃL&4 dÒ@Íõ>=“©l=¤BE-Ï«2Ö6&-jê8¡#ä.QJ¥íA¨þh6üij—CoK8äïÐ8Ÿ‡JÀt‘aÇã@n„25´%ðS2Ö>›tˆP"hÒÔçíoLè™�o—¼ßê~exãws�V«À¡DDL8PW›•Õ½]¸ëµl r›ö&H4Zê‘sJÛ"Ièg·­¶®¡ÍròJ*ZÝW[×àÔD�MóWÔMTª ŒŽF©Fƒ†Ó?ÃÙ—]¾{ëÙj¿]‰ú¼ÆI"Åb1îp6—Âì÷d̲õt@Uj·®DýÑl&|€P©Eížõ¨û~ƒ-©7&9ã'pn""GdÒ@¤\­Dff&ƒq…ªªJäîßχé6hJÔ(*8ÉyÎá@í&˜=_ÿ{3J5Fê}ú~ãáÙ ŠR1XžþàSÖ¸>zCÅ%4”Ú/G¦ˆŠÂwÍ`÷î"–H0yê4Èdá¶ei­§�Àzúe Db DÒp „mäÑŸ³ -PoACu%„2µmøИԻiüöl """&ˆš? Ý3ëO(.*žݻqùrãè†Ò¢®ä]ÂÂdHNMå�‡HNMŰ#°g÷nüZüÛg«ª Öߺá“÷“œ‚kG�îòáJzÿ¬ ¹ÑnÛ=Æ£PYŸÍ©lB‹ó8dŽˆ¼“B¡ÀôéÓñþš í®lBDL8��ÁññP«KPtú44j5te|òº_ÌQQˆV©?dTªÄÃÈd2Üq×]0 (.:�Ó……Е•Áb±08^$,L†(¥ñC†àÈáÃ8�›cë½â¬ ±ÌžãRÏ#‚P1ýFþžt¸0â9›•Ñgÿ~»s †*dæ÷ÅJýl8"ò:r¹iiixêÍ� "&ȨT1|P%ê†ÄèkGcÔµ£ /·sû¶�Wk± ªªÒ¥„ƒÊZ�¹CB kcR³+'ެõzÜ3ëO H —áâ…Rèõz�¨DDD>Bï‚ìKŒtá‰X‚8“O\ÿĉÎ-7š2æzÈår‡ûdÒ@ ì¯ìTú˜þ5Ôµ[FBBB«I(w”§Oo· ¹\ŽëG ïTÚ,£®ÆàÔ—ß¶ÊpGÛx[ûº£mÚ+ÃmÓí«Rq4"_À„õjµÚåsÄb1¢”J�ˆ<ŽN‚Ý\K8D)•XZ h½ÿú¥R)ÒÒÒ:UFbbb§{¢4Mb×Óeð˜2ÜÑ­×mÃöíš¶ñ”ö%"&¨�ÌσF£f š¾˜—•Ál6wè܈ÈH„††2ˆ UU Â6nXÏ �[ìÍΆX"é𽊈ˆ|‡2J‰™xaÉ F+†��zøc͆o Á„Coz04`0ˆß‚‘HÔ¡s++*PYQÁ 6ÓÇjìÕ×/Åï]c;Òs†È‘2KK…:†ˆè7X¶lR®V¶9Ù§/K$P©b|ú;K&“¡�"Š1ò L8ô·X !ELd0šQDÀ,»4s|VV4 úX«!ïåØW>l§XÎôê$Õj ö�„N$å‚ìœêÛásÖ•Û%TÖJ¨¬îMv ì°Èq›ŠˆˆˆÜŒ ‡^ò ”T«a ®ðfØœêëÒÌñÁÁÁ€Ë¤Õä1ˆd#·1§úA-dÊ;>Sw†é(ë�Ÿ\A!1¡¯Ø¥×¨ªªÄvs&ˆˆˆÈíü"""ß ·1±¿˜� """�À„¹‡TµÁRgE�à‡XÕ£�IB¸||Ë9¥ÅGo½„®áõa"""Q ÄÓÇ\[�H¥Äc!œç‡ˆ¨-Ú*3Œ�âžYê±:XÌfhÔjȤ�£(U<ÆO˜Øcu0 ¨Ðë<6F½DDD½˜X"A|@ ADäá´eZlܰÃc"ŒVäŸ8�Ã9û#„¹DDDDD䕤R)† ‚*c-¬V�!ò0L8õbju ¯Â@‘WR©TX´hòÕ•0Zê"Ä‘�H¬×âÕ$ADDD� """"""" R„àè‘Ã(((ðˆú0á@DDDDDÔÎC\„¨o½¹„Áh#F¥§�#ký F·ÃÏ?Daa¡GÔ‡ """aâL5Ç0‘g`Â�ˆˆÈG¨ý#ña‘É¥sTª¼~šÁ#"""·cÂ�ˆˆˆˆˆÈÃ…‡G`Lr Ô:#ƒÑŠèâ®fŒ<DDDDDä• �™™‰”«•�I}úZe2’SSù0Ý•*ñ‰C#Àx'½_ • D'˜ü‚:Æ¿ÇÞûŒ=õ"š€ˆD.ïáòk l¬ˆäÀÛ¶¿@Ÿˆˆˆ:� /•+‰C®$Ž�èA êhüÅqÈ3þ=òÞgì©É íò9ÑÑ*—ޝªªDŽIŠÙDœˆˆˆÜŠC*¼Ltt4ƒ@DD­RÅĸ­,…BÁ€Q‡±‡ƒ—Y¸p!Ôj5á6l@iii‡Î3f RRRÄNZ¶lcO|Ÿ·!==*•k=:Úwë‰'žh±ÍÕ×&"òUFËïK[Ìfˆ%’©ÇÉ‚_=>Veee=úúgÕø†õ L8x©TŠÄÄDÂM±ì(…BÁvè!Œ=õ&*•Êå÷»N_]y¹Ë¯ÅÏQë,µV@Tß~°ÔÖöXÂA«/÷ÜÕ5Æ(>>¾GëqN]Ú«ß«y%xò±ÿEJòõQ© ""òjµ®^åÒ9áá˜RADDä#š–Å g0ˆ¨× Àß¿ý%#ÛšÁeHýØøw݈� DËŸw@ŒÀ?”–›`4×·:Áaó2ÚR[×ÐêD’þ~"„H:UÐØ;£=V«Ðæ î(£;Û×mÓVîhgË`Â�ˆˆˆÜB­VãÃÕ«ðø"ç'€T©bðÖòå y½Xe(ÂC‚Ú=.ç”¶Kˈ —´˜Ü088r¹�‘¡bD„!_]Ùêñ£2Þ÷uÆVç5‘`xLd§ÊàTUÆZä«+»´Œîlß¼’ŠN·M[e¸£mœ-ƒ """""¢N8±w3Þ|óMœ>}Ú©c[ãŽ2¶lÙ‚­[·Ú%.\¹\Ž¥K—B£Ñ@$áž)©øË_þâT­ytNÒÒÒî+((À²eË:Udff¶[ÆuIðþ³E]ZFw¶ïÇË_Fbbb§Ú¦­2ÜÑ6í•‘��À„‘;dddÀd2õx‰¤E²A¥R.\hK:œ'J©Ä‚ <""¸+ÙÐÄÙÕ+|…Z­FMM €–«>4ß×$::Úc–y$×0á@DDDDD䤿É qÉÇãÇ�ãøñã€k®¹ÆéäƒÉdÂñãÇ¡×ëmei4ðé¤Ã+¯¼bû÷Ç�ø›n¶ý¼nÝ:¨KJ쎿ÿ�‘�˜ˆ¾Q ¾½ DDDDDDNÊÊʲûùرc8vì˜í眜¼öÚkN••››Û¢¼+÷'%%!))Éçâ83#*U ÀÔ¬GÃÌ{2o±XøæóBœÃ�ˆˆ¨+Ój±bÅ ‚ˆÈI mîoê­à “ÉÔæþ>}útj¨†/ùaß>—bKž�=ˆˆˆ|„J¥ÂCÏué³ÅŒâ¢"�ˆÈIóæÍs¸}Ë–-غuk‡Ë]¹r%ƒÛ†}{¿Çu£¯µ›ï�<{8ù©TЏ¸8‚ˆˆˆ<{8Q·ylþ|1½{8Q·‰��‡X"a z&ˆˆˆ|DAAž~ê.�£ŒRâ±ùó<""òh÷?ð 'ÐôBRADDÔ‹‰%ÄDd ˆˆ¨Û¡"—z9 Š�…T*eð¼ {8Q·Y±b´eZ¢`Â�ˆˆˆˆˆˆˆÜŽ """!•JëÒ9ju ÿë_<""òh¯¾ü /Ä‘�P©T˜ûH&ADDD� """"""ê6qƒC"沘½DDDDDDÔm,X€(¥’�è˜p ""òz½ÙÙ»]:G"– nð`�ˆˆˆÜŽ """¡Óé°{×.—ΉR*±`Á�ˆˆ<ÚÓÏ>‡ÄÄDÂË0á@DDDDDDÝfÇŽ0 D/À„u›íÛ¶¡ªª’�è˜p ""êÅ vìØÁ@‘Gûaß>èõzÂË0á@DDä# &L˜èÒ9UU•ؾmƒGDDmßÞï¡Óé/Ä‘��Ëå˜xË- y„†€ˆˆˆˆˆˆºËcóçC€ˆ�è˜pp`Íš5ÈÍÍu{¹™™™-¶ãÑGå/DDDDDÔ+ÄÇÇ£DSÊ@ôL88pìØ±6÷;]–\.osMM 4 ]H­V£¦¦Æaì;J¯×£°°Ða{·×æDD]¥  Ë–-Ãã‹;}Nxx&OžÌà‘Gûã¸qP( „—aÂÁ�… béÒ¥¶Òèèh$%%Ùö7ÿw{šŽm>£êîÝ»me'''câĉ zùv·ÜÜ\‡½`‚ƒƒ±|ùržˆ¼†L&Ô©S""òhãnº™ØóBœ4Ò•J…… Úz2h4Èår¤¥¥!-- *•Ê鲤R)RRRlçêõz»dÜ9sð.¤P(Úí‘âJ{¶w,o‚DDDDDm{ü¯…Z]Â@ôìáÐÆƒeóžk×®¤¤¤t¸ÌæsC0ÙÐ=äry‹+³gÏîp;fdd ##ÃösNNŽí½�… 2èDDDDDD`‡6]ÙÓaíÚµÈÉÉéPYL6øF;6ç(Ù •Jp"ò*ju ÿë_""òhŸ®[ µZÍ@x&ºáa•ÉßhÇæ˜l "O”˜˜ˆW_{�� ""ŸSrîL&áe8¤Â…‡ÕŽ ¯`²Á7Ú‘É"j‹ÉdBnnn§V¿q$77§OŸn±=99™sƽùa•ÉßK:0Ù@D­%²²²Ú<¦#÷ G«áÀ©S§°hÑ"žˆˆ¼Ò[Ë—£DSÊ@ôRÑ�‡Ugºå3ÙàíÈd9£½Þ}úôqiIå!C†´{sÄd2áÌ™3l"""òìáÐÁ‡Õ¶þBÎdƒo´#“ D䬤¤$Ìž=ÛvŸÆÂ… ]Zv·¹æ½L&–.] �Fcû½Ò|µœæÔj5>\½ �/ZìÂý0o-_ÎF$""�öô³Ï!&z áeØÃ¡«ŽþBÎdƒo´#“ D䪔”Ìž=PSSƒ¥K—vz6mGÉþ^!""o·cÇ ‚ råauéÒ¥L6øXÒ�É"ꩤ“ DDä«¶oÛ†ªªJ‚ råaµ°°�_ }(éÀdõTÒ¡;“ ³ÅEEl8""òh\“ >¬2ÙàSI&ˆ¨'’�I6( L˜0Ñ¥ºjË´X±b�ˆˆ<Ú§ëÖvz¨"1áàÕ«÷Üs“ >”t`²�ˆ:£#I‡Îöl�Ëå˜xË- >yŸH8ˆD"ˆD¢ÛO�:…äädµzŒ;V'NœèRýȳ“L6Qw&8gõ“'OFxxÁ„CÇþwìØÑãß�=ö8€uëÖõêFnŠyÓþþþèׯRSSñá‡òSà éðÚk¯áÙgŸe²�ˆº%éÀdõ&S¦N…L&c ˜p踧žz ‚ tIÙ/½ô’]ÒB‡¯uüøqÀÝwßÝê1=Y¿îÖT�ÚÚZìÝ»!!!˜;w.^}õU~®ÀDuWÒÁ�ɵZ�W¯réœððLž<™�DDDMÃïèL84 Ä/¿ü‚M›6uI¥·nÝêÔqf³ÙVŸîälýzŠ¿¿?ñÞ{ï€íÿDDÔ½I‡‚‚·öl0™L8sæŒKçÈd2L™:• DDDíÁÙs R©&???<ôÐCxöÙgaµZ�:çâÅ‹xä‘G0pà@¡ÿþxðÁqîÜ9Û1?þø#D">  q¨@hhh‹a¯¼ò D"ŒF£í¸Ö†r\¸p<𢢢†n¸YYY-ŽûòË/1iÒ$(  22&L°K.8[?W®Ùh4B$!>>EEE˜6mÂÃáT*‘žžŽŠŠŠ·Stt4´(£µú^¹½¶¶K–,ÁÈ‘#!“ÉŽë®»ŽÃ4ˆˆœL:,[¶ŒÃ(ˆˆ¨×Y±bÊ´Z‚ ‡Ž©­­Å³Ï>‹sçÎá“O>i÷øªª*¤¦¦bóæÍX³f ôz=6n܈]»v!99eee€±cÇÚ MÕÕÕ-Ê{æ™gZçÈåË—‘ššŠ#GŽ 77çÎ�àAƒ�‘‘�Ï>ûÌvܻヒ™3gÂßß9990�8pà�––†õë×»T?W®Y,ÊÊÊ0gÎ<÷ÜsP«ÕHOOǦM›ð÷¿ÿ½ÃíTPP¸æšk:tþüùóñä“ObÚ´iÐh4ÈË˃\.Çܹs±lÙ2~ºÈi[·nEfff›ÿùbÒ¡ “ DDÔ›Ál13L8tŒ 8p æÏŸ�_|Ñ6´¡5Ë—/ǯ¿þŠgŸ}·Þz+ÂÂÂ0vìX<÷Üs¸páÞzë­.¹øwÞygΜÁóÏ?�øøxôéÓo¾ù&Äb1V®\i;nÓ¦M�J¥x÷Ýw‘��±XŒÄÄD¼óÎ;{T¸ÊÙk dffb̘1�ÉdxòÉ'ß~û­Ë¯mµZqâÄ Ì›7ÁÁÁxã�7:¿¦DËÓO? ™L•J…wÞy¡¡¡Ø¶m?]Ô®¦^6®àØ=ò¥¤Cppp�'Ê´Z¬X±‚�BDDíÓukÛ]^š9}ú4 &&¦Å'(..î’ú�:u Я_¿v�ÍÉÉÁÛo¿�_~ù/^„ÙlFmm- ®®Îå×îÈ5÷íÛ×öopzŽ À~hIee%²³³1þ||üñÇØ¾};èÒ5,^¼O=õÒÒÒpë­·búôé¸ãŽ;:ôWkê�är¹]"�ˆ:G*•"..ºòrƒˆ¨îÁÕ§O�|’Hpóz�MEo*öòåˈ‹‹Ã¼yóðâ‹/¶Ø4þ¥ÜjµÂjµÂÏï÷Q ð÷÷‡¿¿?êëë[} Ge^¹ÝÑ1���¨¯¯oñºWúꫯ�žžŽØØX¼þúëHNN†\.‡X,¶ý•¿y¹ÎÔÏ•kvæú\i“æ¾üòKÌœ9³fÍÂ_|áòëmܸË—/Gnn.A€ŸŸÒÓÓ±jÕ*®­KDÔtúr—ju 6mØ`7”�ˆˆ\g2™�››Û¡¿Â'%%õªL55(Ñ”ºtÎ[o.ÁO<�ÄÄD¾Ù¼H@W¿@XXžzê)<÷Üs˜?>D"Q‹ÙÐÐPTUU¡¦¦!!!¶í555¶ý]!** .\€ÑhDXXX«Ç½ð hhhÀ'Ÿ|‚qãÆÙ¶wf…ˆžºæ+5 áØµkW›ÇµÖ“"==ééé¸xñ"¾úê+¼ñÆذaüüüðùçŸóFDÔÍ_vÏœ9ƒ°ðpƒˆ¨›I¥RöÜ$º‚_w¼È¼yó�W_}���-ö7e©š/Ùüç®Êb5eÏŸ?ßæqMs4Üxã�vÛ:Ôá×î©k¾RÓ*‹Å¶ÍÑp�K—.µYN¿~ý0oÞ<ìܹðÝwßñÓEDÔÍÔj5>\½ÊÅß…1xkùr�ˆˆ<Úý<Ø«z�0áàâCíóÏ?�>øÀáþ;ï¼Z¬lðŸÿüpûí·ÛX›x¸­IDATmoÆàÊüŽ4Mtøý÷ßÛ=ð‹Åb»ä‚R©`ߣÁjµâõ×_·ýÜ|ȇ3õsõš»Jvv6 55Õ¶­i®ˆÒÒÒÇ5—žž…B�¢f“�) p˜X""""""*.*‚ÅìÚ²˜ƒbc¹Z™juH…Z­¶uïïˆ+WnHIIAÿþýqöìÙû§N�ŠÕ«Wã•W^�R©ÄðáÃqâÄ ¼ôÒK¸êª«0mÚ4»ãcbbð믿â‹/¾À°aÃZ}MGÛ›ÿû¶ÛnÃêÕ«ñòË/cРAˆŒŒÄóÏ?�ÚÚZÜ~ûí¶c'Nœˆ5kÖà¯ý+ž|òITVVâÿþïÿ ‹‹³gÏbåÊ•¸é¦›äTý\½fg¯ÏÙ61™L8zô(ž}öY„„„àÑGµíONNÆ—_~‰§Ÿ~�?þ8ŠŠŠðöÛo#$$F£Ñv\ll,ôz=~øa¼üòË Á?ÿùOÀ½÷ÞÛ¡Õ;ˆˆ¨ã4MÇÏsï”NDDD­Z±bffd@¥Šqú“ÉÄç Ýf"ÈᤑkÖ¬Annn‡^pժƮœ�<òH‹}ÅÅÅØ½{·Ãý5558|ø0JJJ`6›ŒAƒaôèÑnñÅhß¾}0�¶MÌre™ÍëÒZ½ª««qðàAh4444 ""#FŒ°-y 4®BqèÐ!œ={f³¡¡¡2d’’’PRR‚ýû÷Ãd2!((³gÏvº~Î^skuo+ÖŽŽkÎßß¡¡¡ˆŽŽÆÈ‘#íæ°°X,Ø¿?4 ¬V+”J%ÆŽ‹o¿ýÕÕÕx衇lÃ.òòòPPPƒÁ�Éd¸úê«1tèÐËpQ÷x|Ñb§�µ˜ÍØ´1 Úv†Î¹“« �Z��Ö3pè™gžiu¸‹Ã„Ûo¾i[¶‘ˆˆˆ¼GX˜ »°þ{Ó*DDDÝÉÕ„Ã[o.aÐ<ÔôéÓ‘––æp_›«TDGGãž{îa‰ˆˆ¼„€Žõ.{â‰'<""êË–-ëÐyéééœ8ÒËڱ̈́Cpp0×9%""ò":}9tåå.ŸÇß÷DDÔ]â†D,qù<•JÅßW^Æ�! """""¢î²`ÁDý¶ ù6&ˆˆˆ|„^¯Gvön—Αˆ%ˆ<˜Á#""�¦Š‰á²˜^ˆ """¡Óé°{×.—ΉR*±`Á�ˆˆ<Úƒ³çpþ/Ä„u›+V L«e z&ˆˆˆˆˆˆ¨ÛÁl13½DDD½X™V‹+V0DDäÑ~Ø·z½ž�ð2L8ù•J…‡žëÒ9f‹ÅEE y´}{¿‡N§c ¼ DDD>B*•"..Ž� """�ÀQwylþ|1½{8Q·‰��‡X"a z&ˆˆˆ|DAAž~ê.�£ŒRâ±ùó<""òh7 …‚�ð2m©¨©©Aaa!£DDDä4�Ëçˆ%øAàï{""ê>"àâ�Šë®¿z½ž+Ux™€ö¾¸,]º”Q"""òaYYYP—”0DDÔmffd@¥Šqúx�Fƒ�Ö3p^Æá� vU!""""""¢ö$%%µºÏa‡9sæ`âĉ0™LŒ‘—P«Õظq£‹ç”@]R‚'žx‚$"¢n±lÙ2—ÏÙ¸a=ÒÓÓ¡R©@¢R© •J[ÝÐÖ‰DDDä=‘4êZèÊË;t.‘»˜L&ÛÜBr¹r¹¼ÓeŠD"ˆD"DGGÛ=äêõz·”OîÀ‘;âý÷ßиÂĸ›n¶íS©T�ˆ][3*J‰¬¬,Àý<ˆA±±0›Íø×§ëзo_Ì›7�A÷@L8‘[%%%aöìÙX¿a®þL55¶}33f¹\Þý³gÛý\QQ�MYˆ9sæ0àŠ """¡×ë‘���‘I£œ>G"– nð`�ˆˆÜ.%%r¹²ˆT.»­\‹ÙlK6,Z´¨Í9¨g‰A"""ïR^Y‰††ìÞµ Ù»wÛí{|Ñb—Ê —…!00°zåJœ={Ö¶oöìÙHIIaÀ‰ˆ¨S.\ºä¶¤CÉÙ³8�›Ãdƒ`Â�ˆˆÈ }þùç(Ó둜’ŠªªÊß“á�Éd.·L«…Ùb†`mÀñcGñØc�ñ˹ÅÑãÇQ]mD”RÙá2ÄAA¤Š†ŸŸê˜p ""òBjµK—.ÅUƒcò”©n-›_戈¨+äää`ý† H¿'£CIþ~ò>l)"""/¤R©°páBœ).ÆŽíÛÜR¦Ålæ—9""ê2)))˜•‘��YP¦Õºôûé_ëÖ¢²\ÏßO^†­EDD䥚'J[ë¼£š&à:qü¿ÌQ—ižt0 Nÿ~ Dbb"èe8¤‚ˆˆÈË™L&øûû㜦 .ŸÏÙ¾‰ˆ¨»´»z?y?&ÜP‘À°Qw³X,.'øeŽˆˆzR[«W:pgÏüÊßO^Ìí}&E"‘SÿpøðaÜ{ï½HLLDXXÄb1 „ûï¿yyy ¹D,ãb©Æ¥9jjj “ÉøeŽˆˆzDÿ¾}Qc2¶˜ÓA„ûï»—¿Ÿ˜ppL„6ÿsÕK/½äS‰Š­[·"%%û÷ïÇ[o½­V‹ÒÒRü¿ÿ÷ÿ°mÛ6Üpà 8tèP—½¾¯Å“ˆˆ]uÕUNO$)  ×�ÆâÅ‹ùeŽˆˆzL�Ñh7‘dó ŒùûÉ»¹}HEW )¸á†pøða�ªàìõ�1yyyرc&Mšd·ï_ÿúî¿ÿ~Lž<Û·oï’zzK<‰ˆÈuí-™i1›!“ɸyŒ¦%3Ãe2̘1£F�bP|@�~Ë0�2d .]ºd·ïüù󈈈@tt4D">l{  TVVâ/ù  „   8þóŸ¡ÓélåTWWC$!!!F£<ðär9Äb1D"âããQTT„iÓ¦!<<J¥éé騨¨°«Ï—_~‰I“&A¡P ‘‘‘˜0a¶nÝÚ¡k?}ú4`ìØ±-ö͘1|ðž}öYèt:H$$%%µZÖõ×_‰D‚ .`É’%9r$d2ÂÃÃqÝu×áÃ?´ûã�?:Œ§3±4�¶˜Ûb†)S¦@­VãèÑ£¸õÖ[[�emmm»u$"¢Îi¾zÅOæ·H6p5 ""ò4M«WÈd2®FáK7 ¸Rlnn®àïï/Ü}÷ÝvÛo¿ýv! @8pà€ÃrM&“0räHA"‘YYYBee¥�••% C‡ŒF£ ‚`±XB¿~ý„¹sç ãÇ�Þ}÷]áí·ß2™LHMMrss…ªª*aÞ¼yá‘G±½ÖŠ+”)S„‚‚Ál6 §N�n½õV€ðÅ_¸|ý‰‰‰áĉí;kÖ,€ðÓO?µØ—ŸŸ/î»ï>aîܹáoû›PUU%”””“&MK—.mµ�œ�e]]�-fãÆ�8`³1cÆ×\s�pðàÁVcéJ‰ˆ¨st:�`6›…‚¢bá¿…§…c¿œžyæáÅ_´ÝÛ‰ˆˆˆºJ�'Ažy怰qãFAá‹/¾Ï?ÿ|«å¾ñÆá©§ž²+ëùçŸK–,ABPP�pç�w õõõ-Ê\·n�mÛÙ³gÂÀ�mÛn¾ùfA*• ÅÅÅv¯uêÔ)€0lØ0—¯íÚµ¶DȪU«½^ßê±ÙÙÙáÑGm±ïÉ'Ÿ?üðƒ& ƒmAA�*ÜrË-­¶“³±l~ÞgŸ}Ö"fW&_ÅÒ•:‘{˜Ífáx^>“ DDDÔ­ºl‡vzUØý\WW‡äädh4ìÛ·cÇŽÅUW]…ýû÷# À®Ü¦sÇŒƒƒâ矶ßóóÏ?côèÑ7nöîÝkwîÞ½{1nܸu---Å€V«ð÷÷G}}}›×Ñtl`` jkkÖ³-[¶lÁóÏ?�£G�ÂÏÏ£F�¤I“pÏ=÷´B‘˜˜­V‹ .@"‘Ø^?&&‘‘‘ÈËËCŸ>}PQQ�ï¾ûS§Nuª�AèP,Ŭi(Lÿþý[�¥+u$""÷Ù¼y3Nœ8ÁÙ¾‰ˆˆ¨ÛôÈ*W ÄgŸ}ƒÁ€ë®»F£Ÿ~ú©í!Ö‘S§Nl·=!!pîܹç >ÜaY}ûöµýÛßßßö°Ü\NN222ð‡?ü‘‘‘FPP�-aÒiiiøùçŸqúôi¼÷Þ{ˆ‹‹Ã?ÿùOŒ5 ééé¸|ù÷õhçÎ�‹ÊÊJ|ùå—¶m»víÂùóç‘™™ X¼x±­Ü©S§âÝwß…F£i·‰¥£˜9ËŽÖ‘ˆˆ:gòäÉxî¹ç˜l ""¢nãQ«TL�:Û·oÇ”)S°mÛ¶6Ë h‘hN,Ãl6·Y'g·õÕWHOOGll,^ýu$''Û&žlJŠ4ÛÙU:ª««1wî\¬_¿™™™øàƒ:�Djj*²³³÷Þ{/¾þúkœ?ááá€�7bùòåÈÍÍ… ðóóCzz:V­Z™Læð»3–®Ô‘ˆˆˆˆˆˆ˜pètÂáÛo¿ÅôéÓ1tèPœxÿ}L˜07Þx#, 8ˆ<¿ã__|�øøø¯uåõ®^¹&“ r¹K–,ÁöíÛ±fÝ:ôë×:�Ÿ|ü1Þùç?!‘H “É�™™‰ØØXÌý­¼{ï»/¾ð`ìØ±¶×yhî\üaèPlÚ´ �Λ‡¾J%qäÈ@RR’]<”}ûâ|i)î¼óNDFFB.—ã…^@´J…ñ&àÔÿ‹ô™3QYY‰“'OÚÅ®)f¿þú+>Z½ÚaÌ®Ôt~II ôz=�7ËÞz :�Y°0hР× ÀaÛ´÷^t¦Œ+ÛÆÑûùÕfI%G×ë¨}Ïž=Ûj]•áŽë½²®Î\¯£ºòzÛ®k{×ÛÑ÷bo»Þîúìõ¶ëíŽÏï­¼·òÞÊ{ ï­¼·òÞÊ{kó˜:p Í?P;œÃÁd2!//UÕ—í¶+£”ÿ¶$#¨Õ-‡Lš8Ñ©LÇ OÃh4"mÚT˜Íf|´f BCC*U ÊËË1mÊd„„„àý•+q2?Ë—-ƒN§CDD$~ÌÍEuu5>xÿ=lÿî;\ºt AAA2dfÌœ‰;î¸ÓV×?$ ìl,‰X‚qcSmu1 ¨ªª´»†ƒ?�L&ƒÉdÂ[K—bû¶ïPUUeß¾¸uÒ$dÌš…â¢b¼úòKÐjµ ± ƒhz­ðð»‰Ë´Z˜-f˜L&|½y3rrrPªÑÀd2!88Ñ*F]{-f̘�ˆÈH[]£~ëeQ^^Ž›ÿ8���ø"+ ÁÁÁvmó¯Ï>Ŧ¬�8wî,D"úõï�i·Ý†™3ÓÑ·_?[=þ³s^}ùeèt:„GDà£O>ÁW›¾ÄÞï÷àÂ…  B||{¼·òÞÊ{+ï5¼·òÞÊ{+ï­Meäîßí¥‹øÛßþæZÂÊtzè› ! Ï V—`ê¤I˜=ç°¸�†%""""""ê*Î$ü&ïòæo@*•⡹s """"""òXçp0™L8sæ dŒ�¨««Cy¹ï­xÿÙ¹o¿óŽm9N"""""""Oä°‡ƒZ­ÆG®ft<Äõ׎Â-ãÇãÄ/Çñá'ŸàÖI“""""""ê1ÃFŒÀ]3f´yLÃäùŽ�ÈcˆˆˆˆˆˆÈcÈd2 8 Íc8‡¹DDDDDDDävR©±±±Œµ�»?V¼óN›Ç8L8¨T*<<÷F�ˆˆˆˆˆˆˆ:„C*ˆˆˆˆˆˆˆÈí˜p """""""·s˜pÐëõØ“�ÍèQ‡8Ú¨Óé°{÷.Œ5Š"""""""";É©©P Ðæ1RADDDDDDDnÇ„¹]€³æçåád~žÝ¶¡Ã†cØðá¶Ÿ÷d¬Ìî˜ôŒY¶—iµø~O¶ËeÜ<~¢”ÊVËPDEaü„‰mÖµy°qÃúר¼®]u½WÖµ½ëuTW^oûumïz;ú^ìm×ÛŸ½Þv½ÝõÙ㽕÷VÞ[yo录÷VÞ[yo录÷Ö®þì]wÝu˜:eŠk ‡ÄÄDLŸ>W‰·m5Xàow\BB›S^¦…^¯·;¦yQ}"QY®w¹Œ¡W'B.—·Z†\.o·®ÍË€¤k®iqÝÝq½WÖµ½ëuTW^oûumïz;ú^ìm×ÛŸ½Þv½ÝõÙ㽕÷VÞ[yo录÷VÞ[yo录÷Ö®þìõï×m ‚ €ˆˆˆˆˆˆˆÈ�8‡¹DDDDDDDävL8‘Û1á@DDDDDDDnÇ„¹ÝÿÙ“—4ìIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2563567 ceilometer-27.0.0.0rc1/doc/source/contributor/ceilo-gnocchi-arch.png0000664000175000017500000034212315250267452024076 0ustar00zuulzuul‰PNG  IHDRd¯KrÍübKGDÿÿÿ ½§“ pHYs  šœtIMEß  &în„i IDATxÚìÝTSg¢/ü/¶‚%€¶¸¾@e’VJæBµÕA8k¤wJœÑU{P�sðV<÷¬Z;Ö÷p‰ëŽu�¥í]ƒ«Ð÷Ø™ñÇ{çT}¥óÚ#JG—ÜÕ0ëÎ )Õ¶ œF:¡XBÛ5$Thßæý#<�;ÉN¿!|?k¹J“ì';ÏÞÏÞ;ß<ûy¢<�DDDDDDDDDD4éæ± ˆˆˆˆˆˆˆˆˆˆ¦Y"""""""""¢)Â@–ˆˆˆˆˆˆˆˆˆhŠ0�%"""""""""š" d‰ˆˆˆˆˆˆˆˆˆ¦ÈýÁž8wîl6[ØmÙ²iii!_Ã2Y&Ëd™,“e²L–É2Y&Ëd™,“e²L–É2Y&Ëd™3¥ÌÅ‹ãé§ŸÆT Èvuu¡³³3ì‚ìv;4MÈ×tvv¢««‹e²L–É2Y&Ëd™,“e²L–É2Y&Ëd™,“e²L–É2§­Ì/¾øíííøâ‹/°lÙ²ÃÛ‰4�u»Ý£*¨½½7oÞ ùšþþ~–É2Y&Ëd™,“e²L–É2Y&Ëd™,“e²L–É2Y&ËœÖ2Ñ××ÀTŠòx<µ'®\¹"W*‹-ÂüùóC¾æÎ�;b™,“e²L–É2Y&Ëd™,“e²L–É2Y&Ëd™,“e²Ìi+SÈ–––"##S%h ûþûïãöíÛ """""""""Š$ÓÈ YÐÝÝ�·ß~N§±±±xüñǹ…ˆˆˆˆˆˆˆˆˆˆ&@@ ;00€ŽŽ@bb"kˆˆˆˆˆˆˆˆˆˆh‚ÌcM ²DDDDDDDDDDS„�,ѹŸU@DDD³‘Õj…ÛídggÏéºhmm•Ïõº—ËåBgg'@£Ñ@¯×³RˆˆˆˆhJ0�%""¢Y£µµµµµ0›ÍÏÅÅÅ¡  ………HNNŽøº¨¯¯Gmm-¬VkÀsz½(((@\\\Èr¬VëŒ #×­[(..ƶmÛ&¼|—Ë…wÞy&“ v»=àùììl�F̺ýáìÙ³¨®®ÔÕÕñ`ADDD4ƒ1�%""¢ÏåráðáÃ>=AÕ^S[[‹ÚÚZlÛ¶ ÅÅÅ[û÷ïW b«Õ «ÕŠÚÚZ¼üò˪�«ÝnGUU<Ž9ñû�ÕjÅþýûár¹‚¾¦µµ­­­0™Lxùå—G ³‰ˆˆˆˆÆ‚�,Íhv»‡–dRR ¡×ë‘�� »Ý»Ý“É„úúzÞÞ‚V«Œ¸úP†±YYY(,,D\\4 Ün7\.ª««ÑÙÙ »ÝŽýû÷ãÍ7ß +++ÑÖÖ†¬¬¬9±)ÃØ‚‚äææúÔ›ÝnÇÙ³gÑÛÛ‹ÖÖV>|l€DDDDêñÇGtt4 ÒÒÒ¦ô½ÈÑŒ¦ c PVVæó|rr2’““‘���mÛ¶áСCèìì„ÙlƉ'°sçΈ© “ɲ.„¼¼A«N§CnnnÐ÷S~ΑdggË^²þë000�¡µN§ (×n·£­­-`¬¬¬¬°z�Úív˜ÍfŸúÍÊÊÓDbbÝ�{=£Ã!êM£ÑŒøÚääd$%%¡··7äë\.ÚÚÚ|¶]rr2V¯^tÛø×s}}½ü<a6—Ë%ßW¼VY7¡¶‹ÿþ6Ò:�w9«ÕŠ÷ßLûQ¤ dÓÒÒPZZ ‹Å‚¡¡!ÖM‹¦¦&$†êÕ¨&;;:�N]àr¹dpd2™P]]-Ç_}ýõ×&zª®®Fqq1 UË5ÉØÙ³g‘��­:)”ò½wî܉×_]ur.½^�—_~9 ”ËÎΖ½=›šš�——²ÊÊÊ‚l±‚Õj•áuEE… Ê\.Nž< “É´üäää “†�´¼ÑhÄóÏ?öÄYÊI¹t:ݨÆwÍÊÊB[[>ýôSØíöƒÜÓ§O�¸ožLDDD‰îû§ú§R>0þ|,^¼wîÜaíÑ´ihh�áãŽ;Âî )ܺuK.ÿØc�É�ú[[[ÑÖÖ†¨¨(466Âår¡  xì±Ç`·ÛqëÖ-|øá‡p»ÝX¹r¥O¹.— »wï–½u:Ö¯_�ììlÄÅÅÁf³Án·ã£�>š5k-—ï�úúztwwC§ÓáÉ'Ÿ„N§ƒËå‚Ûí–ë¾~ýzŸ÷Öh4¸rå  ±±n·iiia‡š€·—©xŸ¯¿þ��=ö’““‘››‹‡z€w¬Õk×®ðšF£ÙÙÙÐëõøË_þ·Û ·Û�?ü0 ¸v¹\Ø¿¿\^YGÐÛÛ‹ÎÎN|ôÑG>ŸQŠÙÙÙ>=(ÕÂØÑ|f—Ë…?üCCC¨¯¯Gtt4|ðÁQ•!˜L&TTTÀívC£Ñ ??¹¹¹r‚¹[·nÁl6ËÏ¡$>ŸÛíFCCƒÏsùùùxòÉ'ñá‡âÖ­[>ÛÂß믿Ž[·nA§ÓaË–->û€€±dM&>°Îz½ÝÝÝp»Ýhll„N§ó™ÐBùYï»ùùù>Ÿµµµ½½½½º_~ùeÆ*—¡³xO£Ñ8¦í@DDD4,X€ÔÔÔ)_Y@DDD3’²g�N§õòÙÙÙ2ëìì è‰'züùOòTXX('vª­­Eaa¡O|øða¹ìóÏ?��7¬÷¡C‡`µZQUUå3&© zÅú/ïr¹PUU³Ù «ÕÐ V¯×£¸¸X~®ÚÚZÔÖÖÊIͲ³³‘••2¼6�0�Ø·oÚÚÚ ÓépäÈŸ×('S›kçÎ�rÒ0»Ý°žÕÕÕ!—¿xñ"Nž< «Õ “ɲôxÃXظq#ÚÚÚdoé'NàĉÐëõòöù¬¬¬˵Ûí²gªN§ÃÁƒ}꺰°Pn¿êêê€`Y0›Í>Ë‹ºJNNÆÉ“'xÇýUëyl·Ûåëƒõàö½(S­þŠ‹‹ñÓŸþ½½½xýõ×åvô_®´´Ôg}¶mÛ&÷“ɄիWËe­V«l¿jmD´1·Û�³gφ= Q¤à¤^DDD4ãMVºâââ€00..r¼Ñ³gÏÊç”ASaaa@Ðxƒ`”™ÍfÕ[½ƒ-çsû·rÌNaÛ¶mxùå—}ÆCµÛí0™L¨¬¬DII JJJpâĉ ï=q+}RRRаT*×ÓårÉɰ T—߸q£ ÙÅ8Áj&"Œ<pk½ÕjEmm-:„ÿøÇؽ{7jkk†°ľ ÑhÂX±ýJKK}&• µ>by½^½^�¸¸8ÙÓTô² ¶m4Mᆕ”ŸçàÁƒõ''ˆs¹\hjj XÎ?Œ”ǽõÖ[òqå�)jÛ_¯×Ãh4"++kL?¶Ívì!KDDDO9é–R°†"«¯¯÷™�H’…ê�h4e/Ö¦¦&Õà6X˜ÎdWyyyÈÎΆÙl†ÙlFkk«Ïg´Ûí¨­­E}}}Ð1>C9räˆÚ ˜`ë9R'ˆ0XoÞ‰ c…mÛ¶¡°°&“Iö˜õO«ÕŠêêjÕ1Už n½¶ïäåå¡¶¶6h¨ªÓé‚._PP ƒüÖÖÖ€¶õõõrÿ §>Ä0¡zNçåå¡´´Tö´Vîë#MÀ&öu«Õ*ÇjV¾þäÉ“ªc‹íODDD41�%""¢ˆ§ìM*ˆ™îƒá•Ëå’A“2lTÎjJ[[[О´ã'‡îõÞmkk“­ËåÂÙ³ga·ÛQZZ:ªò“““<«Õ*Ç 6Y“²·l¨Ïê¹ÎÎN¼óÎ;2.,,œ°^Òqqqظq£Ü&­­­òŸ/Å„TÊáDà(Êöùý©…ªjû£�——‡¤¤$ôööÂd2Œ¥+ö¹p'ºÃŒ´¿ù—'Þg¤ÏªÜ.���r˜�F·Û “É$?Gnnîˆ/ÑTùãÿˆ��üáÀ–-[|ÆÒŸl d‰ˆˆhFJNNö ÈFÈ)ÃRµ,T(øŽ[+‚&¥òòò°Ö#T/Ó‰$n{߸q£6@ôÒ5™L(((u\__�¦¦&tvv†=üA°ÞÈ£áß³ôäÉ“a÷-å8¯v»gÏž•½P•ï«ü\"dÏ{†"zØ*{g÷†@HJJw Š2€½°G»¯WTTàСCèíí•eŠrÅrþã3M¥;wî ¯¯_~ù%¦ô½ÈÑŒ¤ D[[[nI¸=5ÇB£Ñ„=öåDö´Z­p»Ý#~ž¸¸8lÛ¶ ÙÙÙ286›Ía׃ËåÂþýûeïJÿÏ-z;îÞ½[uÙ‰ÚþÅÅÅ8|ø°œìLm‚´p´¶¶B£ÑŒ¸-’““QVV†äädTWWÃår©î{:�nÄ@_Yg£UXX(ÇpUNz&Úp{ÇNĶHJJ ;4U†Öz½§OŸ†ÉdBkk+Ìf³|^ ©Q[[«:éQ¤c KDDD3R^^žÏŒó£ dE/Ǥ¤¤1…¢Ê@×?|Õét8räÈ”ÖÇÅ‹e}TTT„®*o÷WC9|ø°|}AArss‘��VÕ‰èñ¨3¶  õõõ0›Íhjjõ~PYY)÷…ººº°–ÉÍÍ•½‹;;;Þ³°°pRCÄäädèt:tvvÂl6Ãh4¢©©I¬á¾÷X{+·¡ÑhĶmÛÆüYü‡Õ=nEûªªª 9&/Q$šçÿ@ww7*++QWW‡?þñ�¬!"""šÉÉÉÈÊÊ9qU¸Îž=+ë`Þ§Ÿ~² q‹¾F£‘Á–�ÚÚÚFì}ØÚÚ:¡Ã(CåÑÔ…è•nà%&“¼ÁdYYòòò½`C({„† �M&ÊËËQ^^POÊá vîÜ)Ë|ýõ×G]§ÊÏ=šzóÿ<þCXŒT‡£ ÀÕˆIãÌf3\.— •CMÎ¥&)))¬uÞ½{7ÊËËqñâE$''ËÏ=Òr.—Kõ³ªÕ�^¯Ç¶mÛðË_þÏ?ÿü¸¶ ÑlÈ  ££_~ù%îܹÃ"""¢iSVV&ƒ¡ªªª°B®¦¦&Ù»Q£Ñ ¸¸Xõu.— MMMAŸ·‡çææÊÇ•½RC�!ÚÚÚŠòòrüøÇ?ÆÙ³g'¤.²³³e¸öÎ;ï„UÊu w¸eÐjX†`Ÿ_€‡{TŒ)jµZCö䌋‹“’‰¡ FCÙ›ôäÉ“aº"üTÖ[\\œü� ¾¾>d9UUUؽ{7ŠŠŠÆÊçææÊ}ßd2ɺmÏ\±þ"Ø ¶ÍŤpjË…?øÄ‰ؽ{7Ö­['_·oß>”””àðáÃA—ãÄ^DDD4—ÍcÑL•œœ,U»ÝŽýû÷û„eJ.— ÕÕÕ>!PiiiȰïäÉ“ªaSUU• ¯DOEÀ†‰Pô­·ÞRíÙçr¹äÐþË�—(KŒñ*&yRS[[+ÃK�Fã,+ùO` ì},ˆ³Z­xç�w‚n3\ Ž[[[åv §~òòòäú‹¡ F³ÈõÞ½{wЙ.— 'Nœ�õš••å*ë_ŒmëOŒ™ `\‘ÅÅÅÉÏ,ê:Ôv Fà ³•�‹@]¹]B}V±ssså¾#ÞÓn·ãâÅ‹ªï©ÜáŽÇLDDD)8†,Íh7nD\\œ I+++qöìYŸq'­VkÀ0¥¥¥#Ž7j·Û±{÷n"++ ½½½¨­­•!bqqq@O¾ƒb÷îÝp¹\(//‡Ñh”½W;;;Q[[+ƒÌ‘á±ÔEgg§ì¡yâÄ œ8qBÖ…F£Agg':;;e]h49«Rrr2ÚÚÚdH©ÑhðüóÏC¯×ËñKëëë%MÀÛ;TôŽãÓú·eeeøéO*ƒc1­ÛíFkk« <“’’¬KKKQRR·Û�×_=ì1mï°¢^ìv;ÊËË�N�N'?ƒ2$LJJ ˜D,//OŽiÛÚÚŠÝ»wÃh4"++ n·f³YÖ�N§ÃÎ�;ǵ½�F#êëëeýŽ%àÍÎΖ“„™Íf¹¿‹ýµ¾¾ÞgmJ¹œÕjÅŽ;PXX(ÃSågõ››‹¤¤$ôööâäÉ“øôÓOeéííEkk«\Ö?ô&"""š ÈÑŒg4‘œœŒÊÊJôööÂn·½e>77W5HU#'µaŠ‹‹U'3ÒëõøÅ/~�C‡¡··&“Iu]&köø²²2èt:TWWËñaƒõøÔét(--U­ ö÷Æz5›ÍÐëõ(--Eyy9Ün·êçaeuu5Ìf3ÚÚÚ|žONNFEE…¬£ÚÚÚ€Þ¼bÝ ãââP\\,‡¨ªª LC-[QQ�ÚÚZ9œ…Ëå’Ã&¨íC;wîT]·²²2ÄÅÅÉà]mßÉÊÊ’¯eˆ)ö×±Á°W+++ÃÚßÅø½ÕÕÕp¹\ªŸUì Ê},..±�äææÊá(ˆˆˆˆæ’(�ÇãQ>pýúuyÛRbb"Ö¬YÃZ"""¢£©©IöìÄøžyyy#NxtöìYÊÕÕÕÉÞz¢¢^¯GaaaX'‰ÛÓ•=Dsssƒ®‡2 5s½¾²³³ƒŽý*Eµ1>õz=rssG7VÛ­­­HNN–½}EùʱKoК��-ƒf«Õê3¶©Úgeˆ»âV|µ°:œÏ}ñâEDŽ:ô=aÕÆTuN˜¯¬;e½ëtº A|8ŸOmk…ÚgÂÙ·‚­óHû»Úrb_©×®Z wÿ$"""šL���èëëàíL‘‘‘1eïÍ@–ˆˆˆæÿ@–ˆˆˆˆˆæžé d9©Ñá²DDDDDDDDD4§<þøãˆŽŽ†Á`@ZZÚ”¾7Y"""""""""šS.\ˆE‹MéP‡, """""""""š"ì!KDDDsŠÑhäìîDDDDD4m¢<�GùÀÀÀº»»a±X044„… ²–ˆˆˆˆˆˆˆˆˆ(¢,Z´«W¯žò÷ è!‹ŒŒ ܺu ·oßV]èâÅ‹£z“�7Žø–É2Y&Ëd™,“e²L–É2Y&Ëd™,“e²L–É2Y&Ëœ eN¦ C¤¥¥añâÅò&�<òÈ„¯8Ëd™,“e²L–É2Y&Ëd™,“e²L–É2Y&Ëd™,“e޵ÌxÓ!h ›’’ Ì2Y&Ëd™,“e²L–É2Y&Ëd™,“e²L–É2Y&Ëd™(` Y"Ÿîîn;v Ï<ó òòòX!DDDDDDDD$ÝÏ* š8ÝÝÝ8r䆆†púôi`(KDDDDDDDDÒ>999 e#‡,&$Œ¼¡EžÉc_@DDDDDD4W„Æ@?š››ñÍ7ß°â"£v?ßYûà¨^?xë|~µŸ¡&+ŒD([XXˆÓ§O{ÊE�Ñ„±B?êêêpýúu¼øâ‹"3°‡¬Ÿ¸ÿ=ª12ÓŽT“Æ ì)KDDDDDD™úúúðᇎ*ŒÐÕÕʼn¾"Y""Œu¹\hnnFbb"–,Yâóo<üËKLLDss3†††ÊEˆÏ?ÿk×®u .Ä÷¿ÿ}ôöö2”�0ìÞI¤âW¿ú†††�þð‡ª¯yï½÷ÐÑÑ1ê²—,Y‚ 6„|ÍéÓ§9tÑ,ÖÖÖ†žžÌŸ?ÌeˆPößÿýßñÚk¯aïÞ½¾ 0�%Rñâ‹/†ü婪ª ñññãz�-[¶ 55Uõ9\‰ˆˆˆˆˆˆf§ëׯchh===Rž2”ýÓŸþ„'žx‚•<Ë1�%R‘––6éï‘ššŠŒŒ V6Q„xýð‡?œÐÎV .ÄøC|õÕWøæ›opÿýŒôf3Ž!KDDDDDDDD4N"Œ}â‰'&åÎ×ùó磿¿ÍÍÍøæ›oXá³Y""""""""¢qP†±c™Àk4D(ÛÕÕʼn¾f)öo&""""""""£K—.ù„±>Aéüùó±pá {¿›7oâæÍ›øÍo~ƒ¤¤$Nô5 1�%""""""""£œœÌŸ?�<òàøššš|^³qãÆ1—/Ê.^¼(ÿîééÁ¥K—°uëVnˆY„�,‘Š·ß~ÝÝÝ“þÁ~ÁJLLDII 7Ñ —””„ýèGòÿ|ðA¬^½`6›a6›Ç\vcc#þã?þ{÷î•�•––ú¼f*&&§‰Å@–HÅõë×a³Ù044„›7oª¾æóÏ?SÙCCCøüóÏU—���G||<–,YÂ�@DDDDDD4 %&&"11Ð×ׇÎÎÎ -?##ƒ•<Ë1�%RQVV†#GŽà‹/¾@GG:::&¬ì¾¾>\¾|Yõ€ý×ý×X²d öíÛÇ�@DDDDDD4Ëåååá¾ûîí[·X$ÍcŠ��ž}û°dÉ<õÔSX¶lÙ¤¾ŸcÓÒÒ°oß>ÆMDDDDDDD¡È1U¡,ÃX"""""""¢¹ƒ�,Q“Ê2Œ%"""""""š[È�`²BY†±DDDDDDD‘­©© W®\óòkÖ¬ÁÞ½{Y‘†�,Q&:”eKDDDDDDùúúúðå—_²"ÈY¢0MT(Ë0–ˆˆˆˆˆˆˆhîb K4 ã eÆÍm d‰Fi¬¡,ÃX"""""""�7n ©©‰aÈ�ÁhCY†±DDDDDDD4Z d#Y¢1 7”eKDDDDDD47åååaݺu¬òÁ@–hF eÆÍ]‰‰‰X²d +‚|0�%§`¡,ÃX""""""""òÇ@–hø‡²O>ù$ÃX"""""""" À@–h‚(CÙ'Ÿ|’a,Ñ××ׇ/¿ürÌË/]ºyyy¬Ès?« ò Àf³…ýúxiii,sŒenÚ´ }ôžxâ Øl6Öç4”Ini¨A{ÃÀæƒÕòqG—�g^¬yîgЦ¸“Òœ9FÔ*–wU„`*ÛÒdµ§Ã°.ÁÚ¶r½ϼŠA·S>–S´ƒngÈ妋ÚgTn“ÌüM0äofƒ˜°õá¾;·gÔz9º,h©; ë5“Ï~ ÚtƒwY³ 1š‰‹w´çÒé&êg2ê‚f{ ÷uqº©†èWLÙ~;Ñû,Ï!DDDcÇ@–f%Ç�vô ÷Þ‰ÍÒŒ–+o"Õ�ƒgÊŽMèŨX�ßãNG�ì]äs1<Ð/è熤9EÙ&LÇÊà X”mi¢ÛŒø�$Å�ƒÕE{ÂjÛ`½j‚éXyÀãÚ¥™ø¸îTÐå¦Ëûç�¢¥î^øõGA·I {AÎYƒn'LÇËa½j ÙV]´\9…gÊ~5á?ŒŒö\:ÝÇ1Ó±rwU°÷0�ëU\Ÿ&hS±æ¹ŸA¿Ê8)ëãtØ`:V®z®›¨ó:Ï!DDD£Ç@–f½œ—6K³¼(¶Yšq¹r׌êeC4WÙ,ÍøøÊ›X±~Ç´­Ã[ûŸÓIKC�ü[ÊÄÄ&àãºS3ªžß?Íç�r‡#UŽ. .W¾§Ãˆ×¦`uÑ$hS‘ M�ÓуA·SþÐàtØPsx6¿|vNÞáa³4ûôô' %^›¢ÚkT uãè²`h N‡ —+wÁ¸«bRz™¾ùÒSc:×Ñäb K³ÞH¿ö;6\zmnÞh‡ÍÒ KCÍ´ÝV•jÈÁÿùÿþ™�@sÍЯ2ÎȱW‹Büp#zê¦rŽ%E³ì�æ6Óñý2ŒÍÌß„µ»ŽøÆ;ˆ(˜¾¾>tttà3·YD�=diNPö65¥ÍÒì3^]‚6ú•6îì Û ÇðDÚ¥™>åŠ÷�;6X¯ÕËÉCÂ]ÿå´Ã“šÞÛSú#jFøÁ�~ƒß_¿íïÎGYYwöY@›n@Ll<:¯ÕOØÐŽ. líøL¶£_Y ºŸ‹v ô+Æ©Ç ñš˜Øxyk¶xÍÐð²CŠ1¡E›R[N�õZ½ ÃÄûŽÔ+jÐí„­ýŸå‚-+Ž5ýŽž€cŒ²ý«=6Ò1%F“€ÔÌïý|Ê㜲>­×ê}ö�)›È†ÇU-ÃÃkDÇÆ#ÿ¹ð& Ëß~�ÃÛÑfiz·‰ÿöööúûÞ¤�wüÛÿhßOÙƒíß6K³Ü¯øü­Öv�l–ddЯ,»Ý º�°4^À Û‰m* ù›F<æ…j—Så[W/¾îøÎuFãQÝRlݺ•�m+Öï€õZ=z†¯A]– ÛÑ»‹ãijæ÷®Ã9×�tŽí~å¾µÏOÔ{�öåÿ¾Ökõ²�†ºvé;C°íÀöFDDÁ0�¥9ÇöhÀÛk¶ñÌ«>_œ„FM–¯+™�‰7Úåøs›Vû\+·^«GË•7U×ÅøBEЉμªº\‚6Ï”ý g^E�¥9E{&|b‡iûòw»}·�>îÚ³ÊÚ]Gð//åch \CˆÉJÔ&þi<ý ô«Œ0¾PáóI´ÁÒP#{û‰[øÅkR 9rÿ±#]ù˜hSjË…s¬in§j» º�h<ójЉ͊6.¾€*�5þÇeûW{,Ü÷M5äÀ¸«"`»)ßû…_tÂ(ÿuæ1bêX¯šä¹Ð�¿9ì!A›ŠÍ«~PT¶‰Æ3¯�ˆkùúX½ù¥ ,Bµñ~ùŠÞ‡þ, 5h<óªêuAª!kžû™Ü?ýÛSãéWäßÊ¡?Bµ›æóGÊUk7;ÞxoíÖg½]äo?0l> ЉÄÂ9שëE;½uÅÝ ¡&ÆãKGÇÆ#Õ�ƒmª\7q®PÛ'ýÛ‹XÖÑe�ÍÒ¬xïÀq«•˦(îQž£ÔÚ‚Z;U¾oçµzÕk±-.Wî zèGóù£t;}†K#"¢¹i€4�Ñ©©HKK xž�l„;wî>é¼�ÁÛC˜¿ìiÌ‹Kš“õÐxæUÕ‹^Ñ»F\„nØ{ÌçbÕÛãí´7\€¥¡‹—>6é³Â;º,Э,ÀÚ]G|.?¾ò¦üïŸ?ê3ÞŸ²×Cfþ&ä?wÀ·G b2¢™rŒÐ¯2B·²`ÌC˜Žï—û´ÿìÔ«‹öÈTÌ`-¾HŠ/Iÿü“Gx{†Ó›Eôx=¨=–fhÓ aOâåtØdû]¼4E«}Ú¨c�çL˜di¨‘_ƒõb}÷Ø>tßri½j‚~•Q®Ûûç�Ê1 G3áØåÊ]ò}×<÷3Ÿí²ºh¬WMx÷Ø>o˜~|¿jà%Žgjëýî±}ho¸0|«©iÚ&Zœ•••ès�A÷7ˆyâo¦eü‡Ë/pˆ@fÃÞã>åæo? ÏCŽ. Þ?tÜa…hÿѱñX»ëˆÏ¹}uÑyÎï§Üm–fƪ�3]ÙCµáô+ذ÷8ŠVûcùÛÔ]ÙW‚¶ñ¾—^{A¶ó`?FØ,Ír½ÀzÍÛ®•ǃ¿ýùo]qÌt;ai¨™ôë¶·‰µxi&nú 5À燼¿{£!`ŸQî—-u§åñ4œsÝÇÃmRí<*Úö¥Ê]2ìw:lA{ºª]G‹v8èv¢áÌ+×®¡Ú‹ÿùÍfiö ]Ųϔ hÿ¡Ú‚rülµó“h§þ×âó;�>çÄùCÅÞƒêNauÑþ(BD4Ç%ø¯QQHÚ²<öXÀó¼·!Âuww£»ËŠoowÃóõ݈üŒb 'µ×�›/=%{Ä+zˆ‹2a­ÊíT¢_üp�²æš7&ýóÄkS°aïñ€‹¸ëwÈõpú]¬7ŸC^ ú¹â¢\7Eã5�£±v×9ÉWsͪƨñ*Õ=Cþfä Ñòëmª}|åÞ"öÞö©M7È/ÏN‡M~6q«¼6E54“ÂÈ IÑóq¬DÏ$X¾®D5Ôѯ2Ê÷utY‚ÞnšbÈQ]oå˜¥Ž®ö9Õæ:::Ð×ó)¾½Ý=mëj,õ±°4ÔÈý{í®#ª!¯ò<ÔRw*ì¶®FƬ.Ú£ÚËwÅúÈwÕÿIªÄkSÂXÑ—¯+t*Ʀ Åé°¡½áoï`µv“jÈñiçÁÚ�ò\£I�Ç7å¹_­w­!3t+ doxší-\b?T¶ G—Eþþöª_ª!G¶-ÿ1ÆG"öÙCŽêy4F“€ÃmAíúSðþs,à:ZÙÅ�pBKÝiÙÕÚKŒ&Áçqå9|¤^þ†üÍH1äx{¶+Žwƒn§ü1F·²@õü”ª8où_;ˆ1´õ«ŒÛÂû�¡‹—f¿/;AQhì!K³žÿ¸nÁDÇÆcCÙ1ŸÇÄ…•n„IVí‘¿´+¡Ÿ ú•ÁoMЦ¢ßÑãÓ{Âé°É ÜëKB~†NÅ$+D3å èX†.P~AR~Y eÖ•È¢Ö«¦Im»¡(�5Á†GЯ4bð9'´éùš {��ø{¢ÇŠTNÆ꘲bý¼þ(†úa½j Šo ºÝ£cã14Ð?!!2M/±ÏÄkSB� <Y¯šÆÜƒÓzÍä³cÈߌö† òÜ­_eÄ Ûy/TYi ÚƒmÅúhÓ3ßL1Nr¨÷†üÍxx$ëµú ?x¨¶›á¯oÏFµc߆½Ç¹CFmº;ÞxNG�‚ ØëÆr�·ã�÷`³4‡Ê&œ^ž¡Æ�_]´G¿Êvÿ·?ÿ-l–fŸýÚ_°s¶rÿð‚ê8îa;V¬ß!ïjQ^;ˆ^Ì-u§T',Ó¦P\q‰;î8utt »»}}}èS±xñb,[¶ ©©©ˆ��åÆ ¢IÅ@–"žè»b]IÀ-‰¢×ËH!�òùÉdC]øj—f¢gø–1åú(ŸŸªÐ†h¢Œeè§b\ÔPm&F“ ¿@Mgð'Úl¨vèß(TÛõþÓ#Çè›H¶QŒ­«M7 ÇÒŒžöTŸµ¼X–f¿ž3·Û—m–æ1²7oüI–ªç»²gœãF;ô«Œ>Ç�Pë› M…!?uÔí&œómª!í ‚îÿÁÖK¿Ê(i<ýŠœ0I·ÒˆTÃ÷Æ41"Í| ÚTÕmëè²ÀyósØ,ÍèT™81\jûÛ Û Çð8¬á§GjKÁÚ}¨÷vÜhúÞ⇠1f«7 5�%¼-¨�Ïj½�[@o\1fï[ûŸ…6Ý€Cô+ ¦íßHÒ×ׇS§N¡££cÚ×%11Û·oGFF7 M²4ë…êMl&hÿ/j#}yš)_rÔ>ËH·0*‰`Šh¦Y»ëþå¥|ï—«š7Bö¶0ªýX´›þ ·ZN¶P“ †Ëz­�WMÞ/©£¼%uD»ŽŽ�‡~¥ú—¾©’D„$b|Ü`_ÊźŠ[7åg ÒØÑeAsÍa­C¸_ ù›e`ÕxæUÕ/àÊÏ?­a7�ãogŽ. jo ¶Ø,Í>Ïëüf'Üã9Þž¡ãßTÙþMÇÊU×[Ùþãµ)>áÌŠu%ò5_ySuÿV.+>«òÿ÷Ô¦d�×–ºSÁÛÍñýŠãBÉè¶Ùð1¬'DPÜ¢¿¦a(‘yñIˆ^ñlÝñSlݺ• -Ø—q·Ökõ¨9T,,ˆ×¦ôRUnÃ`ÇpÓ±ò�ÇwÑVü_ã?4‚ëU“Ï>l2>›¥ÙçPe¹bùCŽl÷¾Ãm?¿µùqC´EG—%h[P®�¨Ge;m>TuY1ä‡ÿñM»4SÖe°óî Û)ïÜ™Š;ê"¡½)ŠGϬ¹.I¼•ˆ^¼4u>ƒ7xü=€óþ§Çƒÿà—¸´^Åä¬Ä�¦xÆõoBÏŠ·À¥á:¨Â½ú’ǃßÃðÚ¹ûÌöáíó‹ýWœ;w.àyY@sþ ifþ&´7\€¥¡ŽíX¾® Ú ô£óªÉçB9ÿ¹3òsò7Ãfi–ŸÃfi†6=1± rVöèØø°Æ„œmî{ða,NY„Œ ŽëI”C¨QNætØðÖþg±|]‰ü‚i³4£¥î” †6ì=ðcŠ¸Íº¥îúoz{ë_¨˜”]R 9r|\KC œ–¯ß�˜Øx8=>NþöˆÑ$ F“às[y‚6©Ã�8n´Ãz­-Ãa’hßj_¸Å×ÍšCÛ [eDLlüˆ·¦n(;†·ö?+ÇßËÌß,{9n´£ùüQY·ùÛÌè! xŒÝÎÎ*ÆÍáóÄ[ûŸ…6Ý ƒÚÁ�~9|…²Ý({±{ÃŽT¬yîg2Àkÿ³X¾¾DþûêÚ]ãZïMò·�A”ØGS 9ˆ‰�hÿþûûŠõ;`i¸€›7ÚÑxæUܼñ'Ù6œŽ4Ÿ?*Û’ò³*� Í5oàæ�?apÀ‰gÊŽ ¿¶çcÐí®ƒ>í¦åÊ)Yîšç~6êv³b} , 5èÇåÊ]>írp -WÞ”“÷ZfêÛeÔý p߃#í;K�–;§Û×Íí¨9Tìó˜ÓÑ£žFÇÆcCÙ±€ó�~¥Qþ ~¹òEïð™ßÃà€6Ëh¾æS^ãùßj¯M7 gø1 ÞÀ+§è%0 ô{ƨ(¤fz'ë“„Yjº~ŒŽ�‡éX9z, 3Óˆí^ÜÝÕ?|þ‹Ñ$@¿²ÚtÃð°õho¨Á Û)ß_ùC�[Pžÿïc¢ÞüÛ‚h§Cý¨9T Cþæ í?3“Ï�+Öï@óù£ptYðæKOÉá…ĶUž§¢·z$´·O>ùD5�1߬ñþ}ýúudddðâaˆ°± €ÝãÁ Ú £˜Tˆf<|o¹ei÷^“‘6b ›´Ï;pèvxÿþjèî�{äß_ EÁæ¿þÔêg!¼Ã,…wL[�0ó0�%~!Ýu1± 21 ßš¤”bÈQ½Pž©ŸÃé°ù\ð{¿@Wà­ýÏrƒÓ¬  \ƒ^$¯2¸« §_Á Û‰æóGѬö%wïqÕÛ1 ù›d/8Ñ ÞÿKÝDÚ°÷¸Ó.Ø­Ð9E{|zò)¿8ª}¾xm 6”Ãûç�¢óZ}Àm«Ú¥™2Ôu:lh¹ò&RÂø|Út6¬Æ¥×^ðŽoxåMùÅZY·kw™Ñ½ciävVt°ïŸ?*{¢)o©÷·|] VíQ=ŠðAôXo<ýJÀkĹh<½cïµ_o;í?Ø>š¿ý€ê­ËE«q©rz†{öù÷îËú–¯+‘a¯ÿ2Útƒ,·ßÑ3êu‰˜ÄL„¾jå‹)ûîàÓLŒ]Š¸Ã ÿ¹ªíJÙqÀÑeÁå×^Pm—úUFþz'êSŒ1¼²@) öÙÌüMH5¤Êóì Û²Í¾{¬\þp£fuÑ|\w*h[*:XðÁ†²c¾ç7¿¡µâµ)x¦ì>¾ò¦÷ü¦£Ú¿-¨�Åõ»ÿ2þíTm�Eùÿø´ºhœÚ.Àé°©~gvì "/q ÷ 7<ôú‰#¯Ä©‹=HKbDÉ�Õ7`�šÑu»Àw}—?z½E€+ÂÛ>'pó¶Ÿô„þœw†ÿuˆë¾á±kÓá h9ñØôc á¶nÝŠOznábKæÅ'EÌç2äošÐ �üí°b} >¾rÊçÖà„áÛƒ½—XÿÛS 9€J´m r†÷_F<ês+×ÿsxÇvì‘!\—' IDAT� š Lj`û~0úUF¬yîgòIµå ù›¡_iô-â7ÖœaxÆè`?¤¬X¿ ÚTX¯™àtô &6^¾G°¶=žçï'†üͰ^5ùk´K3±b}`�6mºÅ—`q•Ç’TCŽ uV¬ß!C.å„B"pSÖ�ò긓jÈÁ᫥ {Ý+oUÕψ­V·¡Žs£©«HUZZŠæO�¸Úåšë#z›®X_"Ç0îm-öGï¾¶iÄ—+Öï€~•1`�QÌ?çÒpÚ¿~eô«ŒA×Y´ q«µÏ˜–!–Íß~ÚôLX¯š08Ð�mŠO]iÓ ²Ýˆ×Ld»Q–ïß.ÅùžAÐô¶·p®Qcbã¡M7„µ­Öî:‚TCŽÜŸÄþæÞûOLl‚ê¹ÎÒPƒÁ�~Ÿáô«ŒØñÆ{xÿüQ8=òüáŽY]´GuèñžúUFò7ããºSÞýr¸œPû¼8¿‰ko±LÌpH­üÑBíüæÓÖ†Û¿rýC]¿kÓ (þù%Ÿvî²â<.ŽÊu õy‰æ*1æk—ǃϢ¢pGùdˆðõÑbiIQH\$&(C̨9U‡Ê×?¼a­Íáýûúgô9�¿ôÖ‘rè!ÉãÁÒ¨(Ù‹vwÙ)åñxн{÷θÏ÷‰}¿|ï îíÌi56K³ì=±ù`uD}a[kX„õY¼™Ç¢ÙqŒ¸ÒÖ‡w-·¹a(býô©%xt†ÜBÍöFlo³ã{øÿõÌü·~o4}-‡,(--�CÜPÔÙLõ€ëðö̼ÆëSµÀ²TÒ’£�‘æ _iüº{�›÷¿×?󨆴þ’<<…e˜Û“ÇMX{P=ü·Z^ȲD@ E°|]IÐÛý{×�W€ë:õ1`Mñ ãaïpiZoйÖëu*¤%yÿyE¡Ï \ïºítØÔǨí�Š‚ € Þ@v€lpüÙÉÂ@–(ȉ/À�¿) ·¬¥¡FŽÍ•bÈ™ÑðÑÌvÀZÅD\*Aì1ÞÞ¯G!U+n½gø:€¼ïøî½á”=hýǤ½àêð?Ñs6 Ö`"1�%ŠùÛ F1³sª!‹Ó r;áPLÂ0³[ÑÔijj‚ÙlÆ]x{,fO㺴hVNÈåÄ>ïÁòG€Œ‡£†Çé¼�ÁÛC˜¿ìiÌ‹Kb¥�DÓ¤²²}î¯1èþ1Oü +„ˆí�ˆhƸ Àäñ M1Fì1À†<àé'Ä’Wì±Oo¿˜ÿ·÷ñÁ¨(TxÓ;ñÜL‘ à¿FE!iË<ðØcÏ3��pÝÝÝèî²ðÞÖBDÄcÑôrÛÑLrÀY�½Š0öq½%?ŠâФ*v�·ÇìÓO§þ`sx¿ àK�k£â‡ÂÁfˆˆˆˆˆˆˆˆæ0“_»å¿/2Œ¥‘¥%e[¼¼ ×¢¢Ðʪ ‰�,ÑÕø S°}¸×#Q¸DoYe({ÞÉáH‡, ˆ`:V§ÃX¾®úUÆ9ñ¹�´©ÜˆxŒà1‚ˆˆˆˆFí.€«0Ènù/@ÞwY/46ÞI¿<ø£Õ»?™lcµ¨b K³žÓaƒ¥¡Fñÿ=¶864žywÝN¬æN@Äc�DDDD4jÀ;<ïáä]4n[…?z§)ÁgîXÈj À! hÖûøÊ)@¼6€7ˆ°Yš#ú3¿{¬Ö«&n|"#xŒ """¢1»áñÈ¿·þ€a,�_b‚ïÐd•¨b K³^{ãÀŠu%2pQö†#"#xŒ """¢Ù*//¥¥¥(ðø—Ý«ø;-‰uMC¹/Ýau¨b K³šõª ƒn' Õ�ýJïmÈí äãDÄc�DDDD4[%&&"##K1ñ·~*&óJL`]ÓĈQ �ú©¢ö\rÞ!þl³¡»»;àyŽ!K³šèå¯M�6Ý€š´Ô�’Ï­X¿#¬r]X¯ÕËÿׯ,€6Ý€A·ŽííÒLÄhÂZ>A› ýÊ‚�¯èG‚6EN¸ci¸ 'ŠÑ$@¿² `2§Ã§£Cý€¡�~yëµÿú º�°µG—E>¦M7 5ó{A×k6Š^ñ¬J�CÎwxõ@н{÷ú<Ï@–f-§Ã†Îá€CôzKЦbñÒLܼю–ºÓ#†-N‡ ¦cåãI6Ÿ? ý*#2×lÂåÊ]€Í«‘jÈñy�£ËÓñý>�†Ð¨IÀòu%X]´'ๆ3¯¢ÇÒŒœ¢=Ð.Í„éxy@o½ÆÓ¯`ùúÈîgò1KÃ4Ÿ?êóþ5‡ŠÖÏÒPƒÆ3¯ªöŒ ±^³Ñ}>ŒÅ)‹�‘‘ÈFA7èvâü¡bܾÙß Û)ƒ–èØxlØ{ܧg\þöx÷Ø>´7\€¥¡º•ªëa½jB¼6kwñYÞÑeÁùCÅèGKÝ)Šò7Ã�¿ç£ÇÒ mºE«}Êl®y€7¼ñnuÑù¹”å�Œ¨û#çg.›¥YŽ¥¨ìùxo·Õ­,t^«—¯SrtYdï¹5ÏýÌ'he¬–3²ûû¸î” jüƒaí®#X¼4dø£Æ?h¼ã8Šut;CöÂSûlT×)F“€ÕE{�bÈAJæ÷Të†xŒà1bbŽ¢\#H)##‹St¸ïÁ‡YDloDD3ŽÙ…SuV�ÚÀ] òmlúá` K³’˜¨¸76¤’2<±4\x^9¹ŽÐ"x'ÍQï9'Æ¥·0cÈß@L´lDÇÆ]>X¯½‘Dß–Ü®˜È¿Ü¢ƒÕذ÷xÀ„@D�3ˆû‹:œ, 5ª½ÜB-Á(g%·Yša òþþ7ÚÇÜ£m4Ví�£Ë")G—Ž.‹wòŸá}™ù›BöÚ#â1‚Ç#ˆˆˆˆf†¦¦&˜ÍfÜ�=üo2lýAÒ’�ÓÿËûÿ_ F¡êm`Y*P²HLà¶ {Zþ ¼ýo÷‚Xx(Þƒ £00Èú Yš}�~x¢ øm¾þÚ.½íx´”ï¯M û–Þ„ cMN† {�ÃÑe�¥á‚œÍ€œE]„OÁfv'â1‚Ç#ˆˆˆˆf†¾¾>ttt&{Tì¼ïiZàWµü¥ßÛ�¡Ãü÷ÿÈ5x°á¯¢ÌÎõï[~÷¡w¿Pz\ïAÉ�¢»ìY²4«86y+pfþ¦”†Ó¯àæ�v9Á�024Ð?êuPÞBlÈßI>­ï�¥µ$Yò |’|ÿ®ËB’eéÕZ¯½n=ëy•Ó—½•í‹c¸P€kNÀóÏ?Ï�„sçΜ#ˆFIws:J È) DJr"²³³9(DÜßÈ ib€Ÿÿ£}®t:z›½Ú1婢¼«RÅlÖ<`Î4ŽÕxÑj nú\׊X@ªŠÍ^&`Î4.æ5 dÉ«|ùÞaåòÜ›úí¯8÷ñMøè×/)ßûÈÆ ­.^s5õWak±ôzZ²¼ÊzO�!áhomR‚Ÿ^ƒ€ l­–_§¾âªËب¢ã1÷ñïcîãßG}ÅUüþ'«¤×÷×�½>lé6a2&îÄ9‚sç¢Q%vXÑm6Âh‚'p© "îoäít™@æ,)„Ëû›ãú’jéK­’BÙ¬ylgà«.\.qù†ÔÂÂÙÄ ë@Ö7¤ªXÎÜäU®�“ß �ŽÐb7<ä´pϹw�®_®\v^�Ý™óÂ@n�kïóXö×�û<ú“·~†ß=·ûÖMë7˜ W?ù#ö­›†ßÿdU¯AQôÔ™ˆcoHâÁ9‚sõC­6=ìyXô ëm&‹TA»ó ðïo‰0|á^=IÞ§¤Zê »ó ðë?I•ÑÎaìÄ `å"`Ïf)´gëY0�(â�¸8h4·ÛÈú¸œœäüŸý°}q ÝÍu^ýZn\r,Ô3wŦ}OP¨J FäÞˆ=‡ü?¾î¸ØZ,8½÷½)ΧAŸÞûC—E|dõW•ÕÛãff Ëjé=O§~à!Ç‚@ù|Ýã÷ÔW\E�}5õ‘ìYIœ#8GpŽ `ïÞ½¸pâ l_ã`q#"òÎÁìÊEÒiêΪë%ÄÛyP ôŒu7oÐj•*a¿/bÛ~{sà1\��6>¼¶•Aì@ÄØ xníZ�meزÀÇ�F+n�NkñfÎ�È`V"ŸùÈw”Ðãòûo*AÉ£?øO¼ý’¶ >úõK¸üþ›ˆ›™�¦újT_͇­Å¢œvÜSüÌ ÌY± —ß?ŒúŠ«øÝ�–bΊMʪäÕWóqùýäS—yz×�ŽEtâ åtêßÿd¢g`Ί�ˆž:Oþùÿý+éy=·OþH Uz>¯±ÚÛ’8GøÂá²/rŽ ;yA"âþFDä�Ô*)ŒÓe (6J­ œÛŽÊYÃÒÿSãm�ªâ£ä�6“E ËKŒ@±QDu½\ýêÞvr¸ˆ9Ó€åq1·¡Æ@–¼‚¥¾Z958*qÆ ú-ÆÏÌ@xtšêkP_qUé�¨ŠŽÇ“ÿóm|òÖn%¸p>exΊM U!ÿ¿åñqÙø/Êí¶ òÿûWÈïqŸÀ�p<ù?ßòÊ·æK Mäç-Ÿ¢½ðÉÁR_�kŸ¼ K}5>úõ‹nßÝŽÿ=â}+‰ÆÓ!‡™œ#ˆˆˆˆÈi5Ò×Ú¥ÀåRÀwù+Ç"`2¹ç¬ò÷w´­F@ªFZ@ŒAßð2ÖÕõ@q•ˆ’j¡Gåkï!¬6� u '²äl-dØÃ�ø»èmøè^Aµý\›S5[ôÔ™xò¾�úŠ«¸as‚BÂñÀüoA�‹NAKtâ ·Ç]øä�0ó‘'põ“w•Ç—#~ff>ò�‹Í|ä ÄÏÌèót`Utœòš{Þ/~f¾û§ðåû‡a©¯q—Gð f>òܸôê+¯ øyqŽàÁ9‚ˆˆˆˆ#$È|Púì A}%€·›Ü½êzÕõŽ Zµ H�¡‰ ‰aí½0ÖmíR8n¬“*`ý_{W㣥ÚæL41 aGYò ÑSgÞSYüÌ —0¢çÊæyüÞ Utü Oëuî/Ù›þ7zêL<úƒWüš‰8GpŽàADDDDÃMöA9=^®Î¬®w¿¿É"-•×c�ÛÔx)˜ÕÄH�aH°tÝx×jªS£4vÅU@«Í¹õ€3ÏájJœm‚½J9šøh` KãR}å5üñÿûà»ÿqª× ÅyÁ"âÁ9‚ˆˆˆˆhàÔ*éK>õ½Õ ëíýK«D”Öô^�)·9¸ü•çÇÔÄHA¢Z¨#\oófr•«rÙ4˜E˜,ŒõίÎzljAR²6A@j¼4n}ÝŸFY—œOïÍÿãëøÖ?ý§[uÛG¿~IYA}æ#OpЈ8GpŽ """"º!ÁŽÞ³ºL)4ÖI᫱N «·>´ÎLé˹/­'Îá¬Z%"*Rp\Ñ÷÷ÞM%®\¹Ú9`•/·Ú×Uî=h•õßv�ú¿Êaµ&V€V#�ر†�,�KªèxÌY± —ß?Œ—>BõÕ|ÄÏÌ@Tâ 4T^SVP€�<1 Ó‡‰ˆsÑPËÌÌDjj*j_}‘>øú41rÕ& ‡r%­É"�š_\%¢­Ýs˃ÞÈÁ­óãŽ]ƒ{~)q¢½�ƒÔwwb�r3xõ diÜzdã¿ (T…/ßûl-ܸôn\úH¹=0$sÿþ {?ç""""¢¡¢V«¡V«1žÚ|Ê•´2¹š°W–Ú¤¶€º6˜Eå²§…ļI|401Pz=R[!ÁÒõŽ~¯ ^Ç:+€:–êjD††B£Ñ¸ÜÎ@–Ƶ…OþsWlBõÕ|—•ƹØ�÷œ»ó§†!#‰+ÂçâA4šüÂc8wÖÌQ#%n„ˆûѰ�«iµ.ù–{@)·P]ïÔ NT.÷¦¯Þ¶½‘Ûô÷ÜC‚åòÄ ûõn k1põvµÞ€wÞAJJ vìØár;Y÷‚BUx`þ·ðÀüoq0¼�ÿ¤DÅEB«Us0ˆsqŽ EB@0ü'%@“t?4±!"îoD£ÊÑ¡ÿðvˆöL: ˜‡€ˆˆˆˆˆˆˆˆˆhd°BÖÇegg£´æN\6Á/<†BDœ#ˆFÑöíÛ‘_nÁ¥Šf÷7"""§Èú8�Fk þ•¡ "âA4Ê´Z-n´›àßhæ`q#""¢qŠ- ˆˆˆˆˆˆˆˆˆˆFY"""""""""¢–DDDDDDDDcTnn.òòò`�fÿ""ïÆ@–ˆˆˆˆˆˆˆhŒ2™L())$p8ˆ|Y"òj¶/ŽáBq®}8Ï?ÿ<„ˆ8G�’îæ:t”�Sˆ”äDdggsPˆ¸¿‘ d‰È»ÿ5a2&qŽ Ub‡Ýf#Œf x—ª âþFDD½a KDDDDDDDDD4D‚hD�ññÐh4n·3�õq999(-«„ÍÜŽ ©Yð ‹á ç¢Q²wï^˜Z:`kéDÐ7Ös@ˆ¸¿‘аA³v-&NŸîv;Yg4a¬¸@:­…ˆˆsÑè‘ä "îoDDD4~±Ù Ña KDDDDDDDDD4BÈ�²DDDDDDDDDD#„�,Ñ à�M™™™HMMEí«¯"’ÃAäÈ�QjµjµÁ "¯aPÀR]�ÈÐPh4—ÛÈÒ°è69ã˜!X5"?+pî:ÌŸ†Œ$žsqŽàá�Øi…Ø\Ï o<ïsaцÿ0Þ/<�s×aÍ5Râ&qà‰¸¿�[µÞ€wÞAJJ vìØár;Yºçƒ¼î†¯Ðu» ] ¥@§�ƒB.ü£Sà™ÿè”a `ü'% *.Z­šƒÍ9‚8GŒû9B´ZÐm®BW})ºêK¹�‘«€ øO’ö7¿¨iCÒ ÁðŸ”MÒýÐĆp¼‰†÷7""/ÿ³ŒC@w{À×Qv]·Š8Ô'9è(5À/Rƒ€¤oÂR†sçˆ!ÞçÚ¯þ_VŸSß:m.a½t &¤d�XÅ:IÈú¸ììl”ÖÜÁ‰Ë&ø…Ç Íßò¹è¨ºÔg¥Û“9öãÙí6àN›ûõÝf#Ú¿ü9GpŽà1nçˆíÛ·#¿Ü‚KÍCòxb§%gúüð#8ˆãt;®ÕXk§ûõr8ëß,LH]6"m ¼y#"¢±íßöøñÓ³© òxŸÃï£òf3~ºuŒFY§Ñh` Tÿ2tHúl_sëC7E% Cã‡ijq*�ƒNŠÛmÀW ݸr«Eµ¢ëà�*}c=üÂb8Pœ#ˆsĸ™#´Z-n´›àßh¾çÇên®Cû•­�.×ÏŠ�å‡Ù÷ùaòDngäºÏÞêFiC�}îVº›k8óÿñ©}n(÷7"]¿óÎݲÇ[÷¹G’üðH’j,"Þ-êÄ�ÛÒmbs=l_Càì5lBDD^çð‰D„bj\8ö½YÔk K4V0�¥~ÉUoÎ}kôÇ#I Yhp�ß›ã�¥É~x=·S9u²ãÚ{&Á?*…ƒÄ9‚8GpŽùYpð½9˜}+ÐiàâTžËœ€ÏŒÝx÷o]Ò>×iC{á žÁBDcVDh Ò’¢<ÞVYׄªº&R¬'WÊÐØÒÎ�ô1f‹ 2T`é‚û±4c þÇž<œÍ¿‰¥S884f1�¥~9-ÁÀæù¬x£{;üiV^ÏëÂM‹tºdûÕ÷xðÇ9‚ˆsÄ(€ØMšlžÀv t×hü!8>é´¡ýÊ -Øès=e‰Èû¥%EõÚj`÷±Ï°ûØ%èõ>CÝú`¤äææ"//Viö/r8ùqÌ–v¬^>KH�ìko*�=›ož,AE�êG†aõò©Ø¸&Õå~ÿ¶ÿs<2ÿ~˜›Ú±ï­BL� ÇO·ÌCEM>¹ô56®NÅ'—¾ÆáÅ€9Ó£ðÓ­ß@¤*ož(Q®_º`ŠÇ^·ož(‘^O“t|µ:Kz½õÄ%ïÅ#fêSûÕ÷”~�ÁÀs™ ZèžMœ à¹Eþ˜"�6´_ý¿;­ÎDœ#úÐó�—0Œ¥{§ðâ#l/Õ­�.UØDD4ºL&JJJP€]±Ýí{«á�X½|*¦Æ‡ã‘÷ãO†JTT7 èû¿ÿÏgñwOŸÆ_òo"",¢ü%ÿ&6ýä¬Ò—VöòëŸãåýŸcÍ–q6ÿk~·pö³¯ñòëŸcÍÖ±é'g!ŠÀ�Æv¼öf!ÖlýÛvçâÇ»s!Š@yu“r_gÛvçbÓO΢¼ºIyÛöäaîêwa¶ØøFû5S¯ºJ]Vm~âAôÑ�™8AÀ÷æø;þšëÑQræ®Â‰ 'bïÞ½TÎÄ9§爎² .‹æ=—€‰¸ÏÑИU©"Ýd¯j=|¢¤ßï¿|­‡ß-Á·³Qqæ»8ù_�³Gt¨8³á�Ø÷V¡Û÷|òÙ×ø_;A,þG|yò L�Wn+¯n—'ŸÀÙ#:\þÓwðÈ‚û¥àöD .ŸüŽý±¿‹og%âlþ×.¡ñ¾·Šðí¬D\þÓw”çñ¿v.B!Äf) IDATEMÓ€^ y²Ô«Îª¿*——L°@ÃÍ…†VœJp9øëºU4è ¸n³¦šr””ðçâáÛs„s8¶†€Ð0˜¦öÃß§øyœçBì°¢Ûl„±âŒF#”hq#’¼ö¦˜nrj-°zùTD„âÍ„˜‘áAøéÖyxmg¦ëõª Ì™¡†ÙâÞs81.LY4lΠמƛ֤º\·t�Ô6aÛÆÙ.Á­|¹E‚\ÛØÜîR »iM*þòÖJ.RæƒØC–<êºS…n³ô‹=8@Z)�h¸þ˜Ü¥¬òÜYõWLH~˜Ã9‚ˆs„“ί‹”V“&‚‹æÑ°Yšì‡Ïª»q§Mj]ÐùuîŸÅ�!"¢1éÍ“%ˆTB€O>sôž;S�³ù_ãäÇX½|j¯ß?5>/?7€T-[y³—¯™pùš ×Mž¿'.¼÷ߣ½ô­�3CÝçëˆT)Õ´IYǰtÁ,]p?¾�5•‹“y©`QD`|<4�Ûí d}\NNJË*a3·cBjÖ€DéúÚqòìûüxJ$ « �?nÜî’B‡êÏÈrŽ òÙ9bïÞ½0µtÀÖÒ‰ o¬ð÷u/9þÐOæ 4|&N° Þ.íV¶=o dïv#""ï /æK7œöxŸ}oöÈÒB]¯½Y¨­Îíz>ÆË¯Žôéjüîçó0gºZ¹ïÒ zTÖ4�Øë‰Táåçæáåçæ¡¢º ' R({6ÿk¬Ùú!þò–ŽoºáQ4¹éºS¥œ9E%°G�ˆÙ±Ží¬«ŽPœ#ˆ8G(¯×é�Y±+ÒiØMž<0Ùi¬ÿŠƒBDDcJEuþd¨Dút5V/—Nëïùµi�€£Ï¬'òbY'<ŠÕ˧º·#Æ^¾Ö€ïÿóYœÍ—Ú.L�Ƕ�³qöˆ‰qa8›ÿ5ßtÃ@–܈V‹ryb€È¡¡uLG]wª8 œ#ˆ8GÈû\{›r™€ÐH™¦vìsb›™BDDcÊIC`Ó©½ÞG^èëÍ“½˜/÷ƒ½Ü£_ì÷ÿù¬²àÖHˆ ÂáwKð?~žç²¨WEu*kšñÈ‚ûù¦û²ä~àçôG·óãDÃ)NÅ1àAÄ9“îæ:årH Y…y¿ÓÊA "¢1eߛҚrèêÉÔøp|;+fK»ÒÞ §m¥>ék¶|ˆ5[>Ä÷ÿù,’–É�*ðí¬DRõêp›ŽŸn�‡Ë×LHÊ:†¿{Z�¿{Z�¹kþˆˆð@¼¶sßtòD4&¸œ‚kkâ€ç;çþÎüðŠFJJ”€?Û»et7Õq@ˆˆhÌ0[lظ&SãÂú]èjÛÆÙ˜3#J¹ß¦5©Xº`ŠrûœQøòäxíÍ"TÔ4áŽÅ†oœ…MkRQQÓ„93¢\ï§[çaj\˜ÛÏYºà~`ë<¥âön®ù¹yXºà~>Q¢Tçn\�Šmg÷Ú—¼Yrã|*hJ+qhd8Ÿ†+Z9 œ#ˆ8GÑ€íZ¿»Ö/èó>iIQ.ÿ’w’¿¹Ÿ¬lÓZ·ûÌ™…Ãÿ±ÔýzU�[ ÛÛÏíùs†úzò= d‰ˆˆˆˆˆÆ±Ó ±¹^ºÜa…ØRïñ~]·+û,«¢µBp„àþKùý''z¼^�†0!Xº ! ˜o^Ùü0[ÚxO�ó‹g{åëÏÌÌDjj*j_}‘܈|Y""""""Ñm6º¬bG›ÒvÂ9„¢µq@g1t›�ƒz\çpÖ/<„‰Òõö×/RÃ7ÞÇ¥'GßÕ÷™›mˆ òúׯV«¡V«Á�(ˆ¼‡@Ku5"CC¡Ñ¸þ®b KD^-pî:ÌŸ†Œ$6V$"ÎD£É/<�s×aÍ5Râ&q@†‘\‘*…¯RàÚÝ\tÚ|óõ6×C´_î5Ì ‚_XŒØúEj\±ËýÍ÷œÎ/ƒþb9Ž®£õÔ�¸ZoÀ;ï %%;vìpýµÅ!""oæ?)Qq‘ÐjÕ "âA4Š„€`øOJ€&é~hbC8 C@´ZÐm®‚hµ ëvå�V¸N ⣤˜3$Ðĸ÷… âP˜¨U€É4  Íwu=Ðju¿¾¸JT.—Ö ²O}§ Ýf£ÇÀV‹†ßÄHø…ÇBVÁ/2ÁëƒZîoî*k-8zæ:Ž®£²Ž‹ÑØÆ@–ˆˆˆˆˆh èn®C÷#ºÍUÒ"šwQñ L ¡Mpš©ö³$C‚MŒó½‡vqNµJúê�¶—ºLÏϧØ)c-qº\\%¢­]@u?µØ\�®æztÕ—: ÁRü"à7I¿°n€^ÈÜlÃéür=sç k\n‹ „.#™ƒDDcY—���Òš;8qÙ¿pþ‘ADœ#ˆFÓöíÛ‘_nÁ¥ŠfÑ8ßßäj×n³]·+ÕWur¸µJ WC‚¤jUªÁ§ÞKç×ù²s€+Wç–�V«cPÝ  ­·L»Ó†®úR—�Ö/Rÿɉð‹Ô°/íWPV�ÿÒ_�þb9Ì-®oòÊŒ$lÈšÝB†±D4v1�õq�Ö@5ü+C9DÄ9‚h´C­7ÚMðo4s0ˆÆáþÖm6¢«¾]wªÔz`b�/B#@#ý_ $¾Á=ÈÕ¹žÆ§Ø´Ùc`¬QRí9¨íÙò@‹†ÿ¤øß?›´c@e­§ó˱ÿT�[K‚´¤(lY•]F²O,âED¾��,Ñ0‘«0»JûmA09\j5�ª4Ñr{†¯÷J®ª�3Í1žÆ: ¤Zú·¸JÄí&÷q›ëÑÙ\�NãçR‹ƒ¨øGK_4²^8xôW\®KKŠÂSYÓ¡ËHBb,ï$"ïÂ@–ˆˆˆˆˆhuÝ©B××Eý†°ñÑR¬6Aª€U«¾ŽMŒs?]&‹#œ-©öЗ¶Ó†®[EèºUägïŸÿI Ìp¥¼A¹œž…ÿý£eHOŽæÀ‘×b KDDDDDt�D«]·ŠÐy³¢µÑã}&‡‹˜3 Ð&öjM€°cƒÜò`Î4éýhµJ´—¿òPAëÎ Á˜2þ÷Í‚Ì*Íá’£ÂyÜ”5`ѶãÐ-L‚.#+3’|¾MAnn.òòò`�fÿ""ïÆ@–ˆˆˆˆˆè.‰V :Ê.H•“ÄGK§ÊÏ™hb¾z‹�`Çû0Ö¹EîÕ³¢µeÐQvþ÷Í„ä‡ÌƒßnË–Ui8j¸Ž£g®£±¥ú‹åÐ_,Gdh�Î.LÂÊ ß\ÈËd2¡¤¤Àšl"ßÀ@–ˆ¼ší‹c¸P€kNÀóÏ?Ï!"ÎD£¤»¹%ä”"%9ÙÙÙ>ÿz;«þê1ˆ•+a3g N§Å“7ÓÄÙËýgs‹Dä]u]L®š‰`v¼ío�ž�ôähüâ™Å8b¸ýÅrœÎ/‡¹Å†#†ë8b¸ŽÄ˜pè&ã{Ë´li@DcY"òîƒ!³&3`âPç¢Q%vXÑm6Âh‚'øùîëì´¢³üSi¡§RâD,HPN{'ß$‡³Ùˀܿ†ÏáR5+³šyHú&„€`îoClCÖ lÈš�ÊZ ôùåØªUuM¨¬kÂþSتéÉQxjÙtlY•Î�–ˆÆ²DDDDDDÐÕPŠŽ’3n=b=è2¹(×x”ù ôUlòþ&}É:�Ÿ£óë"Î|þQ)¬a�«ÂÖUéØº*eõ8pê ôùehliGAY Ê.0�%¢Q @#ŠŒ�‡F£q»��¬�ËÉÉAiY%lævLHÍ‚_Ï™""ÎD£eïÞ½0µtÀÖÒ‰ o¬ç€yÑþÖQjp«Šu±ïñN«‘¾t™@Î7ìá|§ íWN`BòØšÉ�FéÉÑøí¶,˜›†>¿ G ×q¾è&†ˆFE,€ ‚€˜µk1qút·ÛÈú8£ÑcÅ Òi-DDœ#ˆF�¼ yÏþ&vZÑqí}tÕ—*×ÅGkÿN àˆœ©UÀW ¸üpø(=f;Ê. »ÕŒÀ™�s�†YdX�KK"¢±ˆ�,Q/z†±‹Ö.B‚96Ô»9Ó€=›�ãgm ºn¡`(;*k-8_t•uŽÀ51F…ų¦ 1ÖQ²î|™ˆh,a KDDDDDäAGÙ—0vãcR¿P¢� 6=&U͞Γ®ëºU„Ž`&$?̺ •µ¼øÆè/–÷zŸ%³ã°sÝ|,™Ç#¢1Ë�C@DDDDDäJ´ZÐY‘«üŸa,Ý-]¦TY-ë¬ÈEwsf� Êê±hÛñ>ÃX8WXƒÇv�ÄÃ5�Y d‰ˆˆˆˆˆzè(» \N@dK÷dÓc@Jœ¨ü¿³ê¯”A07Û°bן`n‘šò®ÌHBÎθvpƒrŸv¯Æ®õóxvßœ+¬áàјÄ@–ˆˆˆˆˆ¨‡®[EÊåìe„îÙò‡ÛQWC)ÄN.¨:PôJ›³sŽïzº…É.=b—̎îõ píàÓHKмôÆ�I d‰ˆˆˆˆˆœtÝ©R.ÇGK=@‰îÕœiNÿé´¡»‰m êt¾Ô¦`×úùÐ-Lîó¾‘aAøÍ�— ÊPYkñúן™™‰íÛ·ã{Ò¹9ù²DDDDDDÎÚÌÊÅð‘ãAC&!Æi{êjç€ PAY©UÁ@¤'G+U²•uM^ÿúÕj5´Z-Dps ò VU¾ª®†Ñht»=€CDDÞ,pî:ÌŸ†Œ$–®ç¢ÑäƒÀ¹ë°fŽ)q“¼úµt™Nµ·ùÞÒйå´=u·šáÏýmPÒ“£|_¹—l£½Õ�œ6îv¨ð6¼óRRR°cÇ×yœCDã…¼Úföž÷¼æ9››mzMã™ÿ¤DÅ%C«Õr#'Μ#8GŒ‚ÊZ ^ïóì>W%âÁ9‚FÍ‹‡.`Æ3G°ÿTÌÍ6ˆ¢s³ ûO`Æ3G\ÂÔÁjliǹÂ倮”7ŒÈ6½ÿTÁˆ|8cn¶aÝÏßǹÂ4¶ðÔå1%Ààtv Èý‡„î]ϰK˜ÉA ¹ýÀÑ3×tÿs…5J«ù{ɳ`§Ë—KÙ¢…†FžÓïMÎtž1�õqÙÙÙÈþþÎ]¿ð˜q;æfŽž¹Ž§–M—~‘®�éçÛßgAY=mËñ‰~HÄ9‚sçˆñdûöíøæšÍœ»Îë_Ë Ïcÿ©¤%EáÚÁ ¸¸/Þ³÷eãƒÝ«ˆg÷�ñÊôˆýgTÖZ°â_N*}ilïo§s�V+ǔ8þ†]wK·Pê»ûØ%8UÐï¾nÏû€§–MGd+dû²À©zñÌ—LŽ Ý›Ü¿AÙŽ‚¤qHâmÇQQkaå–—ìo& °÷¸ÈP–îJ«UÚ~ÚlÙ{ÞÃŒÍo!{ÏûÊßA¯l~˜ƒ×�D¸¶-ø¯“œëèîõüði>‡¤W di\ø/ýD„B·0Yùtµ¯Ó]ô˰h[BVÀ”õ‡ðâ¡ J/Æž=Þ*k-xvŸSÖBȪ˜±ù-ì>ö™Ûc¾pð<^8x•µdïy!« dÕdïyÏå1�®á¥7.HÏÑpÝåg^)oÀþSxjÙt\;ø4¶qŽàA£àÀ©+€_l~¸×Ê£%³ã°ký|lѹ×EÈÛ®óv~/•´G ו}ÒÓ~Ó×þÛóg?¶ë$ ËM(,7¹õ°u|èáøþg÷Ü>À9b¸¦ì›36¿…�UðÂÁóÊí»�]Âì$5.¾–­Ì94vØW]ª®ÊÒ Éa>ÃØ{„‹¯e+b‰ç÷r—6Þ½šÕ±´J �ç:º[Æ:×ù.ÀKï_pÈ×UÖZPPÖ œŠ¼!k^*k›pôÌuœ/¼‰¼×Ö"1V…ÄfO�BAYb±xÖDÚÑ'Æ„#ïµµƒZ]”ˆ8G µóER€¹xV\Ÿ÷Ûµ~�ÛuÙ{Þƒþb9Ïš‚]ëç+-CôË‘³st “õ\žÝgÀÃuåñä}ò|áMüaç ,™íxŽ/<�ú+HKŠr»ïû»¿�ôäh,ž5öEcÏš‚Ä•Ëþln±a‹. +3’PYÛ„#öþÓy¯e+þUuM8WXƒúÕˆö`Àynø`÷j—çEcÛƒS�‚Ž bç!?ø¶­†cC}3|ès]ÃØØÉ@ÝŽÍ݈ »Wãè™ë¸â¡ÝKDh ÏŠƒna6dÍà€ B€'a_Þ>×ýìðƒošŽ l¾;þ�æ;•(â).æÕ'²äóöÛ{ =•5]¹N—‘l?ø+s9ð37ÛðÒ¡O‘Ž‹NVO-›Ž…ÛrÜû¥7>…¹Åæ€ì>öv»„#†k. ”5`eFŽïr„0‰±áØ}ìôùåØº*]9@;z溽ºh�Ó}UHä[JÄ9‚s�2¹i°•GG × ¿Xî¶�o]•Ž…Ûrðì¾3X<+nÀ�{®°G ×Ýo‹.3žy Ïî3àÚ¡§¥ç\kÁý,ž59;W~ÆÊŒ$,Úv»�]Âñ]�c×úJŬóþ%ïÏ=Cã´ä(¼xè^€ó´0PÏ? äê¢Æß,"Μ#ÈkÜMK ýÅrR«—DzïCæ›R};ò>$W»ö<`¯´WªJûo¹²�9ï¿éÉÑÈÙ¹ÂcE¼ÌÜlù¤%E¹Uðn]%õ5ôÔæÄùƒocë”NEèîFûÕ÷`Íý ºJÇåö>gðüZ`r¸ãD鼫þý-§ó¸à9‚‰ÓyÀ¿¿%ÚÃXI|´´ýd>È1¢»Ü¶L&”””  €yN*€§ U7*¿»s¥`6ïo|È¡Õ*Íw;º†±QÄSb9Dýb…,ù4ýE)<‘*ÌÜ{6žÎ/Ge­EYŒ¤Ê^ñ#ŸžèlÉì8Ð_Qþ/W–7à±]'=þüÂr“ÇÇ¡!<`üâ.àÚ‡ðüóÏs@ˆsqŽ!ò6>…åÒ)¦=ÝÂdÐ_Á•ò†·-¨¬“Z ¼ôƧî·ÙÛ\)oÀ’ÙqÊé­ž»ëïç]±?ïų¦x¼=-)JiSà¼{š+¼EP€TÕ'8�n(ZÑ~åü"5Hú¦ÛbYÝÍuè(1 §,)ɉÈÎÎö©m^üëÓ _H¡€T-«Ï>þ\ÄœÀŠÙqªØä‰È»*ï/Žýfå" ëC_]èëû[Oeõ°´¶ßÕ÷ö×^‡\ÅØ,ø@¡ý:“E:K@Ÿ+UÌ.⇠ã–É"…óîÚŽ%Ò^Kئ`ÀÈ’O“«Û ÊPà¡Ï �öë©Ç]O¡®§OÊÕj"ìÕ#n¿ø§ð Ýf#LfÀÄ¡ ÎÄ9bĤ%EáJyƒËž÷±k˜„‡”ÚTÖ5!=9jÈž‡Ü—ÕÓ>–Ž„˜pD„p„·÷’öÖJÁWÐEQ dÿ »:”ýªýË?À/Rƒ ©Yð “ ŠVt›�0š�à ¾y"^H°FÌ™äœQZ#�O›M@ÞU ïªtÛ¢¥É·åý È-rmM K‰ñýLJ/ û›³]Àù¢›wõ½ã¡¥ÁP  ƒÔA/аØÈÁlÎ_D,Ÿ'`уà‡Pㄱ0|îùƒ§QÄ£‚€Ó 0�%ŸUYkÁéür¤%Eáâ¾l�·ÏxæŽa‹\5ãéÔ`¹:¦gø²!kú€Â"âAä+t “p¥¼¡ß,^:$õ]½ùûͤ ··?äÀt0ä°õÏ{Öô{ßÄÎã¦R1ëL®¼ê­ŠJþ9½=wyÿ÷µ wçêXÿ¸¹€_:+r•ëºÍFØ>; ÿûfaBòÃãjÐÄ;Ö (6úOÁ, �ºyù+`b�T5›5Oà¢8>J\¾—ê0%˜ˆ“z k5¬#ï—`« à €sNÁ¬|v€>H�—úl§?À>³¾F®†Í-’.÷üàI#ŠX"HdU¬GÁö1 Œ�‡Fã¾(Y—““ƒÒ²JØÌí. ã�Ü+NîéöË%V…•I8�_®,¬³Ä~ v@ÅíôÅÓöÇ“¥%I>Îa�rPYkÁc»NbeF~ñÌbnˆÄ9‚sçìÝ»¦–ØZ:ô�õ^û:¶èÒ±ÿTœºâ±O2¼pð<Ì-6<µlºRY*WÖö\0�ú9Ò"[•–…óE7ÝÈ€|Í€ªú&üç?|éÉÑX<{ Žž¹ŽóE5nÁé?ýê î4ÛpݾXOéÉш Ä…¢›07Û\*eÍÍ6–›<¶Bð%]�7<ﻘ’†Ž² èºUä¸íVºnÁoòÔq·¿i5€ÖÌ>QpÃqPê\5«V©ñ"椬œõB—¿.—:‡°îáâ¥/±Ãã•Í+gEôæ|Q ÌÍ6=s�-íøÍ�—¹ýn »“ ÍC0 HârÏä9Ó¤/†³ÞËX'½Ÿ¹E"ªë=Ïg b&ÀA@ÌÚµ˜8Ý}]²¾¾3�0VÜ �Ö2ž°¯œ®Ëè½/܆¬é8�_Ž·ÏcCÖ eQ‘£g®ãÙ}üP—Kk;öŸ*P‘E†a‹. ôWðì>þóFdXÌÍ6¬ûùû¨¬kºçJ™+å 8_TƒÙS£½’5çÎ4ö”””øÄëˆ Â+›ƳûÎ`Å®?a×úùX™‘„ÄXÎÕà¨á:Ž®#-) ¯8-àµký|ûþs 1áJ�»ûØg8WXƒÅ³¦x w{³uU:Žž¹Ž—}ŠÄ•²O1\ÃÑ3ÒÏ—O—‘ŒÝ1—pàÔ,ž§Üw÷±ÏPPÖà¶0Xa¹ ç‹j�ŽÄX¶®JÇîc—ðì¯ øÍ�²”ýyÅ¿œ„¹Å†�=¾ß׈�ÒѶ¬BàÌÇ!&?Œö’�ÑÝàXÉ£ûv…Òæ ««k\íoZ�Ä™,Rx÷ñ_EÜnr¨š,Ò"`yW¡EªFª´M�çÜ8ææêj{(at^¬Æ=x˜.bùCÍdø4Üò»Až×w­_€Çv�ijûÎ 24hÀ}É©r0[ Ž³2ù ˆ�‘9K@j–«Âo~¼ /º€EÛŽ#24f{ �ų¦ gçãƒz‰±*üöÇYøÇ}<¶ë$"í­BÌ-6$Ä„#gç 徑aAÈÙ¹Ù{ÞWî+ÿì§–¹¶‘+oÿ~§TÉ~|×ãØµ~ÌÍ6Ð_�þâ!·ýyëªtŸ|Ÿ�ûÈv7×)gRÁ*¥=�®;Uè(»àlí÷­¨¨ÀéÓ§±råÊqµ_¨UÒNYߤSÛ¿Š«\ÃÙž¡ …²Ú)¤��b¸7’Z­RQ]W9*ýz39\Äœ½)‰¶ IDATi@æ,š†c‘ü¡ác»NâÅCȃTû×·”ø @]�ûT× 8þÏó?ˆý9¯Ä{¯‚•¥ˆ"Ò©Ò/yàb K>)1&ì^= E6>ؽÚåô—Ȱ ßõ8*k-¨¬kBDh Ò“£±ûØgna‹|ß‚²z嶈РèìUBÎz;Í&-)Êãs½vðiûé6¶^_Ç@NÝ!"ÎDÃeCÖ lÈšýÅ2¥�jDhPŸ•®²f@—‘ }~ªì�¦sÅj_Û¾§mZ·0×fŹ<^ZR”Çðôäh\|-Û從Z.üâ™ÅX2;WÊ”ö#òõOÙ+çûÚŸŸZ6‹gŹ|oosßÑÖm6ºµ¶ñŸ”ÿyß•‚Ùë@l3K÷íî†^¯Gnn.t:-Z4îöM °é1�ÂÙÜ"%ÕªëÝïë|Ê/ U–i5‚REËEs†ŽÉ"ƺ�…Òû!µœ`ë=ä¯+ëšp®°†\“`Ø«f4B g+D¥‚»¾æ¹Ôx~5\ŒuòNÒï “KË~÷÷)RØžhÿ7˜!ì°a K¾¶Äªú\õ¹çÁ™¬²Ö‚gu[tiÐ-LvyŒÊZÇAž§Çèï4šÞn� òøBdXP¿Õ6ƒ9­“ˆ8G ÝÂäAU E†õÛ×ÏÓ¶ß×~2Ð>�½oo¯i ûó`æ˜ÁÜw48/ìÕm©íõ~þ“€é�¡ýË?¸TÕšL&>|z½k׮Ŝ9sÆå>¢‰²—9V)/6Ú«“sGÏ\ï÷ôf"âÁ9‚ˆ|Uwsí€î'bcca±XÐÖÖ@ fýë_#55+W®„V«·ã¨V™J_€ œ>*W/yª m³ öÊ2÷êøh!A´ Òc«#ÆOÛƒV+PÝ`_mRË�V›§ª×¾ ¹V› Ø+õ¾z;s³�aì(r®œ…  @-¤êÙ*Apko8zm{š3Õ*©ÝÁÄ ÇYãõÌ��}Ø„>ç<�("Q�iá©`ûûD#‹�,Q9;WzÑ9[™‘„ßþ8‹DÄ9‚s�Kbs=ÄN+„€þ“>•J…Ÿüä'0 0 J0[RR‚W_}©©©X»v-4͸×�`yerÇÁ°smiMßÉrøè©÷©\Y+ÿ¹ÚL5€±Wi+‡ Êe›}Lª÷é»Ïkßã51ˆ�r­€È÷‘÷07ÛðÒ޾æiQEÃ+Âþ•jý¬�Z¹‚ÖØGh²Ø[�xØïå^´šLJPñÑN—½èÃ)ùuŽ*WÀºÞ͇M€Ô‚@®~��ÔŠ€áëØÀ@–¨‡%³ãpýÐÓ8WXƒ+å HŒ GZRÔ˜>��ˆ8G ¿ @w »©NjO0!!!ÐétÈÊÊ‚Á`ÀéÓ§•ÛJJJð³Ÿý ‹-‚N§ƒZ­æ8;ÑÚƒB]¦£ÍAC£Ô°Á,ÂX']ç©Ý�3Ge­Äy!±Þ ´òLÛÏfÐj•ÕÁ„Cir¸¨œò,ŸÁöý¾&&Nœˆ���AÝ6Ü^8x…¦Ý÷JYƒË‚‘¾ð7Zff&RSSQû꫈ô�í,R0èVBZ¬ RHk륒֙<¿õ·à8›p p=I½‡Ï [­ðx¦ƒLX>ÿõºÊ4¢ˆ`±‚€û U¿F8�1�- d‰ú]Øü�ˆ8G��v{ {§jÀ�¬Lf333¡×ë‘——§Ü–——‡¼¼<,Z´k×®• Ç(§ìjÜÊ‹�R©Ò6˜û¯ªíË@Ò� #!%Nê9)‡¯Ž¾“ Ëh4bïÞ½HMMÅøC·Û÷»ß¡µµ;vìñ}õJyÎÝÔ÷D„â•ÍûÆ VC­V×;‘(-à ÖBª¨­„´p˜YQ'° òñ�«KGwþº·¹I%Šˆ€¶N²W½F‚ÁëXd…ô!ƒ¥º‘¡¡ng1�%"¯8wæO CF«‰ˆsѰðÐènî½^É/<�s×aÍ5Râ&y 6mÚ�N‡œœ(·åååáòåËX¾|9–-[Æ`väSîçLs?ØoµF{¥–ÚJ׋Êå¶v¡Ïj®‘ L ÈUlŽŠ¶‰ö6íšhçê¶ñ@ô·¿ –ƶµµ¡µµµ×ûÕÔÔà—¿ü切²žMí‹na2žZ6�½ý}@¬ýßža­Üö�ÂZY¥(:¶k/ (ƒÄØŸ»\å !-º¥ŒCW¯Q àmx礤¤`ÇŽ.·3�%"ï>Fœ”€¨¸Hhµ<Õ‘ˆ8G ! òám·ÙØçýü'%@“t?4±½5jµ?üáQ\\ ½^�ÒÒR@[[ôz=>þøc¥ÕÝ›�`穞CÛžäöý©®—ß¾~v|tÿ�ãh#à²5ñÍÀ~9�ým ä06,, aaa}Þ7<< #Êþâ™Å|ÓÉ…ÜöNÿJ;‡çù£²—Ëï+Šh`³µ ºø‹"bEþ}„¢Îaª'Îk0�s_Ï�|Y"""""ê�Ÿ?„€ ˆ�6 ÓÑj�|ïUçZ­Z­ÅÅÅ8~ü8ª«¥sHÛÚÚpüøq èt:,Z´ˆïÁpY®ÇæœÃصk×âäÉ“}Þ_¥RaÙ²e8~üø¨TÊÝ­Ä^.€U¥¶‚Ðç"c›Äúùq0iH0�õqÙÙÙ(­¹ƒ—Mð �á€ç¢Q´}ûvä—[p©¢™ƒAÞ%4h”Ó®;U¸Ö�=´V«Å¿þë¿"77z½·oß - tøðaèõzlܸZ­–ûÑêÆ¬CiLL Ö®];æCÙ�U­§¶ðÍ&��ªd+Eµ�‚XE/aì·Ðwå+Ñ`1�õq�Ö@5ü+C9DÄ9‚h”iµZÜh7Á¿ÑÌÁ ¯â?)�ö@¶»é0„�¬,33™™™0 Ðëõhkk ³¯¾ú*RSS±råʳÜ߈zw·a¬l´CÙóE5wu_UH Ò“£¹Œ3rðZe¯€µÉÁêÖTQÄ|†±4ÄÈQŸü&%¹€î¦ºaýYYYYX´h  ƒÌ–””(Áì¦M› V³74Ñݸ×0V6’¡¬¹Ù†u?ç ký½¿Óц!24y¯­Eb,û/¬¢ˆ? ”–×}«¢(Bèq{„(b%‡‘†ão+õyÐàÔÖF´Wʧ��èt:ìٳ˖-s¹­¤¤;wîÄáÇa2™ú},ÑÚëg‡!vXùFÒ¸7Ta¬Leå…¾Z[[‡åy¿xèÂ]…±=™[lxñ� ÜÆ‘`A€§%áDQtùApù?øÀå ÛYgù§èüº�o*ù´‘ceñЗÆVÖZpàT^8xÞí>æfNç—á¨á:*k-ÜHÚf¼à’‡ßžÂXö�¥¡ @#Šx .�ÆívVÈú¸œœ”–UÂfnÇ„Ô,—?¤‰ˆ8G�¬½{÷ÂÔÒ[K'‚¾±žB^Gˆˆ‡ØX @ª@÷�JÏ+33™™™0 Ðëõhmmõx í\ Õqí}é€èþÙ|cÉçŒV+ŽJÙ‚²z¬Øõ'˜[lHŒ Ç/žY¬ÜvÄp /úæ›rÝÖUéxeóÃÜÆ)+€ÓJz\Ÿà>8Zgi�# ±6bÖ®ÅÄéÓÝng…ì8øEl¬¸�n³b‡•BDœ#ˆFQII L5åè¶/2DämüÂÜu[jÇÜóËÊÊž={Ü‚ŸÞpé(=ƒî¦Z¾±äsß�f+ÊJÙÊZ m;®®¡A.·=»ïŒK ûO`÷±Ï¸AŒÇ¿·E—06R+‚ ‚ UÁöøžXË{œMA4¬Sqˆˆˆˆˆh ü''*—»îT�É瘗—‡¶¶6åÿ=OOui_ÐiƒíË?0”%Ÿ1VÂXÙP…²»�IõŒ¡�ø`÷j\ÜçXÄlÿ©åò+›ÆÍßoÆSˤj´§®p£G¬þÛþesú .E±@ªÓ}�Ã×`+E‘}ciD1�%""""¢�<„9jŠÄ–ú1÷üZ[[¡×ë]®ëYëV-key¦y;“É4¦ÂXÙP„²ç‹jH�ë’Ùq.·�ΗïKKŠÂÖUéˆ Âo·e!"4æÎÖpã.Á½*V%ŠxÀÿ+è¹7$ æÛCÙå¢È¾±4òSqˆˆˆˆˆh „`„û©Â�6t7×�©çg0\ªc©BVþòôùµ0”%o×ÐÐà¶ý�*• *• ·oß¾«çXY×X2Ë5Œ-(«WnÓ-Lr¹--)ŠÅ8Ðà(€�àZû�(b³ ¸TÅö´Øþ•Æ0–FY"""""0!Ò±RpwÓØ d[[[a0ÜŸ¯ (_žþ/›ë`ûò|ƒÉkiµZlܸõõõ8~ü8¬Ö±ñƒÕjÅñãÇÑÜÜŒçŸjµú®+1VåòÿóE7•Ë+3’¸Œ3—E87ÐQ‰"¾àQU±= si”0�%""""¢�@„9-ì5ÆúÈêt:¬\¹)))wõýbsÚ¯¾Ç7™¼Vffæ˜ e{†±�枯²ÖâòýÅ2RoÙôäh—۪앳ä{j!±=«b°Y�È!"oø{ŠC@nÅÄHåòíVŽ�Œ¶§7‚8 œ#ˆ8G�Õß“”ËÝÍcg1¬��deeA§ÓaÇŽ.·Î]‡ )Ë�ôM‘N¡rO]·ŠÐU_Ê7š¼ÖX e‡2Œ•Ûœ+rôƒ57Û” Y]F²Ëý÷Ÿ*p´9èÑsÖ[ßÓíÛ·ã{ÒÇùö}À꜂Øÿ` €`Nä%8Ô“ì8 ÄÔ*r@hDÔXœ¶µÐhç"ÎD£¨­Ãq +Lp=¼õw dÅæz¯x=þ“ûóž�ôMÇó·6¢»­bsÄNºîT�6ˆc¬7.Ñ`effÞ|óM?~|ÄùêÊXÝÂ$\)oÀK‡>…‰±á8j¸îr»ì±]'•…¼žZ6Ý'ÞOµZ µZ=®ÃÆJ‰¢K HU±K¸ËÓdPÀR]�ÈÐP·y��,yµÀ¹ë0j2’T "òÉ9Â/<]�Õ€ 0MÍ÷•†_uc·côPM*„E+al×�*øOJ€_x ç®Ãš9j¤ÄMòŠ×)GÀ?8Â-¬m/=‹Îª|·>³DcéwCûÛh…²CÆÀ]:öŸ*€¹Å†ÜçÚ+:!&º…Ž Y9Œ� Ä®õó¹±x9+€ó�úÅÂiNÖˆ"V "8D4FÕxÞy)))ngï°e¹ÿrWÅ*— ous@hD”68ªßœO‰ï�ÿ¤DÅ%C«Õr9G矜#„ÇÁsu#«Òid8W¥ �Ý„9~È}d…€`øOJ€&iÚ�0£¥óëBt}]À0–Æüï†�ìo#ݾ`8ÂXˆ »W#!&ÜåúˆÐ@äì\árÝ]v­Ÿ�kŸv[Œ¼K%¤ö—œ® ð-Æ’—c…,¹ÿ�í´rîÍ&©oßÄ üƒ”†—s°ç“ÂáAÄ9By½©è¬Èun4ì¾29Yÿ(÷}ÎoRºnº}äôþ®;Uè(=£´+E‘¡,ù„ÁTÊ�FTWW÷úX>ø T*Ï!çp…±²ôäh\?ô4ÎÖà|Q Ò’¢°xV"Ã\{Ëÿâ™Å|Ó½œR{‚Âs0«bÉ—0�õqÙÙÙ(­¹ƒ—Mð �Ð÷Á"â!ÚO�,¼%b�†ŒÒð¹Ý&{ÊA^¤†ƒÂ9‚È'çˆíÛ·#¿Ü‚KÍþ¿° HØ,°v_™º1MÍ“œhøÞaí´ÿ'HåÒ;\Ù.�~gŒÕ>²ÝßDk#Ú¯¾‡n³Ñq¥„.7ò e?ýôSÔÔÔôú8µµµX½zµÛõÃÆ:[2;Î'ê"ÏJèE6§06€@*?$#Â@ÖÇi4XÕð¯ ܆«E‡=lù ¤ 4<ð£áó~q—ã/jšËé¹Ä9‚È—æ­V‹í&ø7šõ}þNÕˆ”tcë"îs4|>)wìsþÑÓ<ÞÇ/,B@ÄNDk#D«Åcp;–÷7±ÃŠŽÒ3ʾåòú"¦ ûv¹×¾‡/¿þ9þmÿçýÞï§[çáåçæ�©ç~òã ¬^>•;â0h(ë©×!üò—¿Dkk«Ûõ#Æ’ï²8 )�uî›"ŠÐ x„H¾†�,y>ð»ïAt–]€ØiÃí6é`x…–§HÒлÝ\ªvœŠ yˆƒÂ9‚ˆsD’VB£¯L"«diØ|fìVÚA˜�° ÷;‡Fö纛káì½Å+:«?G§ñ¯@§ç*X!4ðâ@V–>]�HU`¯·O� SÏ7iÙ1$Æ…1�FC½ÐÃX —œëQ«E<*¬Š%ŸÅ@–<ÿŒ€”,t\{ðIy7Òî÷CœŠ“! �¶‡>ëtlwñð·¯rLœ#ˆ8G8ísÁ*øß7K e¹ /,Ø¿™†|Ÿ{÷oNÕ±ñóú¬zõŸ”€N{ Ûu»Òc¯Ù±¦óëBt–çB´6öz¿¨iü}ã0éµ�‹°4cŠ×<ߊš&$ޱ�Ø U(Ë0–îU#=€*À¥*ö!QÄVÅ’�c K½o÷ÏB§ñÄæzX;�7.uâ…%<ø£!óîߺ\úB=¸’ƒÂ9‚ˆsD/&¤.CwC©R™þzn'þöî=º©3½ÿw[²äûM¾–| –Lbc‡ <@(ÎJšgBÚ¡��Î�$§Iº’&]…öœ¶«ÐžL�&˜Y“dæ˜ ©Ç$ ÎJ&™˜!„±!L¨o,_�ÌŶ@ø.ÛÒþý!k[²dc[¾Hò÷³‹­ÛÖÖ«ý¾Ö~ö³Ÿ÷Õ‡‚¹£Ð´èñ“Š!©v¬ WBž2~FzPT’´lëò퉽FOØåL‡3‚‡ƒò…9°uÝäN1U—:p§k€#W9îó¿øê :R�¼%ñ“z/Çk'ú^4>oƒ² ÆÎ®ŠŠ TVV¢ÀÒá¾äê^s@µ(bÐ)&Šø® I0Ö_•ùxjþ&€Ùªj=ü�fáxšM@ãQÜûg¸Pârð÷7ù ¸�÷Þ«ÂyãÈ ÎÁK÷¹ÚsÄ1‚8FøA‚àœ�øŸ_°Otö‹?á/seìs䕾A¿ø£ë Ų-w­×ì<Ážc¢G_ÓÛy– ¿v�° ®AXÁùrØ�(È2ç]@öÔ¹kø“ï„—¿Ÿ�7ö廓ɽ^À7’-°—¸ {–ëÔܸþ~éün/‹�`9€Ôü¾ûüNqG¦ìö–R‘$Ð ·1Í £ñw�ˆDçl”n_ëþ©|­�"‡¦|Ðw°Ò5Ð"S/ƒ|aö”Ög¹P‚3ÇÞFQQ—cqŒø1B›‚à%�K·koسoõqß¡©ií´ïCŽº±€ýHPDâÝ�£å!€räD‰µí2,JPú˃(--�ÓÏÕÛÛ‹ââbüîW?v ÆŽÄì�Gp€_”]˜ ëV,Bjr~SÞâöXñ‡ö Ð+ÛsÍÆ.üÉ÷?ÂíN þß¿>„Ûç·ã÷¿Ú€Ü,þêï¾ÀñÏ›ÝÖñÕÈÍŠÃïµÿsü)|¯ §Î]Ç+*yKTøý¯ìWAäfÙ—wlÔ6¾ø;Üî´àØ¡G Ö?‡¦ò-ø^A*Þ8\‹âëçmßµu·MKËÏÏÇöíÛÑÞÞŽ£G�b```Üç 0;�õø€÷`Ÿ|ëÎmÇ�á÷ox{ú¨�õ‰"jç( –á÷þ€wDqξç@Ç YšðÁŸ£VdÿðãÓCxL'ÃÚ4Ö¯£‰;o´á“z«Kà@¶  íÃSÿ1j6ÀdLl^ŽÄ1bžŒò…Ù°uÝ€Õx€ýDÈk_ âOî‘áÑLžk§‰éqºYÄ￵Je X[0© A‘‰°Y:‡û›6³3<·ûâÑ£GQYYérŸ§¬XQ]2dƒSkâ@GÖéXÄúç¤åuø§ƒ_ãÔ¹k.ug×#7K…4u$à~ sç~ÿ« ÒóÖ­X„S+!mýá‡*Ý&åzèÁ…8þÓG¥ÛÅÿ¶±ËKY¯1QJi]1Q —÷oníÂö�Zi�iêHÿÛ:¼r r^—-û§­¿9gÊ@LL̘ÏmooGhh(ƒ±óÔQD›‡I¶,X0µ¿E}} �Ôknܸáúû@‹(bgLþuÀû*—ûãââ P(feº»»ÑÝÝ-Ýn¼#ŠØ*Hb˜ÞßôlšèÁ_Pd¢ti2`ŸUý÷ß+4AxPÃÉ|ȳ[}@í ¾h´ºepk W?ÀFâA#8FL’Bû0†"¸œù¤ÞŠsW­x(C†Å*�}ަ—¢Ç?o†¹sÿñw#ûÅ_]Gt¤Âãdaë\„ÃÇôh6vI\ÇýÎb¢”ÈÍRMh»R“#pø˜‚<ùp¾W�†˜(%Šÿm;ô4rʆ……�ù¼¹ Æ>÷F9R“"±u}R“Xæl.œ\‚±©©©øîw¿‹ˆˆÙŸˆ¯»»øÃÐÒbÏêoœ°Ö�Û÷&†ƒ±Ãâââð�ï|iii³¾-‹ øãÿˆ��XeôžM!4¢…ZíqÌd@6À•––¢¡±ó‚µºül,A‰P>øW°\üHªÖ?dŸ]ý‹&Bä€:Z@h0 Žf†Î|ÖÑcÃí>àV¯èù2Zeº‚y{Y ÇŽ#æïQTTSÏ ,=CP.ÛâÕº'B,ÕÊ·ú€c­öò¡@\ûÆ;6ô Þ¥Ïå>5¥¿Bx‚´ìi¬¹d2™‡ž;·ìÛ*nx�îS�fÇF­ÇÀ©'iêH|¯ ¿)o–îs”pÎxudµ º·Æ\Ws«k@Ö“˜¨‰e{?ôž|ñ3¨—Ê'<ùpž|8 Û‡ËÐôÈÏχJ¥B|¼ç × 6'™±-m�8rò2ö—œÇÚœdl]Ÿ…­YüÒf«ýá,\·n´Ú¹ëxôÑG¡×ëqêÔ)`xûRáŸ5eû|àt;55�>úèœm�R©Dvv6ÒÒÒðÁ```m‚€ÏD�0(;aI¶ 7mBh–ûxÅ€l€3 04kÿ‘çMvj IDAT<è}e!$ !ü%†®×a°ñŒtè¼8ê�ÕÞ°²ñÉ}ÿ‘+!S?yÊwî:QqŒ Ž�È1!Çt ŠHDèwÿ†®×a¨¡\ÊPìÁÙ[}ìs4ÞW‚3V{ˆ”ÅŽL/#ôødE‚LØ¥ ¬[`V®ä‰bØ�¿)oÁñÏ›±îÁ…øMy ¶oÔº•ÈÍRá�q&ÔÊ[¢š¶mÊ[�擉Sç®áøçÍ8õÕuÿ¼Ç?oÆÿ|Óá6 yG§ÓMé±Ùtº¶§k[ñê;gP¸2/.EnF¿¼T/ŠÒeô©©©sŒu¦ÕjÑÔÔ$eÊÖ‹"Rý0`è<9šB¡Àºuë|b»"""°nÝ:|öÙgöß“aw˜6 ÈÒÔvœ…Ù�/̆µ£Ö¶Xo_u ¼¹ âC– …,a1±#ˆ8FÌPŸ“%,†µý l·¯ÂÖÑàœ%räJÅg"(6eÚúœ­–®ŒðÉÏ,­RðÕãd^ ™‚9þìxJ‡WT¢øÃz˜;ícÈèz°©É¨¾lò˜yÛl삹Ë2­µ]«.u &Ò^cÖñžÍÆ.ä=ùßxóWu ÈΟ؈wË/¡ìl>:×0÷Xðnùe¼[~¹ñx±0V¤#&BÉ›f7�–srr|jÛrrr¤€ìM?m_çé³³³¡TúÎ>œ––†ˆˆtwwÃ"¸ š]bz~¿³ ȲøL)›ÀÖÝfϰ³Z`ë¼ÉÆ™Ï{¡1Bìµ�œ3gˆcÇâ1ƒí*±g:g;ZoÏÍË>7ïE%2åŒõ¹ ÈDX}8 똸˞) Øì³˜9—-�-ÈæŽ2ìɇÓpø˜=»859Â- ë¨ûÆáZ¼²}$0cî´àO¾ÿnwZÐ|rË´e›�]¸ÿɱ}£Ö¥flš:éêHT]â´®óɶ‚%ØV°æn Žœ¼Œ#å—QÓÔ¨nìÀso–?/ ϬÏÂÚœd6Ú418�—‹-ò©msÞƒŸ^NÇi9*Ê÷j$«T*i¢¯›`@vº0 KÓ÷cÜ©î/ù""ŽDsË9ðÆ>G3:¾G.€£(†#øé+œ·'(2Šœ�¨=îZï6$*`Oýð_+ïZ«õ÷¿*t¹ýÊöl>¦ÇoÊ[ð^rŸXñ�½«püófüð@%ît `݃ aîÀ?þäk4·váÿýëCSÆ>ôàB|ñÕuüð@%¾W�Šu+á¡âð1=ÒÕ‘X÷àBÀñÏ[PuÉ„—¿Ï@ú|¡ÄKOäâ¥'r±¿ä+ì/9ïò¸#k651[ ²8ù<³SIˆ¹˜$ínT*•”…|«wOd‰ˆˆˆˆhÊd±)ô±mêííu Æ r%÷þ‚Bcòà \úÖëu€`Íwö»™JiÞ’xäf©P}Ù„&ÍŠ‰R¢êøŸcÇß�Â?þäkéþèHþÏK`ÇSS¯3ºc£U—Lxãp-þçRN­X„ã‡Á+*=¾×?þÍì€X“½¯í\�C'jPv®wzÐÒÖ…ý%籿ä< W¦£pE'#"ŸÁ€,M™A®„8dñ™ìØ¢¢"—mQ,Ûâr¥†bÉã�)a5~ YBàe�ÿãßx¬¬úÍŸ�ûxš:§Þ-„¹Ó‚ªK&¤%G"Méö¼u+A¬Îã:N½[èvߎ§tnݘ(%ŠÿmŠÿmª.uÀÜ9à±~-QnFÞz¥@�[½Ù²³M(;Û„Wß9ƒ­Yxf½Ž�Ñœb@–ˆˆˆˆˆ¼žøHÙââb�®ÛâŒuPh `[˜ !„Õð¦*&J9«ÁѼ%ñltšG½Ù–›�(;ׄ²³�ø²îÌ=:ׄý%çaî±øÝçP©TP©TáWJä7ú´è4�Fãò8²Dä×÷?�åiX‘ÎBýDÄ1‚h®ŦÍ€˜¸xlÞ¼yÖ·¡¢¢'Ožä—Aó£ÏE&BqÿÓØ˜§Bfr,Äs·�kBÙ¹F”�mry,:\É"¢uÀ{ðþûÈÌÌÄž={\g@–ˆüš,6ñÉ1ÐéTl "âA4G‚"GJܹmrË™iõõõ8|ø°t;77ÕÕÕüb(` òÈbS I_MRÄÉéÚV¼wò2ÊÎ6¹eÃn]Ÿ…­YX›“̆"¢9Å€,yE�‡@ˆH€ØÝÀ Õét³òÞƒ?ûÙϤÛjµ;vìÀøC~1DóDgïö—|…#å—ÑÒÖåòXJb$¶dáÅÂ\ÄD03–ˆ|²nóæÍhh½�cU&—Ì…é&õCìn‡­« °ZØðŠÑ@‰†ÂK…9FpŒ ްk×.œkêÄùæî}±¿bÿö9!S"(2BDùÌT! ŠH‚u8 k4g% ÛÛÛ‹¢¢"ôõõâââ°{÷n„……ÍZ#¢¹su8øZÝØ�êÆ—Ç6¬HǶ‚,®Ì`C‘Ïa@6Ài4ô+T�µ„OÿÁÞP?¬×/ÂÚ®‡Íl`cÓ˜„ˆÈbS ×,gp–cѼ#t:¾0AvÇ<ý}®¿Öuj«—²‰Æêsò…K!KÈœÖ>•ë�:ö Ù‚‚‚ý£ƒ±¡¡¡xá…6ãý�ˆæNuc;~ZVã±$At¸/=‘‹­ë³�šÄã"ò] ÈÒäø†úa5^ÀàÕóÀ³nhûLw;†ºÛ1dø²ÙÎXÍÀ,Ç"ŽÓÝçϰ1hÂ}n°¡ƒMgœ²2õ²iÉš Š©k4güs>|Øå}þú¯ÿzÖk×Ñì0w[ðÞÉË8rò²[&,¬É^„­YØV°„�ED~�Yš[wjŽAì¿ãöØ¢(‹ã€Ð`� E€+&¾½åzŸõF¬ Î,€|a6‰cqŒàá…¡ëul(÷xòãž8 9J`Ÿ#@ß ˆÖNÑµÏ Y0ØxƒWÏCqïã�ÅgzõA#¥oL&L&Tª™™P¯¸¸UUUÒííÛ·ÏZÍZ"š}›|Œ/뮹Ü®ÀÖõYxé‰\fÑßa@–&wÐwéc—ûbC�Ç´2ä,ày"ÔÞqªqhä pÈ‚ÁKÃvû*÷>ÎfâA#8FL¥Ï5W¸eÅÞ¬Ë�c±Š'?ȳ¾Aµ7Dœ3X]úÜ@Í1û‰Í^­_ˆVC¼cÏZ5 3�-//Gee¥t{Æ ÈÏÏç—K4O,M�Ç‹O,Eኌy3IWEE*++Ñ`éð?"òo ÈÒ„ 6žÁPs…t;D{:¯Z­ÆîÝ»ùÒ¼%öÃf6À`B‚yœˆÈß0 Kcÿ‘ê‡åB‰t{Q”#РòÞCéA0Þ]þ‚bS ‹åE8#ˆ8Fxìsý�.'@Œ¥éôLž ·zG&Úl(‡,6BÈä'ÊE‚`ÿ[`0 Ñh¼Ú¶ÞÞ^KÁØÐÐP¼ð  ãG4O<º÷˜Û¤^ÞX“½ŸØÈ†%¢@#ŠP¨Õ 1 àJKKÑÐØ‹yÁÚ—Ùoïfèê¥Y›CäÀ߬b …föào¨é ÈrŒ è1¢¨¨¦žAXz† \¶eR¯u¾Œ|Q”€§îcFM¯�ËåøI¥Õ^ÇyÈ‚ÁÆ3^O¬çm@¶··EEE0íuiCCC±{÷î MæM#"""òF€m‚€ÄM›š•åö8²Î`0ÀÐü-ûe-%õcÈøµtû©ûh¡™ñÌýÁøçòAöK‹­ �Åg²a8Fäá˜�c²¬·¯ºÔ�}ê¾ ö9šv¡Áö@ÿÁÊ‘r!bÆêIgÉ:²cû|~~þ”·éèÑ£R06mÚ4áïTûùž¥éñã>ÞÒÖ…«m]ìÙ¯c©iêÀ�ž6(Í9dÉó�ß�‹Ræ[l(89͘¸PàÑÌ |Ú`¿,y°ñ ²#ˆ8FŒîs×G‚±ËÕAX¬bŸ£™±X„{â¬#¥ ¼Ì’5 S~mii)*++¥ÛÛ·o÷*¸KDþëÇÏ®÷ñý%_aÉy·Át—> "š*þš'�†®ÕHË�iYŸŽfÖºŒ‘¡Hìn‡8ÔÏFáAÄ1‰µ£Ác{Í„Çt#ãºÍlðj]F£½½½“~]EENž<)Ý^µjƒ±DDD0ø‹žÜˆCý»Û¥Û9 xI$ͬÐ`÷Ä�ܶ¶_a£pŒ âáø¼·¯ºd¤'G±ÏÑÌZ¬ BÈðutbÿغÛ&½¥R)-O6K¶ªª ‡–nçææbÇŽübˆˆˆ(`0 Knœ3!î‰kÔѬXºÐ)çöU6Ç"Ž>oNûÍŽœ#‡ Ö¶É×b ‘–'SËÕ`0 ¸¸Xº­V«Œ%""¢€Ã€,¹øuÞ”–Y£ŽfK²Ó|!¶>3„cLjaÎ%Táìs4;âB½{½s@v¢²½½½(**B__Ÿ}ââ°{÷n„……ñ !""¢€ÂI½ˆˆˆˆ|˜­kärñä(¶ÍutûdzÞ–,˜H†ìè`lhh(^xác‰hF¤&ú×Ôüü|hµZÜ|ýuÄðë# L³ 7b§´¬ 㥑4;œk"Š=ílŽD#ˆæPh°8Òç'?‘žR©Dh¨=Ͷ¯¯&“iÜçÿìg?ƒÑh”nïÞ½�†_MÈšìdiÙÜm¹ëóS“"ýêó©T*èt:¤ˆæ×Mäú\pÅhôxµ3dÉ�ó¥ qLJ Y;ðs ì Y&ü:ÅýOcyZV¤3mŒcqŒàA4—‚"¡¸ÿilÌS!39¥¥¥hhh`/[ R©<¾®¸¸Ø%‹vûöísŒ#5zûE<I¾Ýßæ»èp…´\v®Û –¸=ÇÜmÁ—u׸óѬ¸ à=xÿ}dffbÏž=®ã8›ˆˆü™,6ñÉÐétl "âA4‡yd±)Ф/†F£qéwõõõ_S^^ŽÊÊJéö† �ŸŸ?÷Ÿ%,ÝÎÌÌÄŽ;|¦¿ !Q" v·£Ï" üP˜ÏSuÅi‹VCa9�éôÚÎÕ¨iê@MSÌ=ì/9ïñy?Þ¹š�EDsŽ% æÁaMúbÈbS ÈyZŸˆ8FÍ%�N‡øä ÈbSØðÂÂÂ'Ýv”-0 (**’îW«Õxá…|®¿É5Ë¥åò "K�W*.U:í_‹–²Q¦YL„¿Ýÿ$¶®ÏòøxJb$~»ÿI®Ì`cÑœc†,Í�N'Õˆ5�Ðh4(..F__ 44/¼ðÂÂ|o–HùÂl ÎKY²EGEìÞ$@à Jh’ mÀÑß�L'D«!_˜Í†™1J¼õJömYŽÓu­¸ÚÖ…èp%Öd/BnFˆˆ|çw›€ˆˆˆˆˆf‚F£‘²MMM¨¨¨€Ñh`ÆîÞ½*•Êg·_¹l úÏ–N)(û×ß ãJ4Aõàg¿ÑgÆÊ•PÞ·� 3ÃR“¢°-‰%!ˆÈw1 KDDDDD3B­VKË555°XFf>ß±c‡ÏÏ/ÈC Ì} J YÐgðúQ{=Ù «øýÒØzûíY±•ß�2cåJ(–maíXš´ŠŠ TVV¢ÀÒáDäßXC–ˆˆˆˆˆf„N§“–�ƒ±Û·oG^^ž0E$Bùà_Aˆ¹Ü¹¬Øû¶=û‘h´ª+ÀÞwÁX;!"Še[Áš4y&“ z½W˜ÙD�²Dä×,Jp¦^ŽKŸc÷îÝl"âA4GlÝmÔ—£´Q�ÌŒTlÞ¼�˜˜ˆ¶¶‘±V­Z…üü|¿úlBH”˶`à›�aë¸0u¯´j`C>XÆ€Pu(ÿÐGV,Èd#X»~Z'P«¿ªG÷×uצm}k²áÓ¹ÓÑœa@–ˆüûàÏl€É ˜ØDÄ1‚hN‰ƒý°™ 0˜��`û…xUUU¸yó&ÁœR«Õرc‡_~>AåÒ§`íhÀ@}9`é`¾½~P'ˆxø«îã¾0Ÿôö•ߨ±¦ÎQ*£ Ð@Ÿ9+ý�ˆˆü²DDDDD4í Š‹‹¥`,( ¿ÿ\²øL„Äh0tõ�°¿†8d/Å`lPü[{9ƒ¼ÅÀªû ¯NÜý» ¨¼T\™°ËA�+!S?yÊw¦5+v>[šïÓë#"-€F¡P«=ÖÌg@6À•––¢¡±ó‚µ¬YDD#ˆæPQQL=ƒ°ô A¹l „ÖÐÐŠŠŠÐ××çrÿõë×¢¿ òg¬†<å;n�YS'P~ÁþO� "?[@Þb@Åyœüž©Ó^– ¢N„±Ý„uªË@ìŒùñ³kØDäW’l$nڄЬ,·Ç� pƒ†æoØ/k!"âA4wôz=�æ…ÖÖV BCCa³Ù`±XÐ×׃Áà1SÄû›s`Özã"[ÎK¥ {ÖìÑßGoÎê43gýí·Òp&lÕç’®±PF!8u9d îc –ˆˆ&„Y"""""šVŽ`,ìÞ½eee¨®®€Y ÈÎ&A¹úÈÕÀzû*¬×ë`ëh�²f{pÖØnÏœ UÚƒ³Z�}R0h}‡ÞhÂê @½Á½ÁÈw®DP|&d ³!‹MaÃѤ0 KDDDDDÓBE—š±Û·o‡F£�F£q È2Yl d±)‡úam¿k»¢Ùàœí³¨ºbϺtЪ]  Õêx Œ‰–3®·0v _¯Úƒ±®Ükà 1È´�/ÌfÑ”1 KDDDDD^³^¯u ÆnÚ´ ùùù­V+Ý?_JwòÈfK�;kG¬m °Þ¾êRÖ@j£k@ÐQâ@«±gв­÷L�öà«¡Ížý:RvÊ({�=1²øL6"M d‰ˆˆˆˆÈ+C×ë`½qQº…‚‚é¶s‰£Ñ8/ÛH?гu·ÁÖÕÛí«ch�K8¨D„)èRìZU4³iGsd½šîذõW�^‹§àëÁØálPl ‚"9á)Íd‰ˆˆˆˆhÊlÝm¼ô±t[E$%%¹<',, jµZ ÆÖ××C§ÓÍÛ6 Šô gÏŠý�°uß„õV lfÄîv�¯sG_Zª¡I ŠcϪ†ƒ¶˜Ykê´ÿëíŒí@‡Y„©S€¡}¬š¯cg  ŠÑ@—Š  'å""¢YÁ€,M‰­» – %.÷9—-p¦Ñh¤€¬^¯Ÿ×ÙÑ„�(ÈB¢\.‰·Þ¾*eЊý�³hú,Âp�vìÀ£spÖ¸•î�vÎlrX�‘ÌV`$Ð:ú9ZpÔÿcPFÙÛz8–“q‘¿ÈÏχV«ÅÍ×_G ›ƒ( 0 KDDDDD“&õc æà˜¬J XÇ|¾V«Eee%€ÀŸØk:8& vºÏzû*`µÀÖy¶î6ˆƒýïL¬„k@SðjÛ¥îÆs©€É˜Úk…h5„à{&rT S2øJ~M¥RA¥R�ùÛDþ£@€N£1áá.å›d‰ÈÏ)îËÓ"°"�3]Ç¢Ù"õÃr¡bÿöÙ烗>ˆ"6æ©�™ëöÖ‘õž#¨8zr)±¿¶>3Äžvˆ}öÀíðýãeÖNÕă¬ÂÌ4Äp¦«£ME(„ðE&Λ’A‘‰PÜÿô˜ý�ˆˆæÖMïÀûï#33{öìqyœY"òû“øäèt*6qŒ š%ƒú“.uNƒï}²{ÀU“¾š¤0·×8dM&L&T*öÍéà(y€á€m°‡ç8‚¶ŽeÑiÙq?¬–1ë×ÎèöG$2% (4F ¶ NËÎ÷ ÈC ‹M³¿‘oc@–ˆˆˆˆˆ&là›�a½Q'Ý^ò¸[ÆæX233ÑÐÐÀ^¶€ÙÙ#m§�£\Â]ßs¸l¹b@6ÀmÞ¼ ­·q¬Ê„ Hþ""ŽDsi×®]8×Ô‰óÍÝ÷ÙªÛÑÙ;€”„H¤&1‹-P ]¯s ÆÊÔË _˜=á×ët:—€l^^û›b�•ˆˆÈ; È8�Fƒ~… ²–ðyßï–_ÂóožDL¸ß¼½ 1JŸÞÞ–›�¨iê@áÊ —û�”_Æ¡²jT7vR#±µ û¶<Èž8FpŒàáãt:¾0AvÇŸÇÜmÁ�_ŸÇ‘òË0÷X¤ûc•xñ‰¥^íw§k[ñ§ûŽcß–åÒzÝ{ _Ö]Cï‰güs}Y×êÖ¿¦ËéÚV*«FÙÙ&©Ÿ®ÌÀÞ§—ûôØ3t½ƒ—>–nËdC¡}xÒwêëëQXXÈþFDDDóN›€æ‹Ÿ–Õ :\s�eç}z[«Û±äÙwQÓÔárÿÁÕxîÍrˆ"°oËr¼¶s5R#±¿ä<6ø˜_2ÇŽ4«ûáªWJqðD5rÒUØ·e9~þòzìÛ²Qá ì/9�çß,÷ËÏöÜ›å8x¢zFÖí4Ÿ®m•úéšìd Õ�¨nÙêÆv|t®_ÖµÂÜm‘._4w»¤9;R~_ֵ޹>Ç{¶ÜìÄGçñѹF´Üìty^ËÍNÔg½]më’Þót­}½Û ²ÜÖí¸ï˺kü²‰8FpŒ ÷nù%T7v`ëú¬1O^¼õrH—å�öe]«Ô‡FïçSQÝØ.õ±»­Ï¹ÿŽ~î—u­èì@gï€ÇÇG÷gO}¾åf§Ô×ïeî¶ åf'ZÚºP¸"í4Á†éÒk}�­§}$+WBqïŸM)ëà€ÕëõìPDDwQQQ�¢¢"PÃæ Ì�¥yὓõHIŒ”2ÕÕàЉ¼õJ�ÇÀÉÓÿú‰ÜìµðönYŽWß9ƒßîks’¥Çö—|…C'j\jç¥&Fâ×{sÉD{õ�3€­YxþÍ“.ïùÒ¹xmçjÀ‘“—±¿äüðïe¼[~YzÏÞ/z>ðkëâ—LÄ1‚cÍG�õÅ'–ŽùœÔ¤(\z{›Û_Õ�íøÛ_üÁ¥�ÞÏ'c2ëóÔG?÷ѽ#eÝëZöìl#žó¤[½Üÿ»ó».�iG?ýùËë¥þ¼4=gßÜ [ ²°4=~äÀÏÃ�Ú¡áì¾ÂáìšùÄr¡gêå¸ôY0vïÞÍžvŒØ°"ks’9FpŒð �k’úËxFc[nvâ±}¿�?y= Wd ¥­SÚÏEQÄ�Ÿ]3áí0w[ðô�O`î±àµ�«±u}îôXÆ\ßcÕ�xmçj®H—úÒÁÕHIŒÄKOäâ·ûŸ”úók;W#51R+6øKÓãñó-ëQ¸2egñ£wÎHŽžìoßù^,\ŠÔ¤(D‡+Æý,~m?Ѳ&;Ùg¾gQ!ÂÈ�CfÁ¸ÁX[wõå(mT 3#›7oöø<µZ--3 K45íoDDä›X²€Þ»å—ÝŽ Ê‘“—ÝËÎ6aMö"üøÙ5HMŠBjRÞz¥k²¹<•œGJb$J÷>.”®Ì@éÞÇaMâ8`ÜV°©IQ(\™�}ÃA ÇÁmjR”\IMŠÄÚœäq³ež³-m]Òß¼û1j6ÀÔÚÄK)àÇÇåÏ#8FøŠ»=9x¢Úe?�‰P"7#G÷=Ž¥éñ8TV3©Kö�œ¼Œ–¶.ìÛò ^z"1J—>y¨¬FÊ/;Û(•Yxé‰\¤&E!&B‰·^)@t¸BÊš]›“Œèp¢ÃX›“,õ›Wß9ƒèpJ÷>&^ WfàÓýOJ��öÌz~üì¼ôDî¸u©ž¨ÆéÚVéÄ‹¯pÆk _˜=îóÅÁ~ØÌš¿7ЪÓé¤e£ÑˆÞÞ^v(¢Išh#""ßÄ€,4{Æ&lX‘îˆØºÞ^OñШ`HÙpPÔÓd£³^N×¶ÂÜcAáŠt·`ˆã`îÌE÷z�kGe¾D‡OýÒÄçß,ǻ嗱u}Ö¤2ŠˆÈwÇçlWŽäËÆš\n<5Ãõ�='·×:vô³‰(;ÛèÒg=õIÇÉ Ç{oõPgùzɳ8ºïñqÇŠ–¶.¬ÉNv;±‘š…5Ù‹ÐÒÖåL=.x²¿ä+¼úÎ,M�—êîú Q¥eY|æ´®›Y²DDDÈBhD÷$'{œÀ”% \ii)[`1 X[àuÍ/ã8¨«mêÀŸî;îòXL¸-m]8]Û*e£8fUö$q<÷PY •y.­^ÝØávßtd¨9×ÀÛº>Ëc�K"Žþ9FÜípŽþ­¨¨¦žAXz† \¶Åo?Gt¸bJÙ/ë®!e¸ÀX}èN�eÂëëìµoâ¿|gÌçÔ4u pe†”;º¯N„£snFü˜ÛþeÝ5´´uMêï¼ã¤ÉšìE(Ýû¸ÏÕ�ufíh€\ýÀ´­O«ÕÂh4°Oìåœ5ËþFDDDþ. À6A@â¦MÍrO`@6À öËXûe-ó͡ՈW %1Ò%ËR#an²àHùå ]8úÑ�qÔpœ--7;ñô¿~‚êÆ—‰F(°õööâäÉ“X¿~=ÂÂÂ8FpŒàá§¥tšìd|t®Éå„…'ϽQŽ;½¼öÿ­–ê¨vN!�;–¨0{Ù„ß— ð$uTøN�eÚŸŽàôDË8øËIç’C×j¦5 ëœ)R__�ÂÂBö7"""š7�¥€uº¶-m]ãè,Üò6Žœ¼Œ}[–»Ôelië�ì¶¾±x:ý²®U:Pœ.æn þtßq´´uáç/¯·–ù—�ÉdÂ矎ݻw{¼ä�8FpŒ ÙR¸2�k¡²ê1²-7;qääe¤$FJY£ŽLRs·{PÔQZ`¬ Úñ¤:½‡óû_mï’NެÍI3‹õGo‰ÎÞü|Œ’޾º¶û<$ZÖ6Û³Ý'’ánL.橊/»Û!öwB™žZÔ£ëÈÍ'¬!Këˆc¢ž•cÏ*î¨9瘸§peR#q ä¼4 ˆã î½“õ.¯]›“,ÍÄ>ºfÜéÚV<º÷8�¨™ÖÏôØßÛ-¥{c ež1™L€¾¾>±ÞÇŽ4§¶,ÁÒôx”�mr«µ Œd€�&¦sô!8TVíö|©O®È˜ðv8²Ï÷—œw{ìùÿ<‰G÷—²×× ×pw¼�ó{¿w²Þ­„ˆ£ÄD(±&{¾¬»†êÆv·þ\ÝØ� +Ò'´ÍÏÿg9ª;ðó—×ûE0ÖùêkGô­W¥R!44TúÛæø;GDDD40C–’¹Û‚�Î5!%1rÜ 5^z"‡Êjp¤ü²tYï�w®ÆæŸàÞgßÅÖ‚,Üé± ìlDˆn¯w½ÅLYŽDD3/8‚\ qȱÿlÝmŠHôzµaaaˆ‹‹Ã­[·ØëÈêt:¶7Ñ(*• *• !l "¿Ñ  @§Ñˆ˜ðp·cud‰F)Ýû8•UãÝò˸ÚÖ`¤ÞãTjÛÑÌRÜÿ4–§E`EúÔëÙ™L&=zÔ¯>7ƒ²#höÆ"‚â3a½QÀ>¹—§²A‘‰PÜÿ46æ©�™;¡õêt:TVVŒF#²Dí“SèoDD4{nxÞ™™™Ø³g�Ëã È�¡ôßoó�,6ñÉ1ÐéTS^GGG‡ß|ÞHõÈåµ ÊrŒ Ù#ˆ�%Ždmß²‚<²ØhÒB“6¡õj4) [__�‚‚66ÑL¥¿‘ï`@–ˆÈIH¬ —GúìöE,P¸ÜfP–ˆˆfƒ,>sFÊ8Oìe4ÙÐDDD4/0 à6oÞŒ†ÖÛ8VeBPd"„è.‚‚· §¯ó&(Ë1‚hvíÚµ çš:q¾¹›�Aþ÷7re &Ëùï”ÉdBoo/ÂÂÂØßˆˆˆ(°W± ›F£�&}1d±)ä,N¨AYY°RPÖ`0pŒ ò!:�ñÉ�Ŧ°1ÈïÈ3¥e[Ç·Ó¶ÞÌÌ‘õêõzö7""" x Ȉ©e‰ˆˆ&ÂQ¶€T¶`:8gÉòoÍ È�±‚²½½½l""òþà!~$›uèZÍ´¬S§ÓIËõõõld""" üßTl"¢À'‡jÉHý½¾¾>”——³aˆˆÈk3Q¶À9C¶¡¡��LD4JEEŠŠŠp@ ›ƒ( 0 KD`nÛ‡¶šé¶Z­FAA†ˆˆ¼&‹Ï”Qìe ¬ÞPU*âââ¤Û,[@DäÊd2A¯×ã*3›ƒ( 0 KD~Ír¡g޽�¢¢"6ìÁXã:¥Ûjµ»wïž¶«‰8F‘,a±´lm ÈÚºÛ`¹P‚Ò_Diié¤ÖÉ:²D“ãM#"¢¹'g‘_ÿ5`2&6ƒ±D#ˆfçbÑRX�À%CVì‡Íl€Á „O.ïC£Ñ ºº ×둟ŸÏ†&‡7ý�ˆˆæGn"¢À`,ÍÚDD¢T¶C–i)[ Õj¥efÈ‘¿  EÜ“œìr%�3d\ii)[`1 X[`ÿMDÓ¦ïÖlƒ¶Y{¿ð$…Û}Þc9FÍ®¢¢"˜zaé‚rÙ6ù-YÂâ‘,Ù¶{mY/èt:iÙh4²¿‘_K°M�¸iB³²Üg@6À ší3àŠƒýl¢ití«N˜.÷Íê{†ÄÊ�Y/Ýö63–cÑìÒëõl c•-ð†Z­–‚±õõõ.AZö7""" $,Y@D4E}·†fý=ûo[¥e–) "¢9;ˆ˜�²Ηó1˜JDDD�Œ²DDÓ@­V#44tÆÖßÐàz Ë`,Í5·² ³½ZŸV«Eee%Ö‘%""¢ÀÆ€,Ñ4Ø´i“×—VŽçù矗–Œ%""Ÿ8�U¶ÀÛ€,3d‰ˆˆh¾`É""?Ã`,ùÄ�Ĩ²¶;­^­O£ÑHW›ôõõÁd2±‘‰ˆˆ(0G± ˆˆüƒ±DD4×äNY±VSã´üms`Ù""" T Èù!c‰ˆÈÈ-•–E/3d¸”ÿ©¯¯gÈÏÏÇ®]»ð €\6Q`ü†bùc‰ˆÈW!Q" v·OËúœëÈ2C–ˆÈN¥RA¥R!„MAä7ú´è4îò`@–ˆüœâþ§±<-+Ò£úsªÕj�Fc‰8Fùù¢¥Ô—„èdlzêId&ÇNi]Z­VZnhh`ã�!(2ŠûŸÆÆ<Õ”ûÍœ›Þ€÷ßGff&öìÙãúû‰MD£ÉbS0tÇhÿ!Ü!b±ŠmB3¯µS”–…ˆ„Ií¯ñÉ1Ðé{Gݽ{7 4Íœc9FÇ"régñ™R@V¼ÓŠ„…ÉÐh¦Öç‡[·n€ô·�ˆ\ òÈbS I_MOÔùÖ�%"ŸÐ7(:Ù)Ù Pu:3c‰cÄ<#„à§v¸3Ьhít:`ˆL.[àpåR­Wïí\GV¯×óË ""¢€Ã Ù·yóf4´ÞƱ*Ó]LK?ª¡Òò­>¶!Í8F͇1b×®]8×Ô‰óÍÝ~MPD"lWÆ;6ä,�q‡ ×;à”•.¿{ÕBç² —j�?]7å÷Öh4¨¬¬à]Ù©ô7"""¢ÙÀ€l€Óh4èW¨ k Ÿø�_ÌÈeaßšlxàG3¯æ†MZ–Ŧ°A8Fä¡Óéðí€ ²;æ ¿F�‘–¯°ÏÑ,©uêsAQIw}¾sÙ‚o/ס··wÊWu¨ÕjiÙ› Ù©ô7"""¢ÙÀ’ä¾SD$B�Û/½ÕçZ·�h6üd‰Z6Ç"ŽŽÏë€þöÖ¨ò D3 oPĵ.§q?æî5\G—-¨ªªšòû;—,0™Lèííå—BDDD�u\Í& �;F|¦´|ªÑÆ¡uÅdCÿÐð e‚"Ù(#ˆ8F 誽Á€,Í,çq]ˆVO¨d`/[ààM@23GþÎxS¶€ˆˆˆÈ'�©Ùä‰la¶´|Þhc�HšQÿUeÙ÷³A8FqŒÅ9ÐõáE+³diÆô Šø¢Éæqß»ëß§“uÕÕÕ^e¶j4#Y¹œØ‹(09Ÿx±—äñ-Γ:�Is¡¢¢EEE8 †»Q@`@–<ÿ ŽM�=R¿ëØÅ!6 ͈/šF‚y‚\‰àŒÕlŽD#F‘«”Q€þ!f¦ÓÌ9Õèš‘.w:w7BH”ËßÇÄ\Sáü¨¯¯çC€œû¹sðÓÜêƒ4ÆÅÅM¹&öt1™LÐëõ¸ €U±‰²4&çƒÞÚ®ÙDÓ¡µSÄ'õN™oê&|Y¤ƒåB Î{EEElPŽÄ1" Çç>÷iƒ�¥ hÚ}e°áÓ†‘±\¡+˜ÔëmÝmÀàHVlEEÅ”·Å¹Ž¬Ñhä—Cä¡¿Y.” ô—QZZê—Ÿ!>>^Zþ¤Þ·®þø¯ª‘dƒ¹ÎŽ%¢ÀÄ€,�I›™z™tûØE+þhÚô ŠøIÅ�tæYˆH€<å;“ÿ1j6ÀÔÚÄË9FLj€#ä ³]²ß«â¤z4mZ;E|xqäHPüb—!öCì½%Ý6�0™LSÚ•J…ÐÐPûxÐ×Ç:²Dú›Íl€¡ù[¿í«V­B\\{6ê/þèAÙc­¸bÙŽÂÂBîpD4í�¥q)´»üýâ�CøÊÀ,8òþ ïŸÊ�-r%”Kÿ|Ò™oÄ1‚8FÌ7Êܧ¤ ¾ú‡€ŸT ùdÝ=ò/µ7ÜO€(î}|ÊëÅ‘@†7“{iµZi™Y¢À†^xAº}Ådÿûÿiƒ WLâ¬g¯˜ìW{½öÅ ËU_6l`†,MI�(âžäd�㈜MØJKKÑÐØ‹yÁÚ‚)ÍL­Ì} – %»ÛÿUmEk§ˆ?Õ!4X`#Ó¤|e°áË֑út˶@‰bãpŒ ø1¢¨¨¦žAXz† \¶eÒ¯ä!PÜûg¸PqÈ‚þ!à`¥ïJçyvš¼/šl8æ”;'@ad쯨¨@AAÁ”Ö£ÑhP]] `jYoûÍ<�FƒíÛ·ãðáÃì'�ËÍ¥ÜÜ\fÇÑ”%Ø&HÜ´ ¡YYn�3 à ûe,€ý²–)ý¨–‡@¹l‹KÀå‹&joØð˜N†åjÒÝ]1Ùð[½ÍåòA®DpÎÆ)‰cqŒðGÓQ:!("Še[`©þ°ØgA9vÑŠ/­øË<«Øçèîjoˆ8vqHš4pdÆþÙ´žq”-P©T“~­s†ìTúËù‡üü|h4ûDÍèÐÐPìØ±yyyürˆhÆ0 K⸠êOÂz£€}æÉ÷ª¬ø¤ÞŠ{T–.B\˜€ä(fÄ‘}ÿ¸Õ+¢ö† 6\ëµO ô1Ë1‚8FpŒ˜¼ ˆD„¬Øár"äVŸ=[6.ÔŠœAX¬�„¸P¶Ù÷�Ö;ö“µ7l.�X¢Õö’ÓT$<<===ìe ¦’%ë|y'ö" l�ÿðÿ€ªª* Ô××Ãh4¢¯¯oVÞ?33�ñññÈËË›ÒI$"¢É`@–&Ì~™äãŠMÁ`ã)+çVpË(â¼ÑÊF¢‰ Á«œ±¶î6XÛô°Þ¾ X-Ò¥“4Ï)£ „D!(4²ÄLÈâ3Ù&#8FLjìs íÀöaXo_…µ][WÄþN)[�Øç„�(E&B– …,6eFßN&“!77Wš”Ë›²•••ì²S� ŒˆˆˆÈ—0 K^ ŠHDPD"‚ÙDÄ1‚hÎÉbSf<ØF4yyyR@vªe œ'öbY""" ˜ãd6M·¼¼<„†Úg•3�0 “^‡óÄ^&“ &“‰ KDóN~~>víÚ…gä²9(Àí9u ¿¬­E§%°Ëp1 KDDDDDÓ.,, yyyÒ튊Š)­'3s¤¤ÉT‚ºDDþN¥RA§Ó!@4›ƒœ±« ÿ\Y‰¥‡ã¹Ï>ÃgÍÍ~ù9ú\peŒ“Ò,Y@D~MqÿÓXž�霈ˆ8FÍ¥ ÈD(îóTÈLŽ`Ï’uÔ€­®®ÆæÍ›'½^�N‡††ö€¬s�—ˆým¤¿šÏš›ñYs3Ô‘‘x$- ?ÈΆ:2Ò/¶ý&€÷àý÷‘™™‰={ö¸Žãüz‰ÈŸÉbSŸœÁY—‰ˆcÑä!�Ŧ@“¾X*5à\¶Àd2y]¶ ¾¾ž M4F#" TÆ®.ü²¶«KJðtY>Ðëý¾¤²DDDDD4c¼-[àlrdÊQ`úß«Vá/´ZD)?{ý:öœ:…Õ%%Øsê¾éèðËÏÉ’nóæÍhh½�cU&E&²AˆˆcÑÚµkÎ5uâ|s7ƒæ o˨T*ÄÅÅáÖ­[ìe &’ÈþFDDäî��Ç¿¯[À^²àƒúz|ÖÒâö¼Î�| ×ã½êÈHü ;ÛÈU*ýâs2 à4 ú*ÈZÂÙDÄ1‚hŽét:|;`‚쎙�Aó†£lA__ŸT¶`²—Xk4šIdÙ߈ˆˆüÛ#iix$- � >knÆ/ëêð�Éäö<ÇD`ÿ\Y‰GÒÒðZ-IKóéÏÆ’DDDDD4£¼-[à\Z¯×³A‰ˆˆæ‘(¥¡Óáã?ÿsœÙ²÷ªTc>÷³æf<÷ÙgX]R‚®¬„±«Ë'?²DDDDD4£œ²ÕÕÕ“~½Z­––§21ud¤K}ÙGRS=Ö›õõ‰ÀX²€ˆˆˆˆˆf”·e œ3d�F#z{{Ɔ%¢y¡¢¢•••è°tøÙý 'o=úè¸õfÏ^¿Ž³×¯#J¡À#iiøAv6î��ŸÓíf@–ˆˆˆˆˆfœóä^åå娱cǤ^¯V«a4سd�ƒ´Dôz= ƒtâf¶¨T*ÄÇÇC«ÕB­VûÜ “É$•kIánBä‘s½YÇd_£ëÍúÒD` È‘_³\(Á™z9.}ŒÝ»w³Aˆˆcѱu·aP_ŽÒF23R±yóf—Ç ¤€lUUդׯÕj¥€¬^¯g@–ØßÆéoþÆd2¡¸¸Ø'jD«T*lß¾�c ‘ŸŠR*ñƒœü 'ßttཟ57ÃØÝíò<ç‰À>~ê©YϘe@–ˆüûǨٓ0±)ˆˆcÑœûa3`0!ÁîSUh4ÄÅÅáÖ­[èëëCUU•KmÙ»q.qÀ:²Äþ6~ó'UUU(..F__ŸOl�ÉdÂë¯¿Ž‚‚lÚ´‰;‘»7>ÿ;>ÿ;?_ Ξ½~Ýcæìlc@–ˆˆˆˆˆfE^^Nž< ^d}!‹Žˆ¼çÈŒuÆ®M  BrÔìmÇ­^ÀÔ+â‹&ú‡ì÷•——C¥R¡  €_Qpg¿éèÀ øem팾_�(B¡V{¬›Ï€l€+--ECc ,æk ‘ÈF!"ŽDs¤¨¨¦žAXz† \¶… BóN~~¾K@v24�41˜cr0•JÅþFäÇ~úÓŸJÁØEQžÉ“!9J˜ý JÖeá½*+ênŠ€²²2äåå�;Ö‘ï3vuá—uuöÒ]]³òžI¶ 7mBhV–ÛãAüZ›Á`€¡ù[ØÌˆƒýl"âA4‡ôz=L­M°™y¹5ÍO޲¤²“¡V«¥åúúzö7"?VQQ!Õ…‘;—Ëç&ë$4XÀÎår,ÞŽ¾¾>”––òË"òCŽÉ½ÿïÿÆê’ü²¶Öc0våÂ…PGFÎúö1C–ˆˆˆˆˆf�7e t:°Ž,‘¿sîÃ¥!.Ôw¶í©û‚p°Ò À^V�ˆüÇgÍÍø¬¹ŒSÞ(J¡À_hµøANΜcd‰ˆˆˆˆhyS¶@«ÕJË Èù7ç>œ/øÔ¶-V°dY¼Dä»&Z’à^• ?ÈÎÆ_èts¾Í ÈѬq”-¸uë–T¶`¢Y²Î“b82e‰È?9÷a{Ô·Ü|{˾\__�pˆÈÝž/¾7¥Pà‘´4ü ;÷ÆÇûÌv³†,Í*çìd²dä´ÀÝëÈQ`9{íŒÝÝÒí±‚±êˆüûC´5R IDATáÌ–-ø÷uë|* 0C–ˆˆˆˆˆf™7e t:*++íaF#³Öˆˆˆœ±« üÿìÝTÓwž?úgø‘ &ˆ àˆŠ-‰¿¥¦?,3SSéévzˆÛκ"ÚÎtzçÂÜ�Ù3ý3û�éÙQ{ÎtîÝN»;söH»Ýq¦ŠÚîµcضë]h-Ø©±Rh5¡U41T05�(„Éý#$$$@ø™_ÏÇ9cò $/òù�<óÊëm0à-ƒaÒnXØ¡P`‡B�-ÙÙ}ŸÈÑ‚?¶ ¹¹………!_×ÈrŽ,ŃÂÂB( ܨ©A:ËAqij8שÎÎI·Ë‹ñ܆ Ø¡P M$ŠŠûÆ@–ˆˆˆˆˆÜ£�>ŠãÇ�pwɆÈæææzO&YA™ˆ(VH¥RH¥R¤°< t½¥×£oppÒm[±;”J<¶reÄÝ�ÝúL&¤/^ì7` KDQNxï.Ü¿RŒóÒX "â1‚(Œ$YÞ» OH‘Ÿ³tÊí ¼�lkk+îܹƒÔÔÔ)¯ç;¢Àb±„|=¢xÞ߈ˆ¢EUc#ÎvuMxy®XŒJ%v(È•H"ö~ÜpÞ|ùùù¨ªªò»œ�,EµÄ¥Ë‘‘“¥RÊb�Da$HJAâÒå�çÉ _6u@*•J‘›› “É`z]²ùùùÞÚ ƒß"aDÜ߈ˆbÏ™Ì=6FfÇ'ðWJDDDDDáàÀNgq/ß�ýqŽ,QlJ ñÜúõ8SZŠ£MÌ„±;dc^II :®ß‰ $H²X"â1‚(Œ***ðÑ•>|Üig1ˆ0»± ½^�FÃý�ˆˆ(F¬•JñÜúõ1ÀŽÇ@6ÆÉår ¥H¼º˜Å "#ˆÂL©TâËA {­,f>¶À·CÖs]îoDDDÑï€Zѳaç Y""""" ›ÂÂBo—l¨�¬T*Å¢E‹Ðßß�þþ~�Æ€Õ‹‰ˆˆ(úLÆ~~ó&ÎvuálWúŽ€ëlÉÎÆ™,jÂ\²DDDDD63[ P(ÐÚÚ d‰ˆˆbÔ[^Öé`²Ù&Þ¨« o Ü‹=¯RaKvvDß/.êEDDDDDaã[àêâ^¾¬aôEQ,jnnFuu5Þð)ËAq¢Ïá@Ù©S¨jlœ<ŒçlWvÕ×ãõ¶¶ˆ¾ d‰ˆˆˆˆ(¬|Ç„È* ïi£ÑÈ"Q̲X,0 ¸€S±)^쪯ǩÎÎ /_+•"W,žðò[ZðbssÄÞ?Ž, ¢¨æ8_‡3ú$\<•ŒÊÊJ„ˆxŒ §½C†Ó8vYˆüU+PRRòuÇ�-°X,�J¥“^Gé³òòD {q#ZXÛ·oGqq1Š‹‹‘á§Èõbs3>·XüÎÛ¡P`‡BtAŸÃ�³]]x½­ g»º¼ç¿ÞÞŽÇV®ŒÈñì�%¢è~2j5Ârý ?ªHD¥R‰}ûö¡±±{÷îÅÖ­[½—566z«©©a×l3Ùlès8f~}»=¢ïY"""""г[ÀY"¢è¢ÑhP]]�“'O¢¢¢Â;ÚÀf³áÈ‘#ì–�C[d2ïéS��3þ>¾×];Å"¡áÀ@–ˆˆˆˆˆ"ÆtÇ( ï鎎�ˆ( eggc÷îݨ¨¨ÀîÝ»Y�8æ»�×ËçÏϨKö¬ÙŒ·|÷œÍâ`ó…�,EŒñc ¦ê’MMMÅ]wÝåý?»d‰(Ö¢¢¢{lŠÁûg6›QWW‡íÛ·cÏž=8räˆ÷2q„/ÌDsÏ7<5ÙlØU_“ÍòõOuv¢ìÔ)ïÿsÅblÉÎŽ¸û™Ä_5E’ÂÂB´¶¶ZZZ Ñh&Ý^.—ã믿 ¿1DDÑN*•B*•"%†î“ÍfCSSÑØØèw™X,†F£Aii)²#0H£ù•+‘à¹õëñz{;às‹Oüçb‡R‰ÇV¬˜0\=ÕÙ‰· †€1Ôê°Ü�ÝúL&¤/^ðÜ„�,E5á½»pÿJ1ÌKc1ˆˆÇ¢0J�dAxï.9 ×hN°…#·®!å�gÊÎÇù:œÑ'áâ©dTVV² qFz:0Øv"ø…à w4Àeë†pí,�4†®|ˆá+N¸Ï ~rÉùÛ�Ä7BæœÓÞ�!Ãi»,Dþª“§Ó1~l�^¯Ÿ°ÓG©TzY£ÑÉZ²Äý�h!Íu [VVÆ@v”J%<èiP__ :{½­ /޾!;]}ƒƒ¨jj‚ÉnÇóúøa K4Kƒßñ c].�÷_ï ÀÏß�hs) 6×OF­FX¬?”H‘È54àîÒó=/È1bä«vŒdæ#13ŸEã1‚fóû¶Ýð c].—ßåž}n¨£Á=6D²ŒE›ãcžÓj„Ñ ¤$ÏíÚÁ=ô---² …Â{Ú`0@µöQþbˆûŵZ µZ�ÊÊJ466¢®®ÎÛeí k³³³±k×.ìÞ½;ânÿ[z}@›+c‡Réž+ú]Ö78ˆ³f3NuvÂd·{ÏY§ÃZ©�­\q÷‘�,Ñ,_ø9o~á÷ÂÏóbo|àâ´1ÒÓÁÀ…(Ž }Ñà÷‘éñÇ_ƒßÁ¢ÌŸ²hD³0ØÑà·¿ùîk.—Ëoºò!D¿Ë¢E‰ÂÂBo {áÂ… ·ó]ðËd2�=UDD ëܹs,B 6oÖ3cÖl6£©©)âÙ>‡Ã/ŒM q@­ž2T}låJüª°0 ³öÅ––°²)ä.„¹¹A$e ãŽ;†ŽËWá°"YQ„q‹2‡œV£ßÿÇ,¡¬½›�,ñOÇ[`÷¼/¿ÀhØ×@/G›Ä¸êêjXnÁq{˜Ÿš˜®Iþ.�ßÿÆÿ §È&—Ëq×]wá믿F?.\¸€‚‚‚€íRSS‘›› “ÉøóÑ�c8™ûŽììläää ££#àyQ$y½½ÝofìÑâb¬ÍÈùúÏmØ€µR)v�Žk0ÙlxK¯_ðy²Ë<# kçN,Z½:àr²1Îh4ÂØù¥ûEÊÐ 2Çœý½ÓÚ~äÖ5$ç}“…##â„ï8“`Ozï©ÎNïéçUªi…±[²³±C¡À[£Ï½?·DÞ3²D³ HN™Ööì>$Š3I¢ÀUÞ';¦$‰X3¢™þM1XõëšeU¦3¶ eô£ŠÁ>�@DDóK¯×ãÕW_Ecc#÷BS~áŸ^¯GGG¶nÝ ‰D¢Í1›Í†úúzÔ××C¯×û]&“É ÑhPZZ‘µ÷ OŸ[¿~Æß‡�,Q ó XCyŸ°ˆ/üˆâ‰@œåýuHÇ.0D4óý-9HIú&Üçüæ8óMÒ¨êØß…½Æ-,�N‡òòr¿óôz=ÊËËqðàAo(ÛÔÔ„ÚÚZH$8p ";5£QWWªªª¼a¸¯­[·B£Ñ@­VGÅ}É‹‘&šyÃJn„ý\Ž‘h6;PºÜýâþ‹†øþë}!�$Bâ7Ö³hDq$Y~_Ðó=‹ ù†C‰2ˆf+I¶Á{Ú3Äw,ˆo8—Ä}.*ù°uÉ[8ƒˆˆFmm­÷´J¥ÂÖ­[½ÿß¿¿÷´ç£ó6› UUU0›Í,Þ ™ÍfÔ�ÎKõ,Üå!‹QVV†“'O¢ºº:jÂX~sdgÂ4n Àz>>_YY‰ƒB,Ãf³ù¹Z­ëëë±gÏlß¾Z­Öïr•J…½{÷¢±±eeeÈÎÎŽšû–+p²³ U}Çl‰ÀûÏ@–h–—.‡pÃSîY‘Á$‰�”÷M$fò…Q<®yÂÊ#gA´á)¾aC4GDŸ‚ }âIAº¢ O±PQÊ3¶€wlA0Ê^I™ˆˆÆ/õÌ(õ=&ûŽ*ðP©T¨®®8Ÿ&f6›±ÿ~lß¾ûöíó›+‹QZZŠ“'OâàÁƒ~¿ƒhò܆±O<½<ðOWŸÃáwÝ>ãŒ"gÈÍ�ÄÌ|¤HžÅеspÚ»á²!H—#Aœ…$ÙzÎ…$Šc‚äˆ6~#=¹Ù§­®áR– )3Ÿ]zDs½Ï¥,AÊæR wµa¤§N{7÷Ü÷ÄÌ|¿±� ü÷ 6GÖwl�ïØ "¢hTXX…B�55H�‚Û¬S¥RA§ÓyGøž¸;>ÍfsTur†C}}}@7¬B¡@ii)ÔjuL,�¶C¡Àëmm0ÙíxË`À™ ;¦ñFkŸÃ�]õõÞ‘Ï«T9O–�,Ѿ*ŠX" *13Ÿ�òD ù$W¶�ák ¾�ì�;w�ššê·�o Ë…½ˆ(ÚI¥RH¥RDúç©<áªÝn¸Ì³à¢§‹Ö—D"�ÍfCWWÙ‰Åb¨Õj”––ÆÜ§BÒD"Ô>öÊN�‚ÉnGUSN]½Šç7oÆÚŒŒ ¯×çpàÔÕ«x±¹ÙÆîP(ð|˜ŒÐ  ÏdBúâÅ3îÈQTÞ» ÷¯ãÁ¼4ƒˆxŒ £I„÷îÂSRäç,�·Ÿ#—Ë‘›› “Éä[à;[Ö^ø.œè´w#Ág¾0÷7¢ù¡P( ×ëº]e2 ««+à:¶_|)’Èd2TTT@£ÑÄD7l0oéõ0Ùíx,/¯·µNuvâTg'r%äŠÅX›‘�4¡pÖlFßà ßÌX“͆]ã:ŠƒùÕCMöÎÄ ‡àÍ7‘ŸŸ�ªª*¿ËÈQTK\º9éP*¥,ñAF‚¤$.]yž òe©óú³ qüøqÈŽç´1�%îoD ¡´´û÷ïÇ«¯¾Š½{÷zÏW*•‹Å0›Í°ÙlÞ0Ñl6{·�.æD‹Ö¹°Óñ–Á€³A‚{À°šl¶ //Ôí<µ ‰�,E•‚‚o ÛÚÚtl�ï¨ç­k€l= GD4Ï4�ßœÓŠŠ oøªR©ÐÔÔ�NµZ ›Í†ýû÷p‡±\�qjZ­õõõsöý *++YØ0` ãJJJÐqýN\° A®"â1‚(œ***ðÑ•>|Üig1ˆfA*•zÇÁ»dÿöoÿo¾ù&Ài¿Á¢-½^¥R �N­V ­VëíŽõŒ+¨©©Á«¯¾ ½^~Z­fñBÐÕÕ�NÓ÷q­tá?Ùæ°�ÈÆ8¹\Ž¡‰W³DÄcQ˜)•J|9hAb¯•Š𥩯<úè£Þ@ÖeïaÁˆˆÀž={Îó ^ÿ1»4§C&“yO› žÅÖ"ɯ¦C+ÈQÔ elAZÆ7Ðwó+ÀÈ­kH\ºœ…#Ša­—{°iU& B Åb1T*UL/P5×4M\Ì‘� d‰ˆˆˆˆ(ê„2¶@š�ç d� dç…Ój 8/!]ÎÂм»z£o4\§/áj·-àòM«2ðô¶Õسm5ÒÅ"l�ìÝ»—�!QÈQTšjlAzfŽ÷´ÓÞ÷õrÚ»�a‡û´­ñœ¾×ÐÀ„ÛÎÏ+QÄþóëÉ)H�,sÿ'Q46ß>ȶ¿¬vÊÿå4´g¯Lº]ëå›h½|?{í ž)Z�ÿçÿøVÔ³ÍÍÍhiiÁ€�£_‘ŠaìÂ1›Íèêê‚L&Cvv6 eÈQTšjlÁ’L™÷t°NÎXâ´á€ëv�_À:ïÁêL ;‚þNFz:¦x;Îz\ÁâL÷ivæÆ¼ÖË=øÎ ‚õöØczÉb!6æe6æeàÓ+7´�Í)ýãéKx¿í:Žþò;Q9ÒÀb±À`0ØçOz½/¾ø¢ßl^�FƒŠŠ ¿ÑUUUP«Õ(..fÑ"Y"ŠjŽóu8£OÂÅSÉODÀ¤,� % ’,RÒGÿ]Õ¿÷HÙßÂÍjwø…±ó2ðãí¡ypUÐÎW«ÝíG—ñëº�q­Û†«Ý6ìzé]´¼\Â󨼼|Æ×=xð 8³ÙŒýèG°ÙüÇthµZ˜Ífo õz=ÑØØ­V‹DÍœÞ]Z-Îvu-èÏê_YY‰ÆÆFo§¬Z­Žøû¶C¡˜öø€³f3úñ¹ÅÿsqÏ­_�4ÑÔo¾äÎCP½ À3²vîÄ¢Õ«.g ãŒF#Œ�_ºŸø�[9•ˆˆÇ¢…åÛBDs§  ÀÈ677£¨¨(èþæí” §½#]méù"¤™¯wIÜÝ®ò,w÷ëX7©€¿ðIÈGß[v×Ë¿Vz#`êq‡¶úk® c\ö Û{£» ÍÐ&Ê6ð�ë0Òž½ À=7vŲ™�I‹ðíõ9¨ÿè ´g/3� 3¥R‰êêjTVV¢©© ?ûÙÏpòäI&D …Âïÿ2™{Ë`##Ôj5êêê`0¢#�U*§¥Ñ€ºÏáÀ‹--xkô9ÀÙ®.-.)”]h d‰ˆˆˆˆ(ªùŽ-0™L°Œë��( pôFn]CâÒ…Y§|¤§Ãýu³vLºmn& È a¥i ^çšRîl[úÜ!­ñ† S𑞀vب’DHÌÈGb¦û‹NÛ÷>­Ù’7«ï³iUê?ºÂ‚F•J…¦¦&˜Íf¿ù§œX,à~“ß·VJŸÓf³ù�&ˆ§1i"¨ÕH ñz{;>·Xð²N‡_�Ÿ/ÈQT 6¶ÀWâÒåùªàºÝÌc ëèðñc wµOÂæç¸ÃW…ÜwÎ+CØ…$M ×Xç®»g¡1ƒÑÝAÛq}ÜïcØ�‘¯Ú1òU;)K�˜y’ä÷C% ÅE3Ïb^KÏ®ËíÛës|ŒÚÍ,j„ðýˆ½§Ë“&¦V«ÑÔÔ„ÆÆÆ€ðZ¥RA§Ó„µz½>îêô«ÂBœêì„ÉnÇëííxnÆyK0 d‰ˆˆˆˆ(ê�[à+!m™7�uöݘ—Ÿ?òU;†®}<á6—öÂc_¾sh]½6ê0lÔ!!]ޤì HüÆzqž-Y, zþ_]'È@ IDATýò>h7ãÛë³ñß/=ÅBE‰D±XŒììldOsnh<Òh4¨­­E}}=Š‹‹ý:c t:ôz½7�µÙlèèèˆËZíP*ñòè‡S��xnƈº} d‰ˆˆˆˆ(ê�[àK°8Ó{Úw§Ùr `¤ë3 Ï� {—Ä…‚{€ÂõȳÀFåãÊ »gÏ6·»pá øÍŸuZ�´!¸ü!’ä÷!Q¶Ž‹�Í“kݶY]ÿƒöë€åY\i>R”••¡´´6›�ÅѾ}ûP^^ŽýèG¨¬¬ÄæÍ›‘�� •J…ºº:èt:ìÞ½f³5550›Ýáñ6b‹OÇ5Y"""""¢y0~l�/ß™±®�^¸†f˜ w6cèÚÇc ‰ÜÞC륜!l,‘g%Û(Ù6Ö5Ûòü[C§1tå ’ó¾…$9gaΕåY\ë¶¡õòÍY}Ïõ7æe°¨ó$Ø¢R¡b‡ìÔôz=êêê ‘H`³Ù°oß¾€mšššpß}÷ù�'“É8Ÿ7Â0�%""""¢˜à;¶ÀårA  DKráêuwÎ:mÝ3^ØkäÖ5 uœMp—ÄÍ7(¸Û3–bú±6Ú9«)šÛ�Óç}F ;0ÔqÃÆsH^ó�[D.–=¼>o4\BýGW`µ;�.žþ,Ù÷Û®{ôzxCNTÝÿÂÂB( ܨ©Az„ßÖòòò_÷ܹs|°OÁ3?v:d28wµ2Ex×5Y"""""Š ¾c |ÃXH�daÄÈÞº6í�Ì5<€á+bØèßý•Ÿã£÷ Pp»aã‘4ÍÊj �æÏí‡.ï8×@/?9Š$¹ ÉùE,Ö,h¶äá�†K€ßi[ñBéÓºþÕ}ø‡?À=‡öém«£ëq&•B*•‚ïõàX …ßü؉( ¨T*H$ñ7¦ã-ƒÁ{:M(\ðŸ? @ŸÉ„ôÅ‹!—Ëý.g KDQMxï.Ü¿RŒó¸º-ñAN ’,ïÝ…§ ¤ÈÏY–Û�ššŠ‡z---—%Þµ#¦óÜ]®ÉÓø¾®á8Î×ùuÅz:b ×1ˆ%·Âu@á:NŸ´Íc³ÃFFn]ƒhãß@�’3ûÛBÒlYå[𻓟âém«±bÙÔµ¼z£o4\ÂïN~ ëm÷/ä'Û7ͨÖBsðàÁ�¶ÓétÐjµèêêÂîÝ»QQQÁâ… ¸¸eee,Ä$ú¼ØÒ‚³]]Þó¶„aÆ ‡àÍ7‘ŸŸ�ªª*¿ËÈQTK\º9éP*¥,ñAF‚¤$.]yž òe©a»AÙñ2ïi×íž�¿ŸÓÞ Çù:¿Y±ÛîuAS(àh ªh³{œÁ±Z¿í–µ÷ÀñéB´¹tNüŠ”ým!½Pz?Ê_i€õ¶kþÏ?†t�ßžlÅï´Ÿzÿÿô¶ÕÓé uN©J¥Bii)ÊËËqäÈlÞ¼jµšœB<ÌÙ}K¯‡ÉnŸÑu?·XpÖlFßà ßù;Šˆ»Ÿ d‰ˆˆˆˆ(f`Ñ¢Eèïï÷;_�’A’®a0ì€ÓÞ�qÖ¤ßË5<€ÁÏÿËÆ.Ï>Ž' )IÓ€¿{R€ _¿ÏÝ-ë²÷Àq¾nÎBÙxóLѼqú>h7‡|�‡7äàwÚO±ºÒ‡�;í,Ñ<òí’MÈÈ÷ž/H—Ãuó î…½¦ d?=áS°HTîäüsIÓy,Þü_ ÔwÿßeïÁÐÅw!Üð‹3Ç~ùJ^zgÂË7æeøý_³eZ^Þ‰M«2Y¼åé¨5›Í0›ÍqÑ:µµµ¨­­�Ñu<rs¬xnýz<·aCDÞ6²1N.—c@(EâÕÅ,ñAfJ¥_Z�Øke1ˆæ‘o ë²w{ÏOgÁé d¿dë'ü#·®Ái5`K³<öË�ï?zoô±ÕÓ�‘›Hôy³€B“.á¿_š^˜Í06²Ùl6ïé®®.²4k¹b1¶dgc‡B–Ù±¡b KDDDDD1Åwl�k ×;ž aér ³¼aëD†¯|è=]´™a,ÍNá:ÀÒ Ô�Ž7¾vŽ�,€ššïiEÎùŒ4*•jÒE½Ìf3ººº¼ÿ@ii)ÔjuÔÔ÷¨F¿K²DDDDDs|»d‡ÍŸB¨xÔo<�gA0®á¿À¶h3ëIsð˜¼g,�uZ�p ôA�’ÆÂPL õãôƒƒf³{&°B¡€D"a§ R©B; ÓéP]]�ºº:ØívìÝ»—Œ d‰ˆˆˆˆ(æø²Î›_ŠG!HJ�@œé cGn]CâÒå×uÚÆÆäç¸�šÂE¼höäYîñýŽÑÇY¿‰ dCòW¿<1­Å¼|Ý9ù㨿ÿÍÍÍhiiÁ€�£_‘j&óMÅb1ÃÂy R©¼‹¦iµZlÞ¼š8é>� d‰æˆk C—?t,Îj„@œ…q’ä*$H–±@Dqn¤§#_µÃië†kxÀýÑÙÌ|$}c=É\i™h® Ïa¤§ÎÑù¡ â,$Ê6 i’™¡[ �,LÁÐà€ÿØñ2ŒŒ²®Û=@°@öëËc§Á0–æŽ$Õ…~Çècjð B!±X,0 €å±¶OH$8|ø0gÇÎßnZÏŠ d‰æâE_W†:€a‡÷<—½#önŒ|ÕŽDù}æoc¡æ�ã|Îè“pñT2*++YŠ8®¡ ]z#=þ/ö­F8­FŒtµC¸æ;|ã†Çš«}n ŽOOø-ää¿ÏµA¸ö R–°XsÌiïÆ�á4Ž]"Õ ”””„ý6}cÕZ/�w?_óŒ-Xº#_µF¾¾Š¤ÜÀ�~º·½§oõ¹†²4Gz|Ött:ìHŒ¡ým>=]´oÈ™r»ÖË7QÿÑ÷u¶­ÆÓE«ù [`çÎ� i;½^�ºº:Ô××£¦¦`ñæAcc£÷´X,fA"Y¢Y¹u Cß�|ã9 §¤!I~ 6×/þ¬FX¬€…¥ 5xñïŠÞÁ¸ìÝp|r)•³S–ÇšÁÂØñ� ǧ'�òÀ³,Ös ¹ç®­@JrBDÜ&™O ë[ÊY�p±÷ô×6î ©`Ø�¡Kï²`D³4tåàa¬ßþ÷!CW>dÁâá…誵€È=£Ó5Ћ‘›Hû²½p ôy•äß·rú]8Á^ì�Aèœä#”D ýÑü u,�ïøŽŠ#£c 0ìp�-HqäÖµI¯*ýT¹'”½ðËI¡»ððo;Y“…`µ;ðA»™…ˆp6› 555Þÿ+ eÈd2¨T*.êaØ!K4 ‚$ÑÔÛøtè°ó�(ÎŽâ¬)»ð|�A4s ’eA÷+_~�³I"R–°pq"1óŒ˜Üƒ`Gº;�¬Ø6ö¸í”�HÁ=À9=Ðïp‡²ÿö' h3Püú¢‰Yú€ãöÌ�uwr3ap¹‹õ™oÿðïg¼§WdqnæB*//i;»Ý½^ïý¿J¥âŒÓ”••…´ø™^¯GSSŽ9»ÝŽŠŠ Žƒˆ0 d‰fóâ/]>ñ ½`Û3l!Нc„$ #¡~,:IA2_ÙÍjŸË¸Ço!½éü §Ñ“½q,�½ÙáÚ'Ü]³÷˜ §½{ÂçiK/ý¨>˜zÜç�>ïÚ4…ÀCëX_òwú< mv¡ß1öº 7ø»'�ÿx‡õ™‰?ž¾ˆkݶ)·»ÖmÃûm×qutÛ�yX±,�\@3™oªP(pàÀo)•J(•J( TUUáÅ_ÄáÇY˜HznÂÍâ…ŸdsU1¹ÿ茟é· Hº‰™ù,QI¾g›{±.ŸÑžãÂø7p„kž`Áˆf»Ïå}Ÿ@v²7¹ Wœ=gg�°ž±’,8=�¬Õ8éç©)@åNwÈÖð‰ûqdé~ÿÐÜ©Ü�´ßZ>þGç‚©GOW,l»×…’mhÞ8}iÚ#–,âàO·±x L¥R…¼­B¡€B¡€F£aáæ‰Z­†B¡€^¯Gcc#Ôj5‹!ÈÍÁ‹?§ÕèýXrÐ~I"ˆ6<ÅbÅAr „kžÀ`Û‰€ã‚ß8Ùz¾aC4$Ë�œ¿ C û™ßßîüm~#(>Œ[�x× oGµ³ïÆ”×OMJ¶ P�üþ]¾¶¹_“ûKšæî˜Ýt7GÄ“;@Ã'î ÖÝ;vܹKâ³ß@)g»�6æe`c^^(½ŸÝ±apðàA!ÂÈd2  ²„�,Ñ, ’S ºw†®|èí”õ{q˜q„Š"~™(n€|ˆîÿ>/¾8O6Iáš'ÆÍå“[ù}ˆ³0xñ` Ïÿ”4÷>Ç™îñùØ7¶ Q¶Þ{™Ó~#ä¿)@Ûìþhzÿè‡ <³‹D.Ü <´^%'cĬ _:\hùܶޅ®‹Dîp¾h3ƒØ¹òß/ÅwsKaa! nÔÔ �š&»ÝÎ"Dâó–€höÉ)*ŠàÊû&œönïÇÞ$Y\0„ˆ� Y†”ž…ÓvN{70ì€@œÅPˆhž$.]ŽE…?Ûç€Ñ¿ËìŠ�ëcñ¸±ñ'cï�kx‚¤Ðß@wn@ËçÀÿœë˜íwÐò¹û|iš{”ÁCë9—ˆzÆn÷X‚ _¸xßpwÄ® h3»¤inI¥RH¥RðaEÓUWWç�ë+‹Y�4 @ŸÉ„ôÅ‹!—û¿KË@–h ’S�¸t9C–$¼wî_)ƃyü8EA YÆ@ˆÇâ>Ãõ΂ðÞ]xª@Šüœ¥yÇ�-ˆ3á²»WêrÚº§ý.5ÅÊm ù3à´nlá/ÀÚ�>ïþ’¦Š\ òœ7E<�°“`4„ ”›éž#\¸NÀým}Ð~½·h½|+²Ò°b™Vf ],â—b–V«E}}}HÛŽ_`�³zÖ ‡àÍ7‘ŸŸ�ªª*¿ËÈQTK\º9éP*¥,ñAF‚$÷Óò<äËR#ò6&/`,�ýª‰Ù›0â do]›Õ›ê…ëÜ_–>wÇlËçï8À}~ËçîîYPäÊå€Bäf°£2ÜL7ƒÐ_sÏ}tl{—Ä…‚{€Gï@šÆým¡XíüÿŸ�öìXo;‚nóð†ür×ýxxCÔsººº‚Ö©ˆÅbTVVB"‘°€„�,ÅAJš_W,\.ïeÎñs¾gHšæ^ü«dÛhwåèWÿ¸ìȳ˜Gn¦ J¹ ¹{¼�”�ýóÎÒç_�Ý€Þè‚©gò×E"÷ ÷çÃ.´ÖË=ØõÒ»¸Úm›t»÷Û®ãý¶ë8øÓmx¦h G1E&“A¥RM¹�ÙlFWW`ß¾}\Ì+1��q%%%è¸~ '.X� áà*"â1‚(œ***ðÑ•>|ÜÉňµ¿%eoÄ�á4Àåû ºÓjœóÛà ïwè×ÜîÂ…/à�7ëËÔ#€©Ç=Þq _&€<ËÝQK³c0¹ÆÁFX=�°…ëœFV»?ú—oûíõÙxºh5Vd�½sÑ{ÛíÙ+Ð~t½·QþJ6æe`ÓªL�b†F£ yô€N§Cuu5ªªªpðàÁ�‚\Z8 dcœ\.Ç€PŠÄ«‹Y "â1‚(Ì”J%¾´ ±×Êb…iKÌÈ÷²Î¯;�$!0< ;àèƒ e~ZSåYc�³–¾Ñ®Ìkî`Ðwî¬Çø°Hä‚<Ó ¦¦¸ÇHÓØMëËØ ôº;_ï Œ°=.ô;|׉»[s3ÝA¸r¹`´S™�°‘à�†Kh½|&í|ÕlY…ÖËñø o£÷ö þáß?Ä{¿~’¤¸¤R©°wï^ìÙ³UUU8yò$ÇD²DDDDD7Æ$/v²Fn]C’lý¼ßOˆêùØû�ÀØã™]êBÇõà!`¿C0êÀ#7Ó…T‘Êå£ßÉèù16ŸÖÒog«¥×}Z ¸ã˜läÀÄ¡j~Ž;|UÈ¥|êí)<7\¼Pzÿ”c6­Êı_>�Ç_xï·]ÇÕ}X±ŒïZP|R*•P©TÐéthlläÂ^„�,Å׋ Ÿ±à™$ë´},@ ;^jŠ; TÊM¡; 4v»ƒWý5,}Á»h}yÂÈ`a­‡'´àí²uŸÚÎ÷ˆÏZ¾<+Ü´ºï7àÂNlê57�¦�°¹AÀð5xºcŸÞ¶:¤íÞ�ƒ�yøôÊM\í¶1�¥¸&‹À;S–"ä¹K@DDDDDñÄwl�ëÎ-ïùN[wÄÜFy–û«hóX`hénöz>Žï‚±Û}^°™´Áøv�N܆Ê3Ba¼À31³ëß%qAšæ®]Fº¹™@Æß± `£Ùt‚Õ%‹…1s¿›››ÑÒÒ‚G¿ˆBÕÑÑÁ"D ²DDDDDWÆŒrõš"úv{F¸?Zï,ê�@¿ctfj· wÜç›n ÐÛ㡤ÂóVƒE" 7ÃÝÓœšȳܳ^‰‚×…bËtÆôÝŒ™ûm±X`0Ë#ü¶Þwß}súýÊÊÊPVVÆÿ èõz=zf³ “ÉX”Â@–ˆ¢šã|Îè“pñT2*++Y"â1‚(Lœön NãØe!òW­@IIId¿ò[€„$À9 À=GÖå¸ p¹\0v púƒ\À?Xõð¬žÓ¹™£ç‹<ã<ºFÛþ6[žño4\ ¥L¹ýûm×½cVdq#еµµ¨­­�öõÄb1Ôj5 IÏCX"Šê'£V#,VÀÂR�Daå€Ój„Ñ ¤$'Düíõ[ç0\.w88Øú\#C�@€Ö/Ý FùŽˆV¾¡æØVÀLNÏbd?'3ԅĬÆËþ6[?Þ¾å¯4à×ucy–dÒ…½Z/÷`×Kï¾½>›ócÃD&“!;;;èef³]]]‹ÅP*•A·Ñét,âþ.8‰„oND²DDDDDw\Cý@ò"¸ï@ @  �Ãc§Ge,apŒg12¢ù¦yp~—÷)>½rå¯4àpƒšó°qU†w›ÞÛhÏ^ÁO_àž!ûO?ü‹®ß™F3á¨O—§R©ÄÁƒƒn3×£b…J¥šÖ•J•JÅÂ…A ¹Ëan.äòÀ?– dcܱcÇÐqù*ÖA$+Š� ÎbQˆˆÇ¢0©®®†åö·‡!Ú\Ê‚…a¼øFºÚ½ÿ¾#eƒʼn .@*• ôz=RSSƒ^6ßÒÅ"üé6”¼ô.®uÛð~Ûu¼ßv}Âí=aì¦U™S[‹Å£Ñˆ‚‚>ÐbÀ ×Ü¿À?—kôXà�Y;wbÑêÕ—3��qF£ÆÎ/¸?ÖBDÄcQøxä ¢…ß߆»Ú0lÔÁi»1aë[øµr¾_Iq ¹¹‡B~~>ªªª.×jµ0™L¨¬¬ K(»iU&ξ\‚Ÿ¿vÚ�.£÷vðE»ŠÌà ¥÷GTk4Q]]�þþþ ;B)ºèœð•Ë�€Ÿ¢ éc KDDDDD1O�².{7\.—7põ==QP»Hèç�R,ó„±SéïïGuuuØBÙt±µÏ(òvÈ~zå&–,bEV6æe ],ЍÚú†±{Ú€¡,ÍY"""""Šy‰K—#9†:üÎУ\ÎÚ» |õ„²¾§‰b™o»sçN…ÂI· …عs'Äb1ª««a4Ãr»?h¿Ž_×ý?í ámüüµ3øuÝ_PÿÑ利­o»sçNdeqöI,züñÇÝ¡¬@-ÿvÐtž“°DDDDD/’ïÙ§­.{÷”�±p—„ã (6�cSRRBº^JJ vî܉ãÇ�/x§¬öìeüüµ3¸Úmó;ßw�¯ôÅ"¼Pz?~¼}SØj,Œýâ‹/ø ‹Q�?þ8°S–¦…²DDDDD7É)m| Hù…±³Éla¡4Ó0ÖÃÊ.d§ìO_DÉKKûwõZo;ð³×Πü•Óa©-;cã;eiºøô‚ˆˆˆˆˆâŠ e „žÂà'GýÎ÷tÌú†³K³Ó‰bËlÃX�…씽z£寸ç?/Ï’à…Òû¡ypUÀ^Ú³—ñÛ“­ø ÝŒ?ž¾„o­ÏÆ3Ek¬¶óÆB¡PàFM ÒùŽX씥é` KDDDDDq'Ø"_¾³d=¡ì"GPì%Œíïï‡Á`zþx Êþºîcî0öìË%A¬‡fË*h¶¬BÙ˧ñFÃ%¼T÷ñ‚²¡†±Áj �@€Þѯ…°dô+ÚÜà˜çŸÑërAW†²ä1 @ŸÉ„ôÅ‹Ž� d‰(ª ïÝ…ûWŠñ`^‹AD¦Ó;Qm#Õ/£l߸àßâM´2”%Ïcñ0¼ù&òóóQUUåÿ„%"¢h–¸t92rÒ¡TJY "â1‚(ŒI)H\ºò<äËR£æv{ùrÚn�#›šù/¤/\¼‰ü¢ .Z.KOâùïoÀÞŸ¨¢ö±¥~¦{¢Â¾¿W…´íÖdhü£†û›�PÃØmÛ¶Áᘸ·pÉ’àý’óÊZo»oÓ·×ç„´}ºX„o¯ÏÆífôÞœ×ßE¨aìæÍ›lñ³¹ðÙgŸá³Ï>›—ï]VV†²²²yùÞ£ÿ>òÈ#ÈÌœß ^$šøÍ�ù eÍf3ºººæì>ˆÅb(•J>‰ ²DDDDD·<‹|õôp ;BÙÔo.}û:ñƒ_4ÂÚ7ˆŸ~o=ž|t% ÖHÑy݆ -Ø÷[öý«·zxù…˜ÿ}n}@†‚Õ|Î×tfÆÎfæéBΔ�ÓéŒMII‰ªzÌt¡¶ƒd2Ù¬~þ¹sçfuýÌḬ̀×{>BÙúúzÔÖÖÎÙmT©TÞß-,²1®¤¤×oáÄ $\Ý‘ˆxŒ §ŠŠ |t¥wÚY ¢Úß)K�²¹ù½ßùîy²‘Û!kísà¿h„Ë|òöwý>¦_�&BÁš <ùèJ¨Ÿ©Ç+hÇóßßÒÇþ£Y¬wÆN×\-àªù e—gIp­ÛíG—Cš kµ;ÐvÅ2¯÷u¾ðŠv*•ŠEðÁñ4²1N.—c@(EâÕÅ,ñAfJ¥_Z�Øke1ˆ"lK�,ƒpíüü� £ "ÑïO`íÄÞŸ¨&œ™šž&¾¿WáÙÿ݈ —,Þ@¶Ódá· øþ“ À¡· è¼nÃÊ ¾ÿ¤"hpk퓸áH IDATsàÐ :¯»Cî‚5R|ÿ)EПkísàO§¯zÇ(L´m¨Û�ÿùéiBüuÑŠ€û½ÿ·:¬ÈãÙïò#¸ ÆzÌG(ûðúï"]ó2&� kµ;Pþ/§a½íÀ’ÅB<¼!gÎï#ÃØ¹£×ëqôèQ466âÏþsLÞǹ eU*ÕŒÇ=Øl6Ô××Ãf³yÏ‹Å|† Y""""""I² ùú œ7.EÅí}åP;àÙ L�'] ë¹gýÎë¼nþÕáŠÉ†C' X""=Mˆ«×íxùPþü‡b¿°ó÷ÿ¯ÿë7-°ö bÓj)¬6^>Ô†—µá?~³ÕoÛ oâ‘ïÕÃÚ7ˆ9b¤KDÞmÿü‡bï"c¡n­—,È+ªón{õºûþU‡ÿøÍV¿ðuß¿ê°õYܲá c=æ:”}¡ô~h?ºŒ«Ý6|ç…?A³%{¶­[{²/Õ}ì�9û“í›æü¾1Œ�=O0X__½^÷y®BY•J5£.d�N‡ýû÷û…±ó9Ë—¦–À¹‰Öm…Qq[;¯Û°"G<«1‡Npâw�ÁzîYt6ìÆüf+¬}ƒØ÷¯º±Ÿc²á¿h‰Ÿ¼ý]\øÓßx·½pÑ‚ü¢Éï{>õãÿ.ðç?£³a7.üéo¼Û>ÿRË´·ܳr¿ÿ¤.}:vã“·¿ ØÿÛó|ÐŽî0ÖÃÊŠÅbTWWÏx&)¬X–†c¿|K a½íÀO_Âí×¶ë½íð†±?ÖlÄ ¥Ìé}b;;MMM¨ªªÂ#�<‚êêj¿0vëÖ­1ÿüq¬[·m´.ׂüL›Í†šš”——Ãl6 >Ì06ÌØ!KDDDDDä#1»Ã×>ŠèÛØirw9­Ì‘½ìÐÛ†€óƒ}”ÿûO)ðä£+½ÿö»JüàM°ÚÆV¦ùP`߸ÑÏ~W‰Æ¿táÐ ÞþŸN<ùèJ¼ý?�è¼nÃÞŸ¨ ~0;`Ût‰;ìu;�M«¥~‹’¬ÉÀÖdhúK°>"%Œõ˜ËNÙ‡7äàìË%øuÝÇx£!xûò, žÞ¶O­žóQ cgÆl6ãèÑ£Ðjµ~Ý™€»Û³¸¸jµ‰$.ê±�3e=]±ž `Wl$a K4‡\Cpڻᴑ�.G¢%¤,aaˆà´Ý€ÓÞ×ðÄYH\ºœE!šG#·®Á5ÐHg"A²ŒE¡�#ÿe’§+¶×'8õðŒ#/ØGùƒº›VKýþá’{¾k°1Ï>¥À¡\¸hÁ“�®ô΂U?¸ÂúïÿoõØ÷ q»±Û8çPý@6Yz½>¢ÂX�ñ¡ì?þã?B*•Îè{­X–†Úç‹Pû|¬vGÀåÏ­ iѯér8ø·û7†±!²ÙlhjjB]]]ÀH™L†ÒÒR¨ÕjdggÇe}|CYÑ‹ÄcÍ¡‘[×0Øvö±.HY‚ä5ßá›!óu¬³wcÈpÇ. ‘¿jJJJ¢úþDËÂ^ž`ÓWÁ)þü‡b¿óù^}Ðë DÓÓ"odÃD‹–qÛß222°hÑ"ôõõ¡¯¯/bYÞÛä¹�³õAûu¼ßv׺m¸ÚmÃÆ¼ ,Y,ĦU(~pÕœß~‘H©T “É„žž²“Ø¿?´Z­ßy2™ jµÅÅÅ   »ÛýºànÌa(Ë®ØèÁ@–h–\Cp|r40hñ¼(ìéÀÈ­kHy¨œ¡ì|<µa±–‚"ÔHO‡; fØ�Á¶HüÆz×>ÁbñAs`ØxC Áÿfôbð“£HÎ߆$ù},Ö<<'rZ�0Z�”d.U±þºhþtúªw\€GzšÈo ÅÇþ&•JQYY‰êêj?~`Û?�îÄŠÑàv²íš>î¦ÕwñAÍiÞ„ux€­MœÑhd¼9溯¸ük :þ 'Ž7öÞ[Ùˆ‰ Ç®}zäýè/ˆ‰ ò4Û‚Ig/ö ·Ï¶è×ë//Eé�U“ú¹O%`ËÝÅ»Ë;‚¼•‹ÐkDUM zû†ptÿ÷ÀÖ.á�¿ÊÁ+?¯¶€Š£X­À{ofy´ÝdŸ+ÙØ“²3Õ¾`*“±ð‹C_°%cO¾·1âp·Û©ŸN�úé¼w>»„_újÊù²'eÙ¾À=‰DµZ µZ “É„êêjèt:tttÀ`0À`0 ¬¬ ¹¹¹ÈÍÍENN$I@ÇÄÉXöŠõ¬�%zV‡ÅBuðÿþð{c&c½ûÃï®u™q®ùÆ”¿FVÊz&!!Ðét(//ÇêÕ÷¬­­Eii)Ö¬Yƒ]»v¡®®. c0ÕÉXVÅVÈM£ 9Ñ Ѭú+î²Ê;y‡(³¿±NU³ü»L�cÑ Þ{+Ûã헥ŢöµÛÛÆzœ­ë”غγ~„¹O%x´°˜'Û�õ<Ý=Ÿ±¶�­¦»RÖÉXÀÖžYšèÑö1âp<³4_\0áæ­!¯¼VVÊNŒJ¥‚J¥Bqq1jkkqèÐ!�F˜Ífèt:èt:œ>}: ^³7*c:äÔ+°U%k4šI=žB¡@qq1wЙ8Tdˆ&/Hìüaf¬¶cmOD�M$–Áz÷´hwóÃèë‚ç%1hD“o¡@D0Ð7æ˜=>‰ˆfƒ‰$eÿþ÷¿£¯¯oÌÇZ°`�ÛÅiï%c}ÙD’²]]]hjjò›×ÖÖæ�Ö>£[:tµµµèèè˜Òßsá´··{5FáááxüñÇÝÞæ­¼ÜÑëõœèü²D (Fîtð7î`H8ûÇÍ2¡ò'04ªO¥ýtéÑóDPì# у~°�OÇÈÝź[‚Œ¾ €“‰hVñ4){þüy\¿~}ÌÇILLĆ \®÷v26I&Ak—ºSÍõ„íµ âüÕži‰­§IÙ+W® ¡¡Á¯ö›(/?~BBŠ‹‹…ªÙ© \$Â×_=-1r—��Îd,ùñçV†€hòD¡K{Cû_Nyî„&ÿ#O�$še‚ç§"(öÜé¾"Ìn GXÚ Ñ MþGܾqYèß<Ö¢^"± ¡ÉÿÈ€ѬâiR655%%%.×ïÝ»ýýý.×OGeì³K…Eº2’c‘™2Ìm{-ƒ(üÇÑ{kÑ‘ax6=Ñë±�H¥lyyù„_§Ó¡ººð=ÏúÁþf0ðÍ7ßÀd2!'' ã·&ÉÍÍ�’ß  Øa�oùÀ 7×{;Ë…»õ"z@Áó’šö¼­WäXÛ,P!DþƒE4 …¥½0nõ«H,Cøcl§[Ñ ÏX QŒ|ì1#GxÆZŠˆf¥©^èkºÚ¼•ÿ$¢#Ãp­ËŒçßú…¿=ŽÏÏ»Vòîûø}õèNÚÛ¶&sÚbëÍ…¾²³³QTT„M2ý`?ÓjµØ´i“�¤^³f�K�S�N‡M›6 ‰æ@ÀÊXš&d‰¦@H|:"–oEpüRá P$–!8~)Ÿ܂0Å ‰h–…F ‡ˆå[$‰c ˆ¦jÌED#âñ|„¦>gû›DDÙÆ\Úóˆx<Ÿg¬Ѭ6UIÙéì»0. ÿñ‹8~ _\pMÈÞ¼5(,öš:oå/ŸÖØz+)+•J¡T*±¶ P_¦Õj¡Õj]®¯¬¬tº¾££ƒ¥¥¥Øµk—ß�«™NÆšÍfèõz Nr¾²Ohp¥½ÝmOf¶, šÂ@žr<ýÂÛ€'‰ñTrƒA>-x~*{VrŽ i"‚g§L³ ‰ a�mÀÚeR¤&Îc@ˆ|x¼Md¡/wfb¯Ì”ù8ùÞzüâÐW8ðÙ%·Û$É$ØüÜl^±dZZ¸3‘ö�èСC…B!œZ¯Õja4¡Õj‘ŸŸ‰D‚øøxÄÇÇ£££:�999SÖ¶`ºÍT2V¯×ãСCÐëõ0›ÍN·%$$@¥RáÕW_½o»òŽNàÃݶ‚a…,ùµàyIˆML�R©d0ˆˆsÑ …D x^äÉ�Œ¹ ;ùÎx›l¥ìL$cíÆEAûÆ ôüšÛê×—V¤AûÆŠKÆÚy³}�/3™LBbpïÞ½ÈÍÍEnn.ÊËË!‹Ø’ˆ V«¡Óé°zõj¶þh¦’±�………¨­­uIÆÚß {[ˆ©Z0�¦²DDDDDDD³Ð褬»ÄŽ#³Ù>0™LBU¬ãb^ö¤­?ˆ¸ûo¦8¶z¸¥Ré·=z²DDDDDD>èý*#r–Ç»TsîÚ§GΓñX”(ÁûUF´\7cÙ)¶¬U &*-íf|t¼-×-ˆ‰ Ãë//ELT¸ËãÛûª¶\7cQ¢9OÆ#÷©—ÛàÚu‹ð{·©=eBÝWÂclÉs­æ­8b;M6oå"üöOÐÛ7„¬XˆÜ§°kŸ ÄØºNéôx;·©œÛñõ�æn;{ß]ûãÚ½±%ÿöË”nS±J6@=›žíë+°þ—Ÿà_ÿÇghxo½ß¿¦úúz444`¶Óã3|ø¹&$$ ¨¨�kÖ¬qZ`J©T¢££ÕÕÕP©Tèèè€F£`;ß“ŠOš¸û-ÖG3ƒ Y""""""söb7jOuà7of¹ÜVú;=~°b!ê¾ê€Õj»®âˆ¥ûô8ºïûX»í/°Z�˜¨0\»nAÅ#þVµÎ)™YqÄ€W~n«˜ÊY�£ÇZPú;=¶®Sà�¿Ê´\7£ôwz§Ë;·©�ûTzûñÊÏëPUÓrwQ.©í9üN�?þ*Ç)ZqÔ–Ô}¿ÊˆÚS¶Þ’V«¹O% ôwzä,�¿—�ý²»öéÑrÝŒŠ#F,L£·oGŒ¨8jÄ_ÿ´Úéu¼òóZT1:=ûï‹–„¹$dsï¶^¨:Þ‚7¶¤sG Pê§S†sÍÝ8×|çªd'£§§G8UÝ×»bïÚµKhK`6›QVVæ²�V«u¹®  €;®ì §ét:§–c1›ÍBul||<èC¸¨ùµÁ3‡pâèÜþ¡'"âA4}îXº0xæÿÿûpøðaäÕ~iK\ŽÕ_õ£ã×ðúËéè=½½§·âõ——¢·oÿôr5vnS¡÷ôV´|¶;·©lÉÍ»IJÀVQúÊÏë�¹DŠo¿Ú‚ÚÔè=½;·©„¤*ä>•«Á–$ÉY«¡¥?¶%J§GUM‹í÷žÞŠÚÔøö«-È\"Å+?¯Cí)çž�u_vàÛ›Cø[Õ:üõO«ï› =z¬«Z‡–Ï6¢÷ôVlY«ÀÙ‹=N¯£âˆGŒØ²V�–Ïò…ç`µg/ºïÕ¹,-Ñ’0¡‚–ã-pe$Û'»ykˆÁ˜F:�nÂ=aóóó¡V«<ØãT]]íÔîa,�&“ b±¹¹¹  a…,ù÷‡ÑÞ6ôô= qŽ šQÖáÜémC[/ʺ�eO:¶p”¹D*$G�{í2—H�’�¹Ëã± @oß½¤Ô{ýŽýßwª6-ý± U5-øíŸÎ;=öh½}ƒÂïzï­láú˜¨pTíÿ>’WBÅQ£Ës/ý± ËÒb=zýï½™å´í[–âý£F§×ñÛ?]@´$ ï½™%¼Ž˜¨p¼÷fþéåê1{Yšu_vp¼¸/.˜„`Oü9ö8���³ç©B¡`«‚ ÈÍÍ…B¡€ÑhÄ®]»pæÌ¬^½Ú©ZÖl6ãÌ™3¨¬¬ª•7nÜÈ8û&d‰ˆˆˆˆˆüLLT˜óAúÝäç²4©Ûë}tüš­��ÉŒk&çÞ‚É $8w©µ§Lc&ƒíÕ§�=*EÝ—&·ÏÍ]ÂÓÞ.À‹�öäìÙ‹ÝNÏ#gy¼K_Ù±ž7Í¿8ô¥p9:2Œ™F:�މ?/Û»w/6nÜ‹Å�NwßJÙœœ¶„˜äV+Â,€\.w¹� Ùwøða\n¾†ÁÞ!„*V H,cPˆˆsÑ )++CÏ­a ÞAøãù ÇÛ˜+AÝÉ]î>é8:‘9Þãç¾Tý@ϱâˆGŒ?w rM8.fÏN?Ï'ù›»<u_vàìÅn�+vif}pü"Z»<[˜è‹ &|~þ: I&ñûþ±þ†ÉXïKHH€N§CII‰PëŽX,ÆÆ�™Œ�!q^‰ {ñEÌY²Äåv&d\[[ÚZšØNk!"âA4sì rÇÛýŒ®€�jËÒ¤ø[Õ??Ðcìܦ·µÁL:w©™côß­½[ÕËd¬ÿ8püÒ¤Zh__ÁàQ@’H$(//‡ÉdBmm-, Ìf3Ìf3 P( R©˜ ÷aLÈÍ"™K¤8{±½}ƒ.U«G ¸f²àõ——ŽYÑj¯Â­ûÊÀ5!»kŸ ÄØºNéõ×qî’ëëhi7߷˜WtdžYšˆ·òŸduì xâ‰'¦ôñ Xá9Ž„„lܸ‘�ðCLÈù˜eK¤^;­>oå"œ»ÔƒûUþø«\áú³»ñÊÏë°0QŒ�ÛÆ®|]´@‚œåñ¨=Õ�ªšä­\$ÜöÞûçQú;ý¸÷Ÿ*olYŠW~^çô:zûñÊ›µãÞïÚu˘ճä›>ýåZ�h’ ¡×ëQ^^î´øÍ,&d‰ˆˆˆˆˆ|LÞÊEøíŸ. öËŽ)OÈ–þX…ªšT1¢öT¶®S ·oG ˆ–„¡jÿ÷�¶_˜(FÝ—x,ïÿàõ——bë:%Þ{3 ¹/UcíkAÞÊEX–&Eí—&Ôžê@æ)ÞØ²Ôë1ÚºN‰Ú/;PqĈªšÛs¸ûûÇÒÒnFËu3^y)w2"/˜êjV&Ý3›Í·æ±X,\[ù0¶3‹ Y""""""“ûT¢%a¨=eÂ[Ò�nËY�E‰b—ûLäú³ý3*ŽPqÔˆßTœÇ¢D ~°bJ·©°h�sÏÁªýßGéïôè5 ‹j-K‹ÅÙªFé>=Z®›ñ›ŠóX–&ÅÎm*¼±Å¹ÝÁ²q¤9Ëã�n_”(FÎòx·=tGo ¿ÎEîòxÔ~ÙøÍ›‹ðÆ–tˆ”Z·¿ÏÞ?vë:w2"/`{ï³W¼NTYY™ÓÏ{÷îEnn.:C˜�%"""""òAolIÇ®}z´´›�’¤µ¨Ýn?Ñë·®SzÔçuYZ,ªþçs¹~Ñ *~}ÿƒù÷ÞÊó¶ÑÏm¼ç4zÛŠ#$/�¸Ü§·oÐöü]³ùíŸ.Ø»\Ð˯œk¾�¾þÉõ~fi¢m¿° â|K7 jnûË’_ª¬¬œT2Ö�FÃ…¿f²DDDDDD>è�-KñÞûçQqÔˆÒóÔÒÑj¿ìÀ+?¯ÃߪÖ9%XÿíW €ÜåñÎÛŸ2áìÅüõO«‚ Y""""""Ž÷ÞÌ¿ýªÁ¥ ÙÖU5-x,ïrŸ²%_[Ú-h¹nÆ–µ —JÛ]ûõøÁŠ…È}*�Á#¿"•J!•Já'Ï×l6£ººµµµº_yy9ßl©Õj�Û Ø“¶J¥’}c§Ñ€.}í툉Œ„\.wº� Y"òka�mÀ“‹Äx*9ŠÁ "ÎD3(H"CØc°v™©‰ó�)²u�g/ö ª¦Å£ö³É²´X´|–�Š£¶E½ sÉCøÍ›YÈ[¹ÈiÛ–v3¬VxÔb�ãÍ÷¼ûÃïáæ­¡zŒŒäXüÇ/òÑ‘a@^b6›ñ¯ÿú¯0 †ÄÇÇOø> …­g¶X,f§Q'€ƒðá‡HMMEII‰ÓíLÈ‘_ ž—„ØÄ(•Rƒˆ8GÍ QH‚ç%AžyÜ\d �׃u¶‹‰ Ç[Ò]>mÑɘ½t9Þ|ßTô{�‡ãÙôD/ÓjµB2V,óù)VZZŠÒÒÒ Ý§¸¸˜�óALÈÑ«®®`«Ê,//ç‚QDc`B6À­_¿—¯‹£g{$‘1 DÄ9‚háÔÕ>|Õba0ˆ8ÞˆˆŠÁ`€ÙlìÝ»—ÉX/ÐjµÐjµ“ºoyy¹ÐCö‰'ž¨T*öî�!LÈ8¹\Ž�0)‚qðp IDAT¯E2DÄ9‚h†)•J4 õ øf/ƒAÄñFDP,–{_€%$pñ<¢ñ0!KDDDDDDDDÄ^} Ø÷b…¬wb\PP0©û:.fŒÉ,FSƒ Y""""""""z`999¨««Cmm-Ôj52ÅT*•Sâ{²&›Ô¥©Äù¦úúz”••á€F®ÅÅÅ‹ÅÐh40 |ó|€Á`€^¯úû’o`…,‘�êéé�Ñh$ùøsíèè@aa!ÊÊʰiÓ&äææB¡Pxt_Vmz‡F£�^¯wZÔ‹f²Dä×Ï C.þ%ÅÅÅ qŽ š!w,]6Çáæ0¤¦,ÄúõëýöµˆæÄ—{úøÞÇÛL»ÖÙ‡…qQ|ãý@aa¡Óϵµµ¨­­õè¾LÈÒl„,ù÷‡ÑÞ6ôô= qŽ šQÖáÜémC[/êß�ÑD÷?m]V"¾Á4%Ú»9Þ&£à·ÇÑÚeƶ5™ØôÜĈù3Ѭ¤Õj¡Õj'u_Çd¹D"�Z­FQQƒ:C˜�%"""""r<ïÞIÁí7Dèé¤,Σdh¾¼—Ü’È” ¸ÖeÆOþý~òï' ~:/­X‚ÕO¥00>æôéÓ ‚0›Í¨¬¬„ÕjåYd^@nµ"lÁÈår—Û™� p‡ÆåækìB¨b‚Äü£ODœ#ˆfJYYzn cðÖÂÏg@ˆ|x¼?¼·¿¹ÐÕ[ÿ;cJæ³3Îû—($‚AñÐ³é‰øâ‚IøYwò*t'¯b¡LõÓ)xM�Á–4+¨Tª ·vÐétèèèÀêÕ«‘��àtÝ¡C‡˜�õ’8/‰D�½ø"æ,Yâr;²®­­ m-Ml§µqŽ š9ö9ˆÈ÷Ç[HÒBB¶áï€BdÿãJ“Sñ‰g¯ˆœö/òÜ[ùËñš:ºSÍÐ�¼ŠêSWتf÷}|û>>‡gÓ±ù¹%XýT2[PÀR©T^˜K¯×£££jµZ¸¯B¡@II À`0@©T2¸ÓŒ Yš·o\Æí—a¸ ÀÖ³�f/‘x>D!…F x~*‚baçÎÄ9bšXGp§û nÿW+Ç‚bl§É‰"¢mcnžœcΓ¸‰e^ð8n·ÛÊßÿÛõLÊÒDÕÿhøÚ!»(›g%MBŒ8/­HÃK+Òp­³ºSW±ïãshí2>?ŸŸ¿Ž˜Èp¨ŸNÆ�ÔÈL™ÏÀ̓Á€Ý»wÃ`0”J%ŠŠŠœ‰ƒgΜÁêÕ«!‘H´iæø^X,d0!K“vÇÒ…‘«ÿ‰Û7.3äœ °Ü€õîeûþ!ÏGhÊ÷›ÊqŽ Îœ#¼àö·­¸Ý®ç˜#×ùXHÈ· ŸÁóS¼@åÔ+•\…)VbÀÜëÍv¶¤ìõ«³º¿þ º8îЪÀþ÷Ž̸(l[“‰›·ñ‹C_!:2 Ñ‘áhí2£÷Ö >8~ ¿„…2 ›fz½Þiñ(À–|-,,ÄÞ½{‘›› ¨««¨***‚Z­f𼤨¨‹ …B¸Îñ,Çëiú0!K?�èÃpó á=‘Gû�å†�"(FŽ�ää ç"ÎS䎥 ÃÆã¬‚¥ ±Ÿ¹#GØ£ÿDì½8–ðÌuÖ*• ååå ì `B–.œ#ˆs爉Œ¹óG�zÅFGGãé§Ÿf"–<2wî\¨T*¤¥¥áäÉ“BÅìH›ÖáAVñ�wð¿ÁóÁ°ñ3áïÞwƒ"èêm­ ²µb…J9×kš5Î^ŽëmUÓŽ‰XÀVªxŽÃfXtd8Dúú‡póÖ�_¾†ììl( tj4ˆñƒçë®õ€R©D]]�ЪÀÎ^±i6›a2™Ø¶à>T* &ußøøxá²ý1¯£iþLÁÐýŒt\pJ´H¥Rdee!44”Á¡ feeáܹshnn`ë9lüŒœ#ˆ8Gxhèë?;%c“’’œVÊ%šÈ˜{æ™gÐØØ(TËÞþæ†#¢¸èÐ8D!{ôÜŽ_ŠáæBoYhøZ„†¯m•²+TÀ²GX5ˆzú€†¿õœ{Ä ûHôÛB•ì‡>£ªO5ãƒã— ;yÕéú$™ê§ý«�¬T*…T*…¯§öíŸG,‹Ëmö…£“²“H$0›Íèèè`BÖƒOÅç¾É&uÉsºôµ·#&2r¹Üév&di\·¿muêMǃ>š ™™™ˆ‰‰Á™3gøà/ì± xr‘O%óh‡sqŽü9bô )))ÈÌä)—4y¡¡¡Â¼mOÊŽ´ÔC4'f§XId{lÖ.“"5q^ÀÇ.x^‚UqûÛVŒ´�Æ�î+Âm=}Àÿþ«íŸb°,•ÉY×Óg«†­¿`Eû ‘Ûm¦3;ÛÆ›§Î5ßÀÿÔ5Bwò*zo9¯iðÌÒl^±/­Hc ¼H¡PÀ`0¸T»Ú+1;::\î3ºj–(t8~ˆÔÔT”””8Ý΄,�É:2€á‹Ÿ?GGG###ƒ�¡)±páBôöö Up#-õš—4á°Á󒛥RÊ rŽ Î=GXú\¾a2–¦ŠJ¥ÂÐÐ�Ð2døòqÏdB§Z‹B"ì×5Bw²Ùm5¬ú©¼¶&™)\Xv¦èõz(•Jèõzèt:èt:H$( ¡]�F£�F£qªŽeÒÐsf³›6mrŠŸ;&“ %%%P«ÕNÕÊä˜� pr¹aR_‹œÐýnówàî©VsæÌÁÂ… Lòйsç"%%E8-y¸ù‚UÎD9G(•J4 õ øfï„î7ÒzZ¸üðÃcþ|h’wÌŸ?R©===¶1×ú¥­ês�·©$ ‰@HüR¡ïíî˸Ýu·¿m]Wƒ]A+� ¬�lj„-“´×Ów/ÛÖi…±]4j1®1’±áQ¶–²TVÂÎ0ÝÉfì×5âóó×]nK’IðVþ“P?•‚q8ƒ5à ]®3›ÍÐëïsŒN$* 3x*))b¨P(�ŸŸ…B�F½^�¢¢"X,èt:ttt@§Ó!>>ž yù&dÉý�_ç½þ#ii<̓¼ëÑG½·¢úÍvXGXuÀ9‚ˆs„ƒÛÝ—�âAäM©©©BBöNwà§ Y_d¯œl}¡ïX:qû¿®áNo¬–.Û÷ô _‹Ððµóõ æ[í�¤�fËÀVmÜsó^òµû¦»Ê×±+aEâùŠ‘#ø¡…Çùmux Ú÷ñ9|qÁ9‰·ù¹%ؼb žMOd€|Œ§íâãã¡R©�››+ô”¥ñét:!¹]PPà6ɪT*¡R©�ŸŸ�ÒÒRÔÕÕáСCÈÏÏgœ}²äÂ:2ëÍ{çN9®ŒHä ¡¡¡ˆŽŽ¶õ¬ƒí´dV!pŽ âasÇÒåT‘Í�‚¼*>>¡¡¡†uà&îXº$æ TSM…àˆ¨{ Ú‘Ü1wáη­¸ým«ÓßÚÑì•´gGuA˜n…|¾Ò(+bcl‰G…üÞíþœ´ulí`l³ýßÝkEOŸm7¬ønÐ]¢uü6¢èž—„ yI’ÈXà’dl[“‰ÍÏ-a5¬�Ú¹s'Ôj5á%öd¬J¥ºoÅ«D"Aii)ÔjµP¥ÌÖ¾ƒ Yr=ð3w —£££Ù£Ž¦íàÏžl¹Ýu™ YÎDœ#îºÝe.³UMç˜kmmµÍû½mLÈNQH„íôøyI°ÿe½ým+¬·nàNÿ·¶¿¿·nÀz÷ w¾ÝM\Þ¿ªcrV9ª5·cw,sÂl}nÇÒÖ|7tÿDZ'Wí ­·µß7j£þ+¶á@ä|Idš;A1rîÓ~æÙôDl[“ õÓ)³òõ×××£¡¡2îþóULÆz—½ïêÕ«=Ú^"‘‹©�F&d}²äâη­NƉ¦Cllì½}ð»^„s爻¬#Â嘘î 4-æÎ�{oúŽ™!Áó’7 Þþ¶Ö�>X¿ëÅK¬7ݶ<�SÅi{`ÄK½¢Ð‰eŠŠ‚ùXl€x+ù¬~ý===0m_ÐâýÄONŸ>Í�ÝC9KQ©T¢®®ŽAó1LÈ‘_Üù®Ö[7„Dúm‡/RÇkƒàëDÑ \^¿(lD‘ó4'fÖõ{�mãíƒãÑÚež²ÇK’IðÒ ®ƒàM&“I¨æô”ãÂ_žö¡�­Ìfó„Þ ò=LÈ’Ûrvaaa M Ǫ/ë-Ï+<îô¶¡§èa9GçÎDSÆ©BvxÀ³¿Ã¸ÓÛ†¶^ "4ˆAœö¾´ŽUµc5rLÔ:žýØ’}÷}ßoŽ[•+Ï‚Çïñi¯fuäðÜYÝÊñfwàø%—E½Ä3K˜�õƒÁ€Ÿüä'“J —% >þøc.B5Šý ÅC‡yÔ~Àd2 Õ±999  aB–\^Ne%M§>¤ãôF#ÎÄ9‚sу fⓈ¼`÷îÝSR‘i6›QVV†ÒÒRÕAnn.ª««¡×ë±k×.�™´ÖëõÐh40›ÍP(¬8žfäV+Â,€\îÚœ� Ùwøða\n¾†ÁÞ!„*V°y=qŽ šAeeeè¹5ŒÁ[#<Ÿ!âx#"¼ûÃïáæ­±W‰;pü|v ð¿Ès»Ÿþû 4^íf@½È`0T* <º�½2¶¼¼œ¼�ÜÜ\¨T*èõzèt:ÔÖÖâ÷¿ÿ½S²U«ÕÂb±ï…X,ÆÎ�;¼ià%‘²_Äœ%K\ngB6Àµµµ¡­¥ €ç§›ç"òû‚DÄñFDžËL™?îí_\¸.\~6=qÌí¢#Ùnkº@¥RMè>Ý~¶Ú»w/ a4a6›]*_G÷â-++cu¬bB–ˆˆˆˆˆˆˆˆ¦„X,†X,öx{&b'F"‘ ²²:�Z­v̘æää@­V³¯�bB–ˆˆˆˆˆˆˆˆØéÓ§'|¶*˜µZ µZÍ8ú)&d‰ˆˆˆˆˆˆˆhJ™Ífa*³Ù,\/‘H„ ΄„Šf%&d‰ˆˆˆˆˆˆˆhÊhµÚ1O§€ÚÚZhµZäçç{¼ø&“ “º¯B¡`û„,ù­èÈð mŸ‘Ë yÑ®]» Ó鄟Åb±Ó¢Rƒ‹f³Z­عs'ç�êêêqÝã)//g¿^„,ù-Çë¹æÈL™ïv»¾þ!@Œ8ܯ^_vv6 :5ÄøøsÕëõB2V¡P ¸¸ØmP¯×£¬¬ F£:�999ÈÍÍåÎL³²DDDDDDDä·Êî�†]}êªÛ„ìµÎ>œkî0ñŠÚ™&•J!•JáÏU£ÑT*Õ¸‹L©T*TVV¢°°z½øÃ˜�õÀêÕ«Ç­r5›Í0�0™L¨­­…ÅbÁÆ�ñꫯ²]Á4Р¯½1‘‘�ËåN·3!KD~-ì± xr‘O%G1DÄ9‚hId{lÖ.“"5qBÄñ6mÆE!I&Ak—û?nDFr,ÔO§·_ëìÆ_}"üüÌR.$å-ƒ$ˆ! """"""¢@Ãdìô[½z5[°¤¤&“Éív&“ %%%Bg~~>ƒç‰jµeee †�a…,=0µZ�ÚÚZÔÕÕ¡¶¶µµµP*•‹ÅP©T0�0›ÍÂ`€-‰ËêMšm˜�%"""""""Ÿu®ùúú‡¦ìñ¢æ†ùUõl}}=0 ãî?_VZZв²2TWW 8%aíV¯^�ÒÒRîä4ë0!KDDDDDDD>ë§ÿ~_\0MÙã=³4Ÿþr­ß¼þžž�F@’<_‰D‚ÒÒRäç磺ºF£ƒ‹m‘E…B•J…Õ«W �Í6LÈ‘_³4+1!KDDDDDDDD¬¼¼Ü£íôz=t:::: T*ñꫯ2x4«0!KDDDDDDDDL¥Ry¼]~~>6n܈ÊÊJ �“¹D�€ËŸÒ¬°}ûvlß¾���~ñ|;;;�~>vì¶oߎŒyŸ¦¦&l߾ǎãNÄ9‚sMË~X^^ŽmÛ¶áùçŸÇ¿üË¿`ûöí¨©©™’1éxPfßÇ›šš¼þº,‹Ë› ö1¨Ñh&ôÚ‰ˆˆ•D"�Z­`«˜5 ÍLÈRÀ;vìÑÔÔ䉈ݻw£¬¬Ìå ·±±3atëÖ-466zå ’ˆsç¢ÑãfëÖ­¨ªª‚ÕjÅÊ•+‘œœŒ¦¦&”••aÛ¶m°X,“~|û˜½�ߺuË«¯«¾¾¯¼òŠWƉ} ;v G�õøµQàaB–^CCd2²²²PSSó@ˆÓõ|Ç£Ñh|þ5qŽàA�«¾¾�)))سgöïß�ââb¼û¨¨ÀÊ•+ÑÔÔtßJP_ÔÜÜ<-ã§²²’_ŽBU,÷¢Ù† Y h���hhh@FF²²²Àã ¸‰�Môàm²{éééèììÄÁƒùæqŽàA3â7¿ù `ÇŽÈÈÈpºM,£¸¸2™ ã&§:ñéÍ19•¿'%%‹Å/ÖDD43²³³QTT„M2쵕••aïÞ½(..æM³ õ¢€fO¬dee!;;‘‘‘øè£�°víZ·Û755A«Õ §üÆÅÅ¡  ï¼óÒÓÓñî»ï:diµZ§äMFF °xñbáºÆÆFlß¾o¿ý6Nž<é´}^^ �¶€óçÏãùçŸÇ¦M›°yófaûÍ›7£¼¼UUUÈÊÊr9&"Μ#È›êëëa±X°råJÄÅÅ�¹�ý *22ÒåþZ­VHÔŠÅbäååá?øÄbñ¤ž�»ÇÛ´i“˶���ÐjµB;€ÑÛþô§?Åùóç@kŸ|ò‰0ž?úè#TUU IVûøÏÎÎv;ž+++ÑÔÔ±XŒ}ûö Ûdee!.. 8zôè˜ó ‘�T*…T*ED€¾¾ÜÜ\¾ÉptèkoGLd$är¹ÓíLÈR@«©©�L&–ì§$766º$*šššð³Ÿý V«ˆ‹‹CMM Þyç—ǵX,øÙÏ~†¦¦&¬\¹«V­BSS<ˆŸýìgøõ¯í”plE2™ Âs«ªªBdd$6oÞŒ¸¸8lÚ´ „L&êU«Ü&SŠŠŠ°mÛ6h4ìÛ·oR°�$ì± xr‘O%Gq‡'Μ#8GxYss3Ü7ÙïîöààÁƒHIIAQQ"##QSSƒ ±±{öì™Ðs9vì4 d2™Ëãuvv¢¨¨Hض¾¾ï¼ó"##…/EªªªpàÀX,bÕªUl_xŒN8ÛÇsVVV®\‰ÎÎNTUUá�wÞAQQ‘p_»ÊÊJ˜Íf¤§§°%o«…‹ŠŠ°uëVTVV";;{Üä¶? ’Èöج]&Ejâ<"Ž7"¢Y«ÀAøðC¤¦¦¢¤¤Äygˆ(PÙ¯q¬\ÉËËTUU¹lðàAX,ìÙ³k×®Evv6vìØ�•+Wºl[UU…¦¦& ¸¸X»v-***`µZ¡Õj]î3wî\ìß¿k×®ÅÚµk…OÇJ;{¥›ý²»ÚÅ‹cÓ¦M<-ù®àyIˆMLa¿!âÁ9‚sÄ4°'d'š@´ï�2™ {öìÁªU«„1”••%,vå){ºL&Ãþýû�oÓ¦MÂb}vZ­‘‘‘¨¨¨ÀÚµk‘‘‘�;v ==]¨zuü’cÕªUÂx;zô(ššš°iÓ&ìØ±ÙÙÙX»v-öïß™L­VëҚ྽Úr IDAT›o¾Áþýûñî»ï:UÎÛ‰Åb\ëQH‚ç%AžüˆKq¼Ñ=LÈRÀ²ØÙ,öDEJJŠÛ¾v HOOw©Zs<ØîäÉ“àrš¡X,FFF†Û•ÌGWψÅbÈd²I½¶Í›7#%%UUUc®¨NDœ#8GÐT›lïÕúúzaß]µmoËq¿ë566 IÔÑ�7ºtSS:;;Ýn[\\|ßJrûórœ+ìc4//‹Åå¹gddÜ·:=;;[HF=z”;Ñ,–nýúõ¸|ý[=Ûƒ ‰lÖ¼n‹Å‚ššˆÅbÔÔÔ¸@ÙÔì‰{Âbt¢p_dï gï1çÈždéìì¼oуœ¢8ú´d"Μ#8Gø¾¢¢"œºÚ‡¯Z,~ùü/^,ôY�ˆ[·n�¹OÛ¯›H²×^©{òäÉ1Ÿ�}¬Ù÷è~¶öß}¿qÖÕÕ…ÈÈH· Vûœ0ú –””�÷‡@l]ÀñFDDD4>&dœ\.Ç@˜Á×"gÕë¶WªX,8pÀí6555B²ÅÝAÚýÌ�;Wè çȱ_œ·Šíý$<(TqŽàÁ9Âw)•J4 õ øf¯_>{¢Ñ]ŸeG���n{±Nµääd·�Ÿžž>e¿×b±x­³½uÁ;ï¼�F3á>ºØã�ˆˆˆ²�>úè#À‡~èö j÷îÝhhh@}}=²³³… ûÊË£ÄF‹ŒŒD¿ÛS•§ÓæÍ›ÑÐÐ ,üCDœ#8G�7Ù“°Ž_X¸sìØ1aÑ<Çv“myànŒ¶ñèÖ ÑÙÙ‰††dee�™ÀMII¹o/{낆†¶. """š%ØC–NSS“°òX-öExìIÀVMsþüy—ÓÝ-î“‘‘‹Åâ¶7ãÖ­[ñüóÏOÙAçýØW‘æâ=Dœ#8G�·ÅÅÅaåÊ•èììDyy¹Ûmêëë]’±ö mÇ1eg¯RŸH·=1lï×ìèØ±cxþùç…ç—‘‘�ÈÈH—ö$öñ[^^>îx´!ãnÑ1ûc:.8Ùq‰ÊÊJîdDDDD³²pìGãØegg#22Òia{¥Ï¶mÛPSSƒÆÆFh4·IŒM›6Þyç§SŸ5 :;;‘——7éÓSRRpõêU·‹ �u h>DÄ9‚sy[aa¡°hÜöíÛÑÐÐ€ÆÆF466¢¼¼ï¼ó"##±cǧý0==]ئ³³‹UUUøè£�é²°Ýýökû‚XöqØZ%hµZDFF:-•——‡ÎÎN§mkjjPUUåv±¾óçÏ _¨äåå!22Z­V˜?ì�ÕØØ8nu­§ì­ ¦ë‹""ò/õõõ(++Ã\®•(0°eœšš�ìV­Z…ªª*TUU¡°°xûí·Q^^޲²2¶S÷ìÙƒíÛ·;%O/^Œ·ß~�»wïvzܼ¼îíU»Ÿ|ò âââ°gÏìÞ½eeeÂø·�ç©úÂñu‘£žž�F@ÃA˜�¥€b±XðöÛo{ÔËmÓ¦MÈÊÊrÚ6;;ÙÙÙBõL\\œpyôŠÉömíUA)))X¼x±K•LJJ öìÙã¶zÆ]RfóæÍÈÊÊ­[·„ç¶jÕ*dddŒ»jóŽ;�·íOúSáàÎñ`É~à6ÖTFFƸ¿s¼ç4ÖA«»Äû%Q<Ù& ?Œö¶¡§èáîOœ#8GpŽ˜vöýÊÓqeßÇÚ·Ç�§ûo\\œÇí<Ùv¼1ù ãÙ“ùg"s”/²àNoÚz�ˆPvF#âx#"¢±0!Käp€XUU…ƒ"==‘‘‘¸zõ*´Z-d2Ù„!"ÎDDDDDD4;E�[­[°r¹Üåv&dÜáÇq¹ù{‡ªX� ±ŒACaa!,‹Ð7Î.==ÅÅÅ“^€‡ˆsç"»²²2ôÜÆà­„?žÏ€q¼QŠð’HÙ‹/bÎ’%.·3!àÚÚÚÐÖÒÀvZ �¯¸¸………Bï¸ÙzŠ/qŽ Îäö9ˆˆã�ˆˆˆf/&d‰Fñ÷þmDÄ9‚ˆˆˆˆˆˆ|»M&d‰ˆˆˆˆˆˆˆˆˆ¦ ²DDDDDDDDDDÓ„ Y"""""""""¢iÂE½ˆˆˆˆˆˆˆˆ|Tvv6 :5Ä0D� Y""""""""%•J!•JÁPù�]úÚÛ ¹\ît;²Dä×ÂÛ€'‰ñTrƒADœ#ˆfP�D†°Ç6`í2)Rç1 DoDD³V'€ƒðá‡HMMEII‰ÓíLÈ‘_ ž—„ØÄ(•Rƒˆ8GÍ QH‚ç%AžyÜ\„ˆã�ˆˆÆÀE½ˆˆˆˆˆˆˆˆˆˆ¦ +dÜúõëqùú·8z¶ABD1Gô÷÷£££&“ ÝÝÝ|#}H||<ž~úib EEE8uµ_µX "Ž7"""š¥˜� pr¹aR_‹d0ˆÈïçˆþþ~\¼x­­­|ó|TGGƒ0¥R‰¦¡ßìe0ˆ8Þˆˆˆh–bB–ˆˆüµk×pæÌ‚ˆˆˆˆˆˆü²DDäóÑÔÔät]jÊÃP.N€â‘Ȥ˜;'œ�šA…%Z�ˆˆˆˆˆÈLÈ‘O»xñ¢S2vND¶nÈŲ¥‹"""" xõõõhhhÀ€Œ»ÿˆÈ¿1!KDD>ëÚµk¸té’ðsjÊÃx%ÿ9Hç‰""""šzzz`4I Q@`B–ˆüÚà™C8aÁÅ¿„¢¸¸˜ ÃÃÃhll~^ÿJ~´†�!ÎD>ꎥ ÃÆã8܆Ԕ…X¿~=ƒBÄñFDDn0!KDþýa´· =½@Cp®\¹‚‘‘áç­ùÿÄ ç"fÀ�Þ6´õ¡A Ç�� Y""òIW®\.¿øƒlȤ ù¼r«a @.—»Ü΄l€;|ø0.7_Ã`ïB+$–1(DäósÄ�7„êØ‡bÄXñÌR¾YÊÊÊÐskƒ·Fþx>BÄñFDDD(ÀK"d/¾ˆ9K–¸Ü΄l€kkkC[‹murëðBD~1Gtww —¥q/ ö9ˆˆã�ˆˆˆf/6›!×�Br¯B®¯¯�¡i1<<,\…„3 ³|Žèíí.+'0èÄ9‚ˆˆˆˆˆ縚! ÑD!Âå¡¡!„¦…c‘ó�Y>G8&ß䉱 :qŽ šfýýý÷æÄ0 DDDDSˆ Y""òisæ„14»?¬ñÌšŽ YQDBDDD4•ŸñrÙ)æ% —M&BÓ±g(+q8GqŽ¸Ç±*Ý©R˜È‹œ²as"""¢©<®fÈe§U‰ãxê0‘·8&ö‚e© ç"ÎÂëU—ÓDÓ5æ‚bä ÑTW34š($¢èn?�yÃðð°Ói¸<ðãAÄ9ÂáÚX&,dÖßß�›7or§ ¯êèèÀÈȈí‡ð(‰e Ñ ÊÎÎFQQ6Èd8ˆã3>C@î„Ä)…Ë—.]b@È«¾þúká²(z�Óé¹Ä9‚ˆs{¯*øâÅ‹Ü)È«®\¹"\žÿBD4äR)”J%ˆf8ˆü€VWÚÛÑÖÖæzLÍ‘;ÁÿFšOÀ:2ˆþþ~\»v .d`hÊõ÷÷£¹¹Yø94å{ºØcðä"1žJæ‚#œ#ˆsDàÎ!IOàö7تoܸ�ùóçs¡)wãÆ §Ö¡IË=¾o�D†°Ç6`í2)Rç1˜D^ÄñFDäÛ:€?Djj*JJJœçq†ˆÜ…D x�Jø¹±±‘}"É+ïíwÑ ì°`”'‚ç%!61J¥’ÁäAœ#vŽËüðRŽ9òªááa§1üðRˆ"<ÿ2C�àyI�'?¹œí‡ˆ¼þYÌÏÇ[jê½³?®ôÜñ¹çwý^·$ÎiD4õŸïKHÒ@¸íCøÈÈ>ÿüsüÑ”:wî:::„Ÿ'ZùFœ#ˆsÄlâøúûúúœgDSA¯× ýšE!áU<Ç ‘×8&9“Ÿ¾à¿¾î¶Ò~衇0wî\¾aD4¥˜� pëׯÇúW^CØcœVF÷„($a�¾Àƒ?òŠk×®9�†²({•o4ósDxæ:ÎÄ9bŠŠŠð�kˆ°Ç6Lø¾¢ˆ(„¦Ýû»ÜÚÚÊ1GSF¯×;}’ºÂïû5?Èx#"ï‹��.b¸�>óÜ*ÏŽ—YKDÞÀ„l€“Ëå�'?‚àyI“úPO‰ „9Â!AÔ××Ç9‚8GxQ˜b%‚b~¶ÒßßÏàЄܼyŸþ¹S2–_’Ñt™;w.~ô£ ?_é±b×ñ|zù®ôX§59{¥ÇŠº«wðnÝ0ê®Þëg»zõjVÈ‘wŽ£òèàïÑ0‡aãqá ùĉˆ��…J¥bO×ðð0šššpñâE§ëƒ^ŠPÅs~J$ÙD#sçqŽ ÎÓ$ãÏgΜ9غu+–-[æ31ª¯¯GCÃÿeïþ£¢¼ï¼ÿ¿0*?TftMÂʯl1‘Üiºë´)»­MµÑî‰f›b�5ßfµâ�ï÷îÞ{zÔó='Ùû|ƒ!-§½SÏ©¸Ù$­9�ZmŒ¹7óc¸Ó­ &�]tHt`F£óýg2à 0 030ÏÇ9ášëó¹®ë}]3 /?ó¹ªÕ/éž[_&ùßÏ”£qÛülMûÊF]ÿä=Ϩi`¤Åùóç}>f gHÓŒ%_c>7^#†år¹GA'©Ã‡ó®7r·/ÕmsÉY\®®/þˆíîM›Ÿ¥™9ßP\B2Å1f³Y?ýéOU[[+›Í¦††µ¶¶ª¯¯/,ÛÏÎΖÙlÖüùó•ŸŸ/“ÉUõ±Ûí:{ö¬$‰wHÀyO 0ê?–’5óKɵäkºvößärØ<£á€!¯›Ù)!Ëüìqí×ù¯ê½†éúÏ·fhçÎ�š×ðs¯q ÉJ¸ïûºÑù}nû³n^n䂈¦ÍÏÒtó—Çõ??n^i×õ³oë·ŸÌTö’t­_¿žBdª>ßòóó•ŸŸÏT¦<YŒéÀø{¾'Iºqùœnt´èfOûÀƒW/ÀÄòµ1;Eº-^Ó�š6w‘nKÉš°9 o:l²;$;eç5¼FÄøkÄmsé¶¹‹äú¼_76Ýh?§›}Iò=‹|ÎÒ$IÓ�º-5[ÓŒæ yι®\{6‡”0ƒ{‰çLn²Ÿ?çg�û¨&±ûÑú¤I,ñÁÿ‡"DPÜô~/” ÉìrifZšÌf³ßã²SÜoû[�û¤ENÇ5ÍÈY¡i³S) €˜}�ˆKþrÀåéw(cÑíZ³ªPþ~•ŒÆ9žÇ zJ§Þ;-W÷Ÿ'tߪÞ=­¢ï<¥Ýÿ¸Y{þqsX·�ð)--•ýêu9¯~®øÿö÷àù¦ ’žˆ‹SêºuJ¼ë.¿Ç d§8›Í&[óÀMt\×û)^#$|í>¿e56¨êÝÓªxù¸jÞ{™‹Â}C<ß@ì"�Äœª7^ô[æpô¨ð;O©öÃU¼|LÅ�­rÝHî'`rcöo$�ssÏV½{ZqÉ_VÙ/^Ñ⻿«Â‡žRáCOiñ=ßÕÞgå×ÞáèÑÆíÕÜEEžuç.* ¸n°Û>r¼JEßyʳýÅwWEßy*`غý'ûü¶½ý'û8Ù!LYCn\¨ó¹kùô ˈm>o¶Žî‚¢OúŒâ>1y^#\ýÝ©AÕ­Ñ«ù÷䎸îöŸìÓ†ï¯Òá—ÿ?IÒžg¥=ÏþJÃl•<ý}ÏzE«þ/Õ~Ø m?ú{­YU(I*ûÅ+Úóì¯ä’<£rGcãÓ{µliŽ¿òœŒ†9*ûÅ+:ú‡S*ùI©*þ×Ïzk¿¿KGŽWéáïxö©âåc*ûÅ+rtõèÀ/wOH_¼FL­:Ñ'}÷{dR<ßâ“5ýö¥Ã®ïêïÖ�‹£8ú¤Ïp÷ÅÏ7@ï­(A …-ëtc”o¬¯òÞ¸¿Y§OúŒTŸ˜<¯.—Kqqqv¬ƒ§ pt]Ñ‘ãUªxùØ@ ú£‘ïÆ½ìîŸðóÈ«¥ÊXºZ{ÿy¿OøYûaƒvÿãfŸàµð�ûTøÐSÚûì¯Tò£¿—Ñ8gTûoHž­#¯<çiWøÀ}2š uô�SžuªÞ=í c�¼Zê³m÷¾íþÉ•‘~Ǹ×wðuÁkÄÔª}Òçp¿G&Ãó-Î�6bàu³Ï1ªÑ'}†»Ïh~¾‚xoE ¦¶ùóçëܹs+[Ÿ9bû§þ8ºíÑ'}FªÏyóæñ„�Ñ׈P>ôTÀåé‹n÷ :‡Sò´h[üØjí}öWªz÷´ ¸Ï3íšïø­»fU�N½wZUï�öŒœ Öšïúícþݹ>A³{ÛÞ£u½÷óà+ÇuäUo¼F|!--M­­­¼æÓç”ï3)))âÏ·yóæ©££ÃoyVj¢v�p< Ÿk_MðÛ¢OúŒdŸf³™7µSœÅbQNNŽÚöí“‘rS�ì·nÝ:åääÈn·‡Ô~ÕªUã¾OôIŸãÝgRR’òóóyÂO¡×«Õð�èñ²Ûk´ªÑ0[ùwçüÄTn‹î°ìvI’£«çÖ¿W$I÷~í±!û©ýèì¨Ù`ãÚ�ÎJ’оóÔ�ë¸÷/žï±ò±sçNÕÖÖò{™>§tŸ&“)*žs»víRmm­z{{}–›L¦ÛΟ?T5¢OúŒTŸÑò|ÃÄ2™L2™LJ À¤Ñ/©]Rwk«Œ³fùýç�ì—””$‹ÅrûÕ«W�û>Ñ'}FsŸ¼FDÇymhh˜Ð@6”y[ƒábÛ=Ìö ¿vß„žãmÃL‰0QÛ庈•×~/Ó'}†7ÀX±bEÈmÇûxè“>£¹OÀøj“ô²$½öš²³³µk×.ŸÇ dA ðµ¹å‚¤/FÊ ³%IÅß_å7W«Ã1Ð~´óÇ+ÿî�º5‚{ÞXïm;ºz&dþXÀð¦QFï…_¾êó³ÃÑ£ƒ¯Wú¢Û=S¸§"(ûÅ«~í7>½Ws©êÝÓ²îvï?ÿÊï±²_¾ªÅwW/ãD@˜1B€T½{Z´WÛ~ô¨ºº¯¨ä'ûäpôèÀ/v{ÖYó�B•Ýýª^øå«Š‹‹ÓšU7÷ªxù¸Ž¯RÁ×îó½:^Ö¬*TÁ×îSÕ»§µöû»TòôßË�<[GÿpJ{Ÿý•–Ý�£âÇø¸#„�,!xþŸwhϳ¿òŒ25fëÀ/wûÜ Ëhœ£ª?¼¨5ßߥ²_¼¢²_¼âylÃ÷W©ìŸwNè>yå9•ü¤T_€Ý ¾vŸŽ¼ò'"€@3\Ýu›ª7^ ¸<ÿî\9lUž)òïÎ 8¬Ñ8GUo¼(‡£Gµ�rÝÂîóÛ¿ÁÛ´Îpûi4ÎQÅÿÚ£²ÞéÙvÆ¢Û™;aÕÛÛ«?þñ�ÊÎÎVnn.@Ì#�` ‚�rÀhœ3aÓDó¶Ûz{{UZZªÖÖVIÒ† d±X( b�,0J6›Mo¿ý¶ìv{È}$%%éÁd¤`ÊÆJÒÁƒ%‰P€Q°Z­ª®®V¿¤{n}˜Üd�Q:vì˜Îœ93æ~._¾¬Ÿþô§0å cÝ&c(ÛÝÝ­äädN, "ìv»Îž˜~jå¦Y „?2ÇC ?RD¿áæq8Œ½ãþ9êhìSçç’&W({ñâEuwwëý÷ßW[[›Š‹‹9ÉY` âçŽîiÔôV'…LI�ÂØ´¯&knf¢Œ™ úädç¤ e+**Ô××§+VH’úûûU]]-I„²Y` âçN×ì…3) æ ÆJÒm3§iÉ·æNŠP¶¢¢BÕÕÕZ¹r¥gY^^ž$éÍ7ß”D( €¡%H2»\š™–&³Ùì÷8�,Æd¤0Öm2„²Þa¬;„u#”@0Hz".N©ëÖ)ñ®»üŸF‰ª`ÃX7w(›à5åÏÁƒeµZ#~,Ã…±nyyyZ¹r¥jjjd·Û¹0jŒ�@HFƺEãHÙÞÞ^}òÉ'ƱnyyyÊÌÌÔõë×¹0j²µPÃX·h e»»»õøã�½~BB‚º»»%I .ä‚@И²£2Ö0Ö-Z¦/¸xñ¢'\­îîn]¼xQ \ �,‚6^a¬[$CÙãÇ�«¹¹9ä0Ö­µµUûöíSEEFD € Œwë‰P¶¢¢BÇŽÓÇ<æ¾’““µråJUWWÊ`DÌ! €MTëÎ9e+**T]]­•+W*++k\útßìÍ7ß”$sÑÆ…ÅbQNNŽÚöí“‘rS�,†5Ña¬[8BYï0Ö¢ŽïP611Qëׯç⌙Éd’ÉdR¥&�~Ií’º[[eœ5Kf³ÙçqY )P;mFœ:ûÔÙØçY–0o†î¸Ψû¿rñšìÿÙ«×nz-uù¬3^¡¬Õj�°0ÖÍÝïâÅ‹¹xbT›¤—%éµ×”��­]»vùdÀ˜œýä‚çûÜÜ\ 2¹o&ÈxŽŠžèëg¨›lõõõÉf³�©o³Ùì77È4JBõö»u:{þ‹@6??Ÿ¢À0d@Hì�WtìäŸ=?/_¾|ÈQ”ÀTeµZURRB!4¦,!©øM¥úú¯IøØúêÕ«) &¥òòreee)//�b¢ŽÕjUuuµú%Ýsë ÀäF FÅö™]¯VªõB‡gÙºuë‹I«±±Q)))•ìv»Îž=+IZD9€)�@íøÿþŸi $éÁ”Åb¡8Y@Tký¬CqŠ£ÒÛçTë…54~*{ÇÙ;{|_µjSÀ(È¢Ú¡£VŠ…U\\¬üü|Š^$™].ÍLK“Ùlö{œ@-11Q+V¬ÐŠÑÓƒ IDAT+”””DAó,‹Ö¯_O!à±@ÒqqJ]·N‰wÝå÷8�, ê¬^½ZÇŽ£Q"))If³Y999ÊÍÍ¥ ˜r¶lÙ"§ÓI!²€¨“››Kð l²²²d³Ù(Âb%€ð �Bd³ÙtòäI € È ¦566ª»»;¤¶6›M'Nœ ˆ�,bZyy¹êêê(‚›zed·Ûe±X|–[­Vuttø­¿|ùr™L& 0Y,åää¨mß>)0%Èp‹Ýn×Ûo¿=¦»­›L&-_¾\¹¹¹!÷±oß>-Z´Hf³Ùgyee¥þò—¿ø­o4•žž.IjkkÓÛo¿­ïÿû~í0ù˜L&™L&%P `Òè—Ô.©»µUÆY³üþ6#�à–cÇŽ©ººzÌýœ={VÏ<ó̘ú¸óÎ;Õ××ç³ì‘Gr}÷º]]]úä“OÔÛÛË €h“ô²$½öš²³³µk×.ŸÇ d¸åòåËãÒ�Ýn§˜@Œ0›ÍJNN¦�,ÜqÿÅÏݯɦ·:)0 effÊ`0„ÔÖl6ûÍ÷ ‡@€âçN×ì…3)¶nÝ:¦¹£€Ñ � ÊlÙ²EN§“BÀ4�]²²²Bž“2%%E›6mò»‹' :È0…$$$hÉ’%JJJ¢@X­V•””P�) ÓÊËË•••¥¼¼<Šˆ:V«UÕÕÕê—tÏ­/“#dˆ2���êîî¦@Ÿs]]]•ìv»Îž=«¿HrP`J � Ê”——«®®.¤¶ýýýjjjRoo/…€(D ÀréÒ%íß¿_6›�b@bY' ’Ì.—f¦¥Él6û=N „Èb±hýúõ $=§Ôuë”x×]~�È ¦mÙ²EN§“B ,˜C€(“™™)ƒÁ@!€0ÉÊÊRrr2…@X0B€(³uëÖ�oʯŒŒ %%%QHˆBŒ�` IMMÕæÍ›N`üÙl6�SOO�çgþB?²D™­[·*///¤¶ñññÊÈÈPRR…À˜Ýãõ}}}½š››£b¿š››U__p?'“’’].IROO�ª««£b¿œN§N�:åùùË·öãƒ@€)$55U›7o8q<€ñg³ÙtòäI ˜²ÒåƽõÖ[²Z­Ý'«Õª·ÞzËóó—].Ÿ‘¦“I‚¤Õ^Óœ={VÇŽÓ•+W"¶OÍÍÍzõÕW=£c“].}Ýk1vÜÔ 1­±±QN§3¤i l6›ª««µfÍ ˜²¾§³.—ºo…ruuuª««Óœ9s4{öì°íÇ•+W|>B/M�°ÐzÿùÖq\¸pA¯¼òŠfΜ)“ÉÖ}¹pá‚ß²Õqq>7ÃØÈ ¦•——kùòå²X,€$mŠ‹Ó1—Kç¼ÂÏžž¿€4œ²]®)~3.Ns%½#É=‹ìµkפá’|«¾é<Æ�,Q¦¼¼\YYY!Ï# €©Ãb±(''GmûöÉÁýH�ôH\œÎJúÀå’-‚£RÍ.—þ:.N9Sìcô÷KÊ‘ô–Ë¥6É3"9ÜR%-º5ò˜‘±ƒ@€(ÓØØ¨”””�Ú¶··ëõ×_×c�=Æ<²S€Éd’ÉdŠš`,Gò¡]’aܶQ’A’¦ð|¦ ß’Ô/©-ÌÛ÷Œ†eÎØ1é—Ô.©»µUÆY³üþ6#�` q:�jjjRoo/ÅÀ„2ÜúÂÄH�˜.`’j“ô²$½öš²³³µk×.ŸÇ d€™Íæ�n@,r�¨%dD¬#�@LËÌÌ”ÁÚø³ÙÌÍÀ‡’Žßúþî[7‹b�,bÚÖ­[e³Ù(Ä;Œ•¤�ââ$BYÄ0Y¢Ì–-[ät:)||(©{ëß­‘çx�è>Û$5X磸8µ»\Ê�‹‹Šý¤OúÏ>;]®aoŒF @”ÉÊÊ y´^JJŠ6mÚäwOL~g\.ÙF1ªÔ¬‘ƒ¤pö™0wºçMWçù~IR[\œ.º\2ÇÅEÕ~Ò'}޹Ïú'�` IHHÐ�wÞ©¤¤$Š„�ÕjUuuµ^|ñEŠ˜Pé/¾¨„çž“Î� ºÍ‚;”ž›;üûÇ0õ™0wº–|k®n›9M’<¡l\\œj¾ô%}}Û¶¨ØOú¤Ïñî3Ð`YÄ´òòreee)//�b¢šÅbQîÁ�·ùóçG¤ÏÔÔT�ó §‡±i_oèe?þøcUTT¨¸¸xÒ;}Ò§7“ɤüü|¿å²D™ÆÆF9�N%''S LϹ”” ˆz‹%êû´Z­zÿý÷=?c݇²ÕÕÕ’4d(;Ž�>é3XÓx9 º”——«®®.¤¶ýýýjjjRoo/…@XY­VÖÐÐ0n�lnn®V¯^MÁ1eÈ!2›Í!Ý ±‹@1-33Sƒ!¤¶f³™›�€°²X,jkk£“Ø4J€X¶uëVn¢& ‹Å2¦÷./½ô’¬V+…Œ FÈe¶lÙ"§ÓI!Œ»––Ùív AŒ� Êdee…<'eJJŠ6mÚÄ]S JÈ0…$$$hÉ’%JJJ¢@X­V•””P�@1­¼¼\õõõL Ï=÷œ~ó›ßPˆIŒ@€(ÓØØ¨îîn „ñ9×ÕÕE!²D™òòrÕÕÕ…Ô¶¿¿_MMMêíí¥�ü<ñIJX,"‚d˜B.]º¤ýû÷Ëf³Q ~ÒÓÓe2™(DÈ@˜L§@h,‹Ö¯_O!4YÄ´-[¶ÈétR0)ìÚµKgÏž ¹}KK‹’’’˜¶ ‚˜²€(“™™)ƒÁ@!€0ÉÊÊRrr2…1ᥗ^’Õj¥ÄY¢ÌÖ­[C¾)W||¼222”””D! 1B€)$55U›7o–Ùl¦@Øl6�¢“�,Q¦¼¼œÿñ0!-Z$“ÉD!"ˆ@€(ÓØØ¨®®®�Ú¶··kÿþý!ß ÀÔöä“OÊb±Pˆ"�` q:�jjjRoo/Å€(4�¡1›ÍJNN¦�,bZff¦ CHmÍf3ùae±XÔÖÖF!&1¦,@LÛºu«òòò(˜,˘޻¼ôÒK²Z­2‚bb„loo¯ª««Õ××7¡ÛéèèÐñãÇ>–––¦üü|®8Àˆ¶lÙ"§ÓI!Œ»––Ùív A1È–––ªµµu·c·ÛuìØ±!ß°aiŒ(++K6›-¤¶)))Ú´i“Ìf3…€(S{§é„¹£Ï§o›| 'z„. Z²d‰’’’(V«U%%%A‹‰²O?ý´JKK}Ñ9iñJœç{øó²GÝwâ¼éJûj²®õÜðY~ù?{uóºËóóòå˵bÅ ®8€(S^^®¬¬,æ‘“ÂsÏ=§ÞÞ^=úè£c’Љ²f³Y;wîTbâ�ëÕ¶kJ^” ù³=_3fÝRÿs3}ú¹võ†_[\\ÌÕJcc£º»»)Æç\WW…@XL‹•ÊÞ¼îÒ'';Ô×ñù¸nÇö~—çû=?ÆF«¼¼\uuu!µíïïWSSSÐÓõˆ-O<ñ÷8аi±t°ÊÆ"íÒ¥KÚ¿È70µ¥§§Ëd2Qˆšk¾uëÖ�áøøxedd())‰Qh%ýô„±0õóI‰ÁfÌŠü¯ÕÔÔTmÞ¼Yf³™“„�ÍfÓÉ“')‚F {K°¡,a,L^«W¯ÖòåË}ΈSê=³|¾²V™tÛÌÑÿŠÌ\9OwÜ?ǧ¯„¹¾ÁnZZ¿7€(ÓØØ¨îîî�ÚÚl6�8q‚"€°±Z­ª¯¯§“ØtJð…‘¦/ Œ�v»}È ®‡š{2gÏž ¸Üd2 ;¿$€Ð¹_·«««% üܵ«7dþêØçl�1ë6™þê‹©:Ï÷©¿ó‹ÿØKKKÓÎ�;™Nˆ2åååZ¾|¹, ÅQÏjµª··Wyyyc’"�d¨Pvö™ê¶9=ëÆNí¶ƒ†e[¥¥¥C>ÆÛÀÄʺÿ³m>ÝÕÚ¾´··kÿþýÜ1@@O>ù$Ÿ Š0Ù! eÝc§¾Õ«Wû�÷³¦))u†ÏWê=³4cÖèÙ´¯&ûõ5øF@‰‰‰Z¿~='˜`ÊŽ5Œ+§Ó©¦¦&õöör‚ 1eÁ0O_@›ç]’n\s)½(9¤»®673Qs3¿|û:>×'';ËkkkUTT$Iª¬¬ô9?Ñ,ØýÌÈÈPqq±¶mÛ6áaóhCYÂØÉ¥¿sà|».¦¾Á¡ìD‡±ÒÀh½åË—Ëb±ŒþîïWSS“± ÀÏO<¡¿ú«¿¢4=Ù¸q£jjj"z 7nTEE…*++ýÙá”””xÂØôôtOÛÑô.µµµZ»v­š››ýÙ©®¹¹Y{öìQEE…jjj¢&”%Œ�|n^wùn€äÊJÑ;MÁ¥K—tèÐ!íØ±C¹¹¹œ<„Õ…ïÖm3™ 𥧧Ëd2Qˆ K [[[«={öhÏž=;ÐŠŠŠ!s�Þ 4Š÷èÑ£ž‹µ¶¶Ö'è®]$9rÄgZoF£Ñ3 Äd™®ÀÛ²eËTVV6äõuäÈ�:uJÍÍÍZ»v­*++'|ŸF e c'�ÜÜ\%&&ª¯¯/¤ö‰‰‰_1À;”eÎXà ÞσþÎ’nPbÜÍë7uµíÚ¨Ú\»Ê{ÄŽéáÚÐÞ½{µfÍš¨œku¨ O˜Àý‡øà s¸vÑ&???j¦U…Ñhrú‚ÂÂB•””¨°°P§N�RUU•jkkÃr­ ÊÞñ•9úìO=„±“HYY™BjK;Ü7ú2›Í„±€×{ ÷<õc1oÞ¼Iyü‹EëׯçBÀKç }r²“BC˜ð@¶  Àsc¦h˜ºS—{.`i`¾àp…ÿ�BYﻯÆN«à:FÏb±(77W—/_ž´Ï­-[¶Èétr2ƒñ°Àà‡áíÚµKgÏž ¹}KK‹’’’˜¶ ‚&<�-,,T~~¾^xá…q™º ¹¹ÙÓ—{ �ŒŒ ­Y³F6lðÅê¾I˜ÛÁƒ=¡�{žÕŠŠ µ´´(==]ÅÅÅjnnÖÁƒ}Ú�:uÊÓWAA� ýÚrôèQUTTÈápxFmfddh÷îÝCÎCëp8tðàAUUUÉápøÜŒ,??_Û¶mói[UU¥S§NyŽËû¸Ýûê}L6l¸íÑÖÖ{ÛÞµÛ»w¯š››UUUå9ÿƒ÷y" 7ƒ{?½Ï{ ƒëL}ÜÇçʺÆEff¦ …À¤a2™&õú¬¬,Ùl6N$c`6›µ|ùò1ÿ'mRRRT~ºz*y饗´jÕ*­^½šbDHœËårMHÇqq’¯’’åçç«¥¥E’TSSðɵgÏO h·öîÝ;l˜k4UYYéÓ·{?qoÃýQ÷‚‚UUU©ªªJEEEC¶Û½{·öìÙã×ΛÃáÐÚµk‡�&àùçŸWII‰Ï²ŠŠ mß¾Ý3UÂPÇyàÀ­Y³Æ¯nCí«÷1UVVú…�#m×h4êðáÃ~íÜÛ.((Pqq±6nÜ8ä~>|سϣ½ŽÕx°ŠŠ Ïö_c#][�®[ïk­ªªJk×®ö¼8p@+V¬ð„²„±BÕ××r8ÔÞÞ®wß}W�?þ8¯?@žsî?@)$›±Œ�---%��°°Ü×h4úÜTk¸Ðn(Þ#kÓÓÓuàÀUVVª²²R»wï–Á`�ÃáPQQ‘jkk=íÜë¸=ÿüó~ËËÏÏ÷[gÆ žeC�†õVTTä ½Û>|XË–-“$mß¾]GŽñ´©­­ÕÆ�åp8üޱ²²R6l�4öz×°¸¸Øçqïãf_ÝA¦{»Þ5\Û¡BÑ3gÎhãÆ�2 Ú¶m›>ì·OîmL„ªª*mß¾]Ò@x;žÿ›æ×‡–-[æs^8 ôôtÏñ�?^;wîÔªU«cDDjjª6oÞÌë&6›M'Ož¤ZØnêUXX¨mÛ¶…4u�ûcð’´lÙ2UUUù|<½°°PkÖ¬Qaa¡'úˆBLba dC�ºÀ}3¦eË– {c(ï0l¼GI«¹¹Ùž®Y³F.—KÍÍÍžc:räˆjkk‡ <'‚»NéééÃŽªõU‡ »‡j?7ó2 *((øå¾ñ�{Z…P¦ÄŽ÷ôEEE:zô¨_H[\\ì™+ƪ¼¼\õõõÀ¸[´hѤ¾!ëTpÊ‚·ß~;èÑ«#©®®Vii©Ï²x@ï¾û®jkkõ­o}Kßüæ7=ëºy·qïKrr²__ƒÍ�;W���:~üxÀ��:tH§OŸöYÖÚÚêùw¨þG vçÏŸ÷<ÞÞÞ>âþçüùóêëëÓgŸ}¦ÎÎN}úé§úì³Ï†<–¡ê7x¿¼ÛýéO¸¦Oq_333uþüy½÷Þ{žu½·ù»ßý.¤ë!óçÏr²éÕ«W«££CG�U}}½***TWW§G}4¨Ú³ŸwÜq‡>ûì3ÕÖÖz‚öÌÌL-]ºTK–,Ñ�wÞÉ« €qÓØØ¨”””�Ú¶··«¢¢B³gÏV||<ÅÂd,ïùFÃf³�)P-**RuuµOVËòóóµbÅŠ°nÓ/�µÛí:tèиmÀn·ûÝùmÉ’%ª©©Ñ•+WôÖ[oiæÌà*ª‘ IDAT™ºãŽ;d·Û=ëº[\ooïˆw‘KHHv]›Í¦7nøõ;Rÿ�Ž#P;ïÀ4жFòÙgŸ©®®nÈ�öÏœ9S×®] Øÿpõj¿úúú$ Œ^©¶�Žw¤sLƒ̹ÿêW¿ª‹/Ên·ëÏþ³²³³5gΜqÙÏ¿ýÛ¿UuuµÏ²óçÏ{‚î9sæè¾ûîSNN¯d"ÊétÊn·û¼î˜xc¹Ó1Àh¸\®1µïëëã½Ë ÷q999a½�ß”—/_žð�ÆÇÇû|¼ýÔ©Sr:�ãÒ·;¬œŒtüøqO;sæLÝ~ûíZºt©–/_®ï}ï{žÑÄã­§§gJ<‰¼§GÏ0Â}ÍnذAJOO×Ì™3}êWUU%«ÕÊ+1Äl6‡t30D÷ Äp™>܃ s§ëöûç„Öó¯þ1f&hñ7çú=¼XsÕ麨÷ÿðgõôô¨ñr½Œ™ Ò­Oàû´¹Õ—kŽ3`_Þì¿á–ü·;öqûýs´8Ï·�„÷§K¤„yÓýûæ8µsÕ÷HÇoµùÒ -þÊÐûûYS›îX¼@’Ôwµ_ÿòòÿè7)^Oü÷ïiIÞ"¿6ŸÔÿÅÓÿàc9ï¢~ƒöË»�ñp²—ºu}F߈µí?2pqzïpÛÍõ0R»€ç&ÐȸÈ3à ãÕQíçgMmžï‡ÛÏ»´PÒßxÚüGU�NW~¤þ^§êêêôÐÓ_ÓÜT¯fBÖôVç˜ûXøåÙJœ7ƒbA¸tºß3'ýh™ÍfÝõ�4]ºr�B€°èùÌ©»Óî¹}ÂÜé£Ëf¦¨ ÿÞ£þÎÏ#²íaÙi3â4{áÌ1m`æìÛ†ìã»[ŠôŸ§Ï©ãb—Þÿß•}oºç1ï6Y÷¦«±¦EÍ ­ÃîO빋žïdÎ ¸nâ¼é~Ëo›9ÍóïPý:Ž@íîY˜%í¾µ?MŸê¯¿»4`ö ýüÿ®�$}ïÇß”i¡AýWF ÿ]É·tÏŠ¬€ízÿãêÇÛò©Ïz3gß°~’”xazÀä~9CœøPÿrIÓfÝTÒœ„!÷·³½K’”þ¥Û=í‡Ûæh¯‡á wn|®�¦O=ßç=°dTûÙöjÀýWÓp½¿þö@ÖqÁ¡ÿóÆ™!û;qàÏ÷÷<�p�¡¶1ž²n˾wV½=ý×ùàÄŸ}µ_ìòülº}è:qfœ÷5C’Ô×Óï³OƒUúà‹ó1(”Œ­ç.êƒ7?ôü|gÖÂ!× xN¼Úz{ã×§ôƯßñ¹¾ü®åÙÜ8ÀøYö��›Àx»øi›®õÜ 4-Ò;�}o† ùʰëÜó@®æ-)_ÿù[:WÓì·Î¿>ó{}øNƒ$鯿}�L·öõicÛ„ÓC?øº¤�ùågïÊž«iÖ‰_„{Y÷¦Ët»QiÙ <�W½ö'¿>{{úõ¯Ïü>è@y¨Ðq°¿yh™îÌØöë?{+`à]õÚž}ºû�\¥e/ ûuÒ¥_çjZ†üúÝÏÞÒÏ~ü’únÕúÛ?øºÏh_ï€þågüÏɉ_Ÿò\?ƒyÿ‡À¿Þ;øÜ¸ÃÚÄÙñCÁ,ãí³Cž“2%%E_ûî}Jœ7�B@ŠŠ¿Ö¾ýƒ}ønƒ:¼F‰zKš“ >»N?Ûú/êëé×϶¾¤´ìºç�\õ^q´½à�$Ý™µ@ßûñ·üú˜·Ð Ž‹]úð�mýÚÿ+Iúù{?��ãq‡ÌU¯ýI¾Ó s5?×=ä*qv‚kšÕzn Nœ¯ÇÿÇw=mÜS3|øNƒ~¶õ_þOߨí§^~æ÷:õÚž‘³ƒkëÞßpk=צŸmý— ÖýëoßãwÌiÙ =õm=צ=ë~®´[Atkc›úzúugÖ%ÎIPã ÐûoZ¦NœQcM‹>xãŒÎÕ´(-{�Ò²è\M‹§½$}ïÇßrÚ‡„„Í¿cžºz[)V«UÕÕgt÷“ (‚2-vbðÔ�¤e/Ô�þ¤g:€Ösmzã×ï¨êÐê¸àPâìx>òý¤bsÀ@ìï~üM%úXy°£HCñwÛ¾¥>óˆgÇLðÆUúÀÆfÝ›®ÿ~`³ÏHÞ>³Î3Zõ\M‹Þøõ;žc”Ë¥oÿàëÚöó'¿Kß;ë³Í¿yh™§>nÁŒ¨MË^¨=¯ýXwßEÚz®MU‡>ðÔV’ ùŠ~üó'£:l¼û�\=ö?¾«Çÿéá€�ÿð™užcìëùbÄm_O¿î~ W?þù“CöýÃgÖ鯿}�¤�‘²¾Ó 7~ýާý¼…ýð™Güæ˜ѯö�ª¯¯§`R8ÿf‡~ó›ßPˆI,Îår¹¼444hß¾}’¤¤ÔÊ\9/¤ŽÝÓ Ì[hrú€ÁZÏ]Tß•�‘†Ù·BÇ@ìúðݯuÓugÖÂÃÂÞž~}ÚxQ}=ýJœ“àiãÞnâì¿�ãwõóöá;ÿ¥ÖF÷¨Øeß›>ìú­ç.}^Çç]û‡:.ºG­úw빋!õœϾ¹�}¨6Þ}Ÿ«iñôŸ–µ@Y÷f »¾{ÝáÎY(׃w»ás~g'èžr=ûä>§Cí§ý‚CŸž»è9Ÿ�ÎŒÕ4ûl}åŽCž¶àüÕu¾÷C é£iÓòåËe±XFÝv`„l5#d@Øœ³Csg¤êÑG ©}ii©Rù³c¾Ž½í×%I;vìPnnnض=a�,ÍX¡þþ~�±} Ž¤Ý6sÅ&ø9G Âm¬�lý'ªyZ�f̺-æë©@–¿Ô˜B.]º¤÷~ÿgõu|N1ø¹=mȦ±‘F a2�¡±X,š™×«Žk)‚B €˜¶ì;™ZzÇR &…Ì•óôÍ”GCnñÓ6]�»Á´Ä”DÃÂY2 ãí³•œœL!@L8ñúÿVǹ> AŒ� Êä¯ÊRž!/¤¶ñññ2ÝnÔm3ù?WˆFüµÀ’ššª¾_‰óø?W l6›ZjÚ(‚F €˜æ¸pEÝÝÝ!µµÙlj>Í ½@øtžïS}}=…˜ÄdÓÎüá¼êêê(˜:Îõé£�>¢“ذŸgìïü\Ÿœì J„Qã•.%>p»òòB›G¶ó|Ÿ>ù�ßß@8ñž„Kçu)5ôö)©)êjîÐÕ¶k1^ÇÏ#¶íaÙ›×]ºÚv�+€°º®®®®�Z¶··«æƒuµ›ßß@8ñž„‹ËåSû¢Â":tHÎî3Bü¦,ÈÍÍUbb"•`r:�!Ï… ±&11Qf³9¬Û 8B¶¬¬L œ"`ß¾}cîã‘G û› ŸsîçÙòåË)$‹ßþö·cjŸ––¦;vPÈ[rssþÍéÑ´3`|˜Íf~—AÊÌÌ”Á`ù¹f±Xø„›o|ãjkk ¹}RRƒ7"l:%@,Ûºu«l6…QÁn·«ººÚoùòåËe2™d±XtöìÙ�û饗tÇwhþüùžeóæÍ“Åb¡øaB @”Ù²e‹œN'…bпýÛ¿é�ü£ßr£Ñ¨ôôô1÷ßÒÒ¢––Ÿe‹-òŒšMMMU||<'bŹÆzk60®úúúB­×ßß/§Ó©¼¼<%%%QL`‚ŸsÒÀ´LYÆKoo¯ìv»f̘Ööž8qBwß}·|ðAN›F ˜:´dÉÂX L¬V«JJJ(7î9^.\¨äää°lóĉúøã�•��À ¦, Bìv»ìv»ßòY³f�KÿCÍ+•““Cñ/åååÊÊÊR^^ÅQeáÂ…’¤ÖÖÖ gÝaì† ˜G6Ldˆ€ÎÎÎ!ç†*,,Ô}÷Ýrß½½½’¤ÒÒÒ€�ÿò—¿Ô´i|HpkllTJJ …Qéâŋڿ¿V®\9îÿ�L²D@RR’²³³}îlêf0ÆÔw__Ÿúúú´nÝ:¿ÇæÍ›G L"999JKKÓ›o¾)IãÊvwwë“O>!Œ�Y" >>^K–,Qyy¹ •šš:îÛpß%U¸ÙWFFFØæ 0>’’’´sçN•––Žk(›““£gŸ}–ûODCdˆ�žž]¾|Y‡R{{û„mçĉzýõ× c€IÊʺGÊÖ××�©?÷ Ãc#ƒ@€1›ÍÚ¹s§¦M›6a¡¬{N¨o~󛘋EeeeL8w(›��­Å‹‡ÔG}}½'ŒEäĹ\.e rl6›JKKuóæM­[·nܦ/`‚~ 8}ô‘œNgȘ˜Íf%&&RHV/^Tww÷¨ÿ>رc‡rss)`1B€ó);Ö� ~³E Œ,++‹Q"`ÒÍHWï¿c#�²D »Ý®Ù³gËf³éæÍ›cꫪªJK—.%Œ‚Ð××'›Ír{FÈ€Húå/©… y£/kDŸé”€è`2™$ „;c e.\¨Í›7SP l6›>þøc­Y³†b€°ëííÕåË—U[[+I~¡¬Õj%Œ�BŒ� 9�N�>}Z7oÞÕœ²LÐŒÞXæ�µZ­ª®®Ö‹/¾H!@Dôööª´´T­­­Z¹r¥O(k0táÂåççS¨(2a#dO�:R;ƒÁÀE2ŽõOOOWFFFÈý477ëèÑ£r8žeF£Q?üð˜ú />>^¿ÿýïÕÛÛÔ�¾ª««µråJÂX åååZ¾|9£FÀ¤”””¤�;wª´´To¾ù¦¤�‘²îÁ , HQfÂÙÂÂÂ�Ú¨ªªjÜöãàÁƒZ¶lYÌ…¼îúïÞ½[{öìuûªª*íÝ»wÈsQRR¢üü|=ÿüó!Ÿë‰ÐÜܬ#Gލ¤¤„g7€IïþáTZZªC‡ ʺç„Z¶lo¶€äÊÖÔÔhÅŠ ÖˆbÓ¦ê�9©¸¸Øgt'FVQQ¡¢¢"Ÿ0¶  @JOO÷,«­­UQQ‘***¢b¿ËÊÊtï½÷êÈ‘#œDS‚ÙlÖÎ�;5mÚ4:tHííí~ëxOÐÏ'L€Øåeò“ŸÆF¹ ¿©×²eËTVVôúF£q\¶[[[;®#mc…ÃáÐöíÛ% LQVV¦ââb¿uÊÊÊTVV¦®®.mܸQùùùŽ9Bø`Êq‡²î‘²›6mRBB‚$î– Œ§þþ~¿eíííºvíšßòùóç{ž‡Ñ$))‰"LÈ�ƨúH;†WQQá 5+**Þ1Øh4jÏž=ÊÈÈÐÆ�% ŒN�–‘²0Õ¸CÙ¦¦&%%%éæÍ›ª¯¯'ŒÆQgg§L&“ϲßýîwjnnö[wÓ¦MºóÎ;% ÌÏ6^ ¦Sxs�*6 ÃXoÅÅÅ*++Ó™3gtôèQŠÈl6Ël6ËétÊf³iÅŠúÒ—¾¤ÜÜ\ŠŒÑŽ;”””äÈ>þøãêíí ø|t�>1™Lºÿþû)"‚ÕsÈ–””¨¨¨Èo>Soî¹bÝóÅJRQQ‘çc÷’´}ûv¼Ñ“ÃáÐ /¼ ¢¢"ÅÅÅ)..N÷Þ{¯¶oßpD„ôÅ«îþöîݫŋkîܹZ»v­g_ÝûU[[p;EEE:xðà°5p·[»v­/^ì³�7n÷iÜ£c»ºº‚Z¿¸¸XÛ¶mÓ¶mÛ<ËÊÊÊ<Ç>T ŸãÁçæÈ‘#Z»v­æÎ�ëS¯^xÁoZwgΜ‘$�9sƧöƒUUUùÔÓ}Þ†;ƒûó>—ƒÏ»»ŽÛ·o×½÷ÞëYgãÆ�CÖ#Ðõ±xñâ÷ @ì‰��×’%K”œœL Œ“ÜÜ\™Íf¿åf³Y¹¹¹~_|câš ’\’\!÷QSSãé'##ÃÕÙÙé·Naa¡g�ÊÊJŸmþ¼/555.£Ñ8äú’\ðÛæîÝ»=ý=ÿüóC¶qÿ|øðaW~~þ�ÛÈÏÏòøGÚ¿¡öÑýØîÝ»GUsïã)..óy{þùç‡\¯³³3àzÅÅÅïÑhtÕÔÔxÖ/((r]÷5áÞÞš5k†í»°°0àuì¹<|ø°«¦¦Æ•‘‘Ô¾»\.WSSÓ�ë{_#�ö “KT².—o@¸mÛ¶!ó+++}{þùç]•••>A˜whh0<ë455¹>ìZ¶l™OÈæÍÈfddxÓ‚‚WAA�Ë`0x‚3ïN’ëá‡v>|ØUYYé:pà€+==}È಩©ÉÓÎ`0¸8à9†ÊÊJÏ>¸¿šššÖ´�lgg§Ë`0øá{öìñ Gâ>¶¡ÂæÁçÏ]³x–=üðîÊÊJWgg§ç˜Ýû–‘‘ás.+++=çlÙ²e®ÊÊJO[7ïð¾  Às¾++ÿÿöî&6ªêÿãø5 Mf:Zt3jä¡£&ˆm S]š–&ÒQ·´¡†¡,Ä-jš�Lc\´ÓqÓ΄®Ì”'�dF…S„ ˆ¡½5”Âü¿ÿ=ÿ;Ó™ét*üÞ¯¤qz>÷Üsn]|9sî±T H+Êæzž³�¥s,ÊÊÊLquûöí¦µµµæ˜@ �–m·Ë9Îö=;Ï›éXàßgÎ ²vAoº?Ù8g@Ú3�ÕlEßcÇŽe�%isÁ²oݺe |�'­°ç,Àå:ßÙ¹f±:‹Ÿ™÷°}ûvsn®|gA33ÿ~ŠxgΜI+Ê:‹‘UUU©ýû÷OY u¶-³Xl³gšÖÖÖNë\…|gnæ¸æ;×YèÍ,ìg;&WæêSçxå3»}�Çl»téRÞgÄyž³ €‡Óœdgú“Í¥K—&ÍŒ´‹yÅÅÅY ~ù ²ÓýJ½3ÃY,sds÷œ}ðÊ+¯ä<Æ9ÒéóÏ?O½ýöÛ“fSföK®"áýΪ¼uëV*d-Ì:‹íû÷mÙúȹß9Ù.>f›¥êì{v«S¾‚¬ý¼,[¶,ï}ÛEøÌ™½ÎüTÏJ®1s>7S=c™ÙöÌY<Üž˜ë5j‹‹‹µzõêûÊ(++Sgg§6mÚ¤¡¡!=ÿüóæåH���*++›Q^86ŸëêêrWUU¥ââb�ŽŽêøñãæ¥a™ÇL%ß1«W¯V$™´}Ïž=󺶰ÇãQgg§FFFtüøqóc¿8K’†††ÔÜܬÎÎN;vL�'mÌjkk‰D‰D´ÿþ´|û÷âââ´1(++Ó‰'Ì‹·>üðCÕÖÖÞWߌŒŒ˜—qåo{ÿï¿ÿn^Äæ¼'{¼¦ó¼N—óûä“OôÇ(¤eLçÀÃaÎ ²«W¯N{ûülÕÕÕ™Ÿ]ŒÝ¾}û”¶lœíùè£�ò»`ÁI2×Ì”Y°›í1S9q℆††444¤�� älÓƒäñxTWWgúyddDápXÇ�WWW—$i``@ëׯי3g&�™=^iÅL»�YännnV8Öèè¨Âá°Âá°<�ªªªTUU¥ÚÚÚàíb¬}]gQ9“³O&C§sí™þDss³¾øâ �ŒŒhÏž=Ú³g�ÊÊÊTUUež{<²dtèС¬'466jùòåæ÷ .¨££#çîÝ»§ññqIR0ÔÅ‹g�Y^^ž6›tË–-úꫯ²fVVVšÏ:{ö¬É<þ¼Ù7ÝbñùóçuöìY-_¾\wïÞ5Û;;;uøðá´c_xá555™ßïÞ½›³��?þ¸ù<>>žvï7nÜÐéÓ§s^Ÿ~úiݺuK’Ô××§k×®™þt^ÛyïùÚ9�1zê©§´yóf­]»Vÿüó�~øáݼyÓ/ß{ï=“¹yóf577kttT›7o6ÅÍ«W¯š{Ú²e‹y>.\¸ C‡Éï÷ëÔ©Sæ~ì"p8Vss³Þzë-•——g£ÌgîÂ… joo7ÇØEíéhoo×áÇÓú³´´4k^½zÕ|^´hQÖgþ·ß~3ÇØÏ’$}öÙg×Áƒõ÷ß›vvvvª³³S�G555úòË/åñxîûïh6ãN&™d’I&™d’I&™d’I&™d’ù¨e~ýõ×Z·n�ü~ÿüd¯\¹"Izíµ×&훘˜0ûíß³÷í·ßN:~ÅŠzî¹çf�ùÍ7ߤýþñÇkïÞ½93mׯ_WII‰É­žž³¯¨¨H/¿ü²***äõzåóùtïÞ=Õ××›B¡}Î{ÍÙŸ™íŒÅb:}ú´&&&ôÎ;﨨¨(ï½ûî»Z³f�>ýôSIÒ“O>©+V¤·aÃõööjxxØ´á»ï¾“$­\¹RÏ<óŒ9ÞÙΚšS�¾xñ¢âñ¸ÉíØ±cÖÁ«V­’$UTT¨»»û¾ÚÕÕ¥ÖÖVI’ßïW¿©‚gV·íâbCCƒ$) ¥Í˜Ý¹s§z{{%IçÎ�›q[‚Á ©´ç;ßîƒ\mÌ—õꫯʲ,¹\.8p ­ýÓ¹Æt®�©§§G»ví’$8p@ÕÕÕÓ:Ïnk¶±N$f¹;Ó>¾¥¥E�@`Úý�FÕÓÓcƾ²²R¡PÈìߺu«âñø¤v8Ÿ…¶¶6SÄžÍó<›çm¦Ï�Ͳ,E£Qutt(™LJÒŒû Ùµ··kãÆ�ª©©)èu˶±¦¦F»wïþ×tÎðð°)dUTTèàÁƒf*qGG‡b±ØŒò|>Ÿù�FóÛÑÑ¡ŽŽ%‰‚Ýo,“eY’¤M›6å,ÆÎô¾d¿Ø,Ë2m­¨¨Èši//�F�FÓî-3«··7gWWWëàÁƒæ:Ó½gÿMu_±XLêíí5í,ijmÿcƒ“ÛíV}}}ÚK膇‡ù¿%ÀC챇¡‘���f¶h[[›$iß¾}r¹\’¤]»vͨxæ\Â9Ã2SOO�‚Á ‚Á`ÚWæ ©´´4ç¾|mŸ ŸÏgŠ�½½½æ¥]ùØã!e/ÈJ23RûûûMÕï÷ËívO:vçÎ� ƒiË5d²ÇÝþïtØÅßþþþœÅu˲´k×.3æÙÚ÷ õ÷÷+ ª««kZæB´ sgÎ ²cccŠÇã3úqWƒÁ ) 555™¥ÛíÖ¾}û$ýw½ga0“ýw[ii©ùzy,Ó¶mÛ&tc±˜Ét¹\“fsÎ%çLU{i…L­­­ÓžÅ:---¦ÐÙÚÚª†††I³E-ËR¿Lá´¢¢"çL^»ï,Ë2³=³-àv»M±< e½¿h4jÆ3×’ ÉdrÒLR狹&?-ËRCCƒYÀyü\r>WmmmYgÀN§è €‡Ãs}�D"¡­[·Îè{ ÎD"‘¶TAæÚ™ÕÕÕf=Ùžžùý~S¤«¬¬”ËåÒØØ˜ºººÔÕÕ%¯×«£G�J’vïÞ­x<®ÁÁAE£QÅãqù|>¹\.%“ISv¹\êîî.èÌD·Û­††…B!% mذAÕÕÕr»ÝJ&“ækÿöRƒƒƒlIŸÏ§––µ¶¶jllL±XlÊ™›ååå:pà@Þû±Çɞ霫˜ÚÒÒ¢x<®±±1mÛ¶M>ŸO^¯W’ÒÆÅëõª¥¥eR;âñ¸’ÉdZa·²²R¥¥¥jkk3³©äóùäóùL®s)…Ù¬3;Û±¶Û•H$ä÷ûUYY)¯×k Ëv‘8ßò˜™>ø@/¾øbÁ¯›u†ì¹sçôã�?Îk‡X–¥mÛ¶IRÚR™ò-]�Y°³ [¶H$¢ÆÆF¹\.Y–¥X,¦h4jŠ~öË¡œ3V ¥©©É“ɤºººÌWùS©”‰DLÛ~ù嗶橽né¦M›ò. `E#‘È”kg�3ßlãÒÒRuww›™ ‰D¬=k�‹ßïW(štͦ¦&S¤¶9—š¨¯¯W(Jøèï8IDATËîéé1köº\.566š™×…R__¯¶¶6Ó×±XÌ´+™LÎ[»eË–-SIIIÁ¯» •J¥279rD}}}Ú±cǬƒïç…Sv‘Ñ.À¹Ýî¼EQç,B¯×›¶îªeYi³Gó½ kppP–eÉëõšÙ“S]/ߌE»2Û4“¬ááa3ëÓív«¼¼<í8çýù|>S¤œÎµg2–ccci³SóõO6‰DBuuu’¤p8<­s�÷.ý·@î¼Ç|ײ‹Ó¹î¦ÙSõg®q˜ésã|³�7Œ… jéÒ¥¿îœd§½{÷* ©¼¼\‘H„À¼š¯‚ìct=æšóe^™ëóáòå˺yófÁ¯û]�¹‹Å´`Á³þíT/ó ©»»[7nTMMMA¯KAs"�«££#mÛ¾}û¦\ÿx”e-È®\¹Ro¾ù&½ƒYs^½^¯™ €ÿyY ²«V­Ò¢E‹tçÎz³dY³b€ÿÃK½0§(Æÿ�‚,€ÿ9K–,QIIIÁ¯» •J¥²í¸|ù2Kx$-\¸PK—.-øu³Î�=räˆöîÝ˨ÀÄ’P d @žÈ·sxxX¥¥¥iÛ,Ë’eY“Žu»Ýr»Ý“ÎφL2É$“L2É$“L2É$“L2É$“L2É$s>3O�:¥õë×ëõ×_Ÿÿ‚lQQ‘$éûï¿WkkkÚ¾3gÎèèÑ£“ÎÙ°aƒª««Ó¶µ··g½(™d’I&™d’I&™d’I&™d’I&™d’Iæ|gŽ��«Ð¤R©T¶W®\ÑíÛ·µjÕª´í7oÞÔ�7&¿xñb•””¤m;wî\Ö‹’I&™d’I&™d’I&™d’I&™d’I&™dÎgfQQ‘–,Yòï)È,^êBA „‚,Y(�ÿµ}?;á@ïIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2563567 ceilometer-27.0.0.0rc1/doc/source/contributor/devstack.rst0000664000175000017500000000202115250267452022276 0ustar00zuulzuul============================== Installing development sandbox ============================== In a development environment created by devstack_, Ceilometer can be tested alongside other OpenStack services. Configuring devstack ==================== 1. Download devstack_. 2. Create a ``local.conf`` file as input to devstack. 3. The ceilometer services are not enabled by default, so they must be enabled in ``local.conf`` but adding the following:: # Enable the Ceilometer devstack plugin enable_plugin ceilometer https://opendev.org/openstack/ceilometer.git By default, all ceilometer services except for ceilometer-ipmi agent will be enabled 4. Enable Gnocchi storage support by including the following in ``local.conf``:: CEILOMETER_BACKEND=gnocchi Optionally, services which extend Ceilometer can be enabled:: enable_plugin aodh https://opendev.org/openstack/aodh These plugins should be added before ceilometer. 5. ``./stack.sh`` .. _devstack: https://docs.openstack.org/devstack/latest/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2573566 ceilometer-27.0.0.0rc1/doc/source/contributor/events.rst0000664000175000017500000002714515250267452022014 0ustar00zuulzuul.. Copyright 2013 Rackspace Hosting. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _events: =========================== Events and Event Processing =========================== Events vs. Samples ~~~~~~~~~~~~~~~~~~ In addition to Meters, and related Sample data, Ceilometer can also process Events. While a Sample represents a single numeric datapoint, driving a Meter that represents the changes in that value over time, an Event represents the state of an object in an OpenStack service (such as an Instance in Nova, or an Image in Glance) at a point in time when something of interest has occurred. This can include non-numeric data, such as an instance's flavor, or network address. In general, Events let you know when something has changed about an object in an OpenStack system, such as the resize of an instance, or creation of an image. While Samples can be relatively cheap (small), disposable (losing an individual sample datapoint won't matter much), and fast, Events are larger, more informative, and should be handled more consistently (you do not want to lose one). Event Structure ~~~~~~~~~~~~~~~ To facilitate downstream processing (billing and/or aggregation), a `minimum required data set and format ` has been defined for services, however events generally contain the following information: event_type A dotted string defining what event occurred, such as ``compute.instance.resize.start`` message_id A UUID for this event. generated A timestamp of when the event occurred on the source system. traits A flat mapping of key-value pairs. The event's Traits contain most of the details of the event. Traits are typed, and can be strings, ints, floats, or datetimes. raw (Optional) Mainly for auditing purpose, the full notification message can be stored (unindexed) for future evaluation. Events from Notifications ~~~~~~~~~~~~~~~~~~~~~~~~~ Events are primarily created via the notifications system in OpenStack. OpenStack systems, such as Nova, Glance, Neutron, etc. will emit notifications in a JSON format to the message queue when some notable action is taken by that system. Ceilometer will consume such notifications from the message queue, and process them. The general philosophy of notifications in OpenStack is to emit any and all data someone might need, and let the consumer filter out what they are not interested in. In order to make processing simpler and more efficient, the notifications are stored and processed within Ceilometer as Events. The notification payload, which can be an arbitrarily complex JSON data structure, is converted to a flat set of key-value pairs known as Traits. This conversion is specified by a config file, so that only the specific fields within the notification that are actually needed for processing the event will have to be stored as Traits. Note that the Event format is meant for efficient processing and querying, there are other means available for archiving notifications (i.e. for audit purposes, etc), possibly to different datastores. Converting Notifications to Events ---------------------------------- In order to make it easier to allow users to extract what they need, the conversion from Notifications to Events is driven by a configuration file (specified by the flag definitions_cfg_file_ in :file:`ceilometer.conf`). This includes descriptions of how to map fields in the notification body to Traits, and optional plugins for doing any programmatic translations (splitting a string, forcing case, etc.) The mapping of notifications to events is defined per event_type, which can be wildcarded. Traits are added to events if the corresponding fields in the notification exist and are non-null. (As a special case, an empty string is considered null for non-text traits. This is due to some openstack projects (mostly Nova) using empty string for null dates.) If the definitions file is not present, a warning will be logged, but an empty set of definitions will be assumed. By default, any notifications that do not have a corresponding event definition in the definitions file will be converted to events with a set of minimal, default traits. This can be changed by setting the flag drop_unmatched_notifications_ in the :file:`ceilometer.conf` file. If this is set to True, then any notifications that don't have events defined for them in the file will be dropped. This can be what you want, the notification system is quite chatty by design (notifications philosophy is "tell us everything, we'll ignore what we don't need"), so you may want to ignore the noisier ones if you don't use them. .. _definitions_cfg_file: http://docs.openstack.org/trunk/config-reference/content/ch_configuring-openstack-telemetry.html .. _drop_unmatched_notifications: http://docs.openstack.org/trunk/config-reference/content/ch_configuring-openstack-telemetry.html There is a set of default traits (all are TEXT type) that will be added to all events if the notification has the relevant data: * service: (All notifications should have this) notification's publisher * tenant_id * request_id * project_id * user_id These do not have to be specified in the event definition, they are automatically added, but their definitions can be overridden for a given ``event_type``. Definitions file format ----------------------- The event definitions file is in YAML format. It consists of a list of event definitions, which are mappings. Order is significant, the list of definitions is scanned in *reverse* order (last definition in the file to the first), to find a definition which matches the notification's event_type. That definition will be used to generate the Event. The reverse ordering is done because it is common to want to have a more general wildcarded definition (such as ``compute.instance.*``) with a set of traits common to all of those events, with a few more specific event definitions (like ``compute.instance.exists``) afterward that have all of the above traits, plus a few more. This lets you put the general definition first, followed by the specific ones, and use YAML mapping include syntax to avoid copying all of the trait definitions. Event Definitions ----------------- Each event definition is a mapping with two keys (both required): event_type This is a list (or a string, which will be taken as a 1 element list) of event_types this definition will handle. These can be wildcarded with unix shell glob syntax. An exclusion listing (starting with a '!') will exclude any types listed from matching. If ONLY exclusions are listed, the definition will match anything not matching the exclusions. traits This is a mapping, the keys are the trait names, and the values are trait definitions. Trait Definitions ----------------- Each trait definition is a mapping with the following keys: type (optional) The data type for this trait. (as a string). Valid options are: *text*, *int*, *float*, and *datetime*. defaults to *text* if not specified. fields A path specification for the field(s) in the notification you wish to extract for this trait. Specifications can be written to match multiple possible fields, the value for the trait will be derived from the matching fields that exist and have a non-null values in the notification. By default the value will be the first such field. (plugins can alter that, if they wish). This is normally a string, but, for convenience, it can be specified as a list of specifications, which will match the fields for all of them. (See `Field Path Specifications`_ for more info on this syntax.) plugin (optional) This is a mapping (For convenience, this value can also be specified as a string, which is interpreted as the name of a plugin to be loaded with no parameters) with the following keys: name (string) name of a plugin to load parameters (optional) Mapping of keyword arguments to pass to the plugin on initialization. (See documentation on each plugin to see what arguments it accepts.) Field Path Specifications ------------------------- The path specifications define which fields in the JSON notification body are extracted to provide the value for a given trait. The paths can be specified with a dot syntax (e.g. ``payload.host``). Square bracket syntax (e.g. ``payload[host]``) is also supported. In either case, if the key for the field you are looking for contains special characters, like '.', it will need to be quoted (with double or single quotes) like so: :: payload.image_meta.'org.openstack__1__architecture' The syntax used for the field specification is a variant of JSONPath, and is fairly flexible. (see: https://github.com/kennknowles/python-jsonpath-rw for more info) Example Definitions file ------------------------ :: --- - event_type: compute.instance.* traits: &instance_traits user_id: fields: payload.user_id instance_id: fields: payload.instance_id host: fields: publisher_id plugin: name: split parameters: segment: 1 max_split: 1 service_name: fields: publisher_id plugin: split instance_type_id: type: int fields: payload.instance_type_id os_architecture: fields: payload.image_meta.'org.openstack__1__architecture' launched_at: type: datetime fields: payload.launched_at deleted_at: type: datetime fields: payload.deleted_at - event_type: - compute.instance.exists - compute.instance.update traits: <<: *instance_traits audit_period_beginning: type: datetime fields: payload.audit_period_beginning audit_period_ending: type: datetime fields: payload.audit_period_ending Trait plugins ------------- Trait plugins can be used to do simple programmatic conversions on the value in a notification field, like splitting a string, lowercasing a value, converting a screwball date into ISO format, or the like. They are initialized with the parameters from the trait definition, if any, which can customize their behavior for a given trait. They are called with a list of all matching fields from the notification, so they can derive a value from multiple fields. The plugin will be called even if there are no fields found matching the field path(s), this lets a plugin set a default value, if needed. A plugin can also reject a value by returning *None*, which will cause the trait not to be added. If the plugin returns anything other than *None*, the trait's value will be set to whatever the plugin returned (coerced to the appropriate type for the trait). Building Notifications ~~~~~~~~~~~~~~~~~~~~~~ In general, the payload format OpenStack services emit could be described as the Wild West. The payloads are often arbitrary data dumps at the time of the event which is often susceptible to change. To make consumption easier, the Ceilometer team offers: CADF_, an open, cloud standard which helps model cloud events. .. _CADF: https://docs.openstack.org/pycadf/latest/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2573566 ceilometer-27.0.0.0rc1/doc/source/contributor/gmr.rst0000664000175000017500000000600215250267452021262 0ustar00zuulzuul.. Copyright (c) 2014 OpenStack Foundation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Guru Meditation Reports ======================= Ceilometer contains a mechanism whereby developers and system administrators can generate a report about the state of a running Ceilometer executable. This report is called a *Guru Meditation Report* (*GMR* for short). Generating a GMR ---------------- A *GMR* can be generated by sending the *USR1* signal to any Ceilometer process with support (see below). The *GMR* will then be outputted standard error for that particular process. For example, suppose that ``ceilometer-polling`` has process id ``8675``, and was run with ``2>/var/log/ceilometer/ceilometer-polling.log``. Then, ``kill -USR1 8675`` will trigger the Guru Meditation report to be printed to ``/var/log/ceilometer/ceilometer-polling.log``. Structure of a GMR ------------------ The *GMR* is designed to be extensible; any particular executable may add its own sections. However, the base *GMR* consists of several sections: Package Shows information about the package to which this process belongs, including version information Threads Shows stack traces and thread ids for each of the threads within this process Green Threads Shows stack traces for each of the green threads within this process (green threads don't have thread ids) Configuration Lists all the configuration options currently accessible via the CONF object for the current process Adding Support for GMRs to New Executables ------------------------------------------ Adding support for a *GMR* to a given executable is fairly easy. First import the module, as well as the Ceilometer version module: .. code-block:: python from oslo_reports import guru_meditation_report as gmr from ceilometer import version Then, register any additional sections (optional): .. code-block:: python TextGuruMeditation.register_section('Some Special Section', some_section_generator) Finally (under main), before running the "main loop" of the executable (usually ``service.server(server)`` or something similar), register the *GMR* hook: .. code-block:: python TextGuruMeditation.setup_autorun(version) Extending the GMR ----------------- As mentioned above, additional sections can be added to the GMR for a particular executable. For more information, see the inline documentation about oslo.reports: `oslo.reports `_ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2583566 ceilometer-27.0.0.0rc1/doc/source/contributor/index.rst0000664000175000017500000000227015250267452021607 0ustar00zuulzuul================= Contributor Guide ================= In the Contributor Guide, you will find documented policies for developing with Ceilometer. This includes the processes we use for bugs, contributor onboarding, core reviewer memberships, and other procedural items. Ceilometer follows the same workflow as other OpenStack projects. To start contributing to Ceilometer, please follow the workflow found here_. .. _here: https://wiki.openstack.org/wiki/Gerrit_Workflow :Bug tracker: https://bugs.launchpad.net/ceilometer :Mailing list: http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss (prefix subjects with ``[Ceilometer]`` for faster responses) :Wiki: https://wiki.openstack.org/wiki/Ceilometer :Code Hosting: https://opendev.org/openstack/ceilometer/ :Code Review: https://review.opendev.org/#/q/status:open+project:openstack/ceilometer,n,z Overview ======== .. toctree:: :maxdepth: 2 overview architecture Data Types ========== .. toctree:: :maxdepth: 2 measurements events Getting Started =============== .. toctree:: :maxdepth: 2 devstack testing gmr Development =========== .. toctree:: :maxdepth: 2 plugins new_resource_types ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2583566 ceilometer-27.0.0.0rc1/doc/source/contributor/measurements.rst0000664000175000017500000000772115250267452023216 0ustar00zuulzuul.. Copyright 2012 New Dream Network (DreamHost) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _measurements: ============ Measurements ============ Existing meters =============== For the list of existing meters see the tables under the `Measurements page`_ of Ceilometer in the Administrator Guide. .. _Measurements page: https://docs.openstack.org/ceilometer/latest/admin/telemetry-measurements.html New measurements ================ Ceilometer is designed to collect measurements from OpenStack services and from other external components. If you would like to add new meters to the currently existing ones, you need to follow the guidelines given in this section. .. _meter_types: Types ----- Three type of meters are defined in Ceilometer: .. index:: double: meter; cumulative double: meter; gauge double: meter; delta ========== =================================================================== Type Definition ========== =================================================================== Cumulative Increasing over time (instance hours) Gauge Discrete items (floating IPs, image uploads) and fluctuating values (disk I/O) Delta Changing over time (bandwidth) ========== =================================================================== When you're about to add a new meter choose one type from the above list, which is applicable. Units ----- 1. Whenever a volume is to be measured, SI approved units and their approved symbols or abbreviations should be used. Information units should be expressed in bits ('b') or bytes ('B'). 2. For a given meter, the units should NEVER, EVER be changed. 3. When the measurement does not represent a volume, the unit description should always describe WHAT is measured (ie: apples, disk, routers, floating IPs, etc.). 4. When creating a new meter, if another meter exists measuring something similar, the same units and precision should be used. 5. Meters and samples should always document their units in Ceilometer (API and Documentation) and new sampling code should not be merged without the appropriate documentation. ============ ======== ============== ======================= Dimension Unit Abbreviations Note ============ ======== ============== ======================= None N/A Dimension-less variable Volume byte B Time seconds s ============ ======== ============== ======================= Naming convention ----------------- If you plan on adding meters, please follow the convention below: 1. Always use '.' as separator and go from least to most discriminant word. For example, do not use ephemeral_disk_size but disk.ephemeral.size 2. When a part of the name is a variable, it should always be at the end and start with a ':'. For example, do not use .image but image:, where type is your variable name. 3. If you have any hesitation, come and ask in #openstack-telemetry Meter definitions ----------------- Meters definitions by default, are stored in separate configuration file, called :file:`ceilometer/data/meters.d/meters.yaml`. This is essentially a replacement for prior approach of writing notification handlers to consume specific topics. A detailed description of how to use meter definition is illustrated in the `admin_guide`_. .. _admin_guide: https://docs.openstack.org/ceilometer/latest/admin/telemetry-data-collection.html#meter-definitions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2583566 ceilometer-27.0.0.0rc1/doc/source/contributor/new_resource_types.rst0000664000175000017500000000574415250267452024435 0ustar00zuulzuul.. Copyright 2017 EasyStack, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _add_new_resource_types: ================================ Ceilometer + Gnocchi Integration ================================ .. warning:: Remember that custom modification may result in conflicts with upstream upgrades. If not intended to be merged with upstream, it's advisable to directly create resource-types via Gnocchi API. .. _resource_types: Managing Resource Types ======================= Resource types in Gnocchi are managed by Ceilometer. The following describes how to add/remove or update Gnocchi resource types to support new Ceilometer data. The modification or creation of Gnocchi resource type definitions are managed `resources_update_operations` of :file:`ceilometer/gnocchi_client.py`. The following operations are supported: 1. Adding a new attribute to a resource type. The following adds `flavor_name` attribute to an existing `instance` resource: .. code:: {"desc": "add flavor_name to instance", "type": "update_attribute_type", "resource_type": "instance", "data": [{ "op": "add", "path": "/attributes/flavor_name", "value": {"type": "string", "min_length": 0, "max_length": 255, "required": True, "options": {'fill': ''}} }]} 2. Remove an existing attribute from a resource type. The following removes `server_group` attribute from `instance` resource: .. code:: {"desc": "remove server_group to instance", "type": "update_attribute_type", "resource_type": "instance", "data": [{ "op": "remove", "path": "/attributes/server_group" }]} 3. Creating a new resource type. The following creates a new resource type named `nova_compute` with a required attribute `host_name`: .. code:: {"desc": "add nova_compute resource type", "type": "create_resource_type", "resource_type": "nova_compute", "data": [{ "attributes": {"host_name": {"type": "string", "min_length": 0, "max_length": 255, "required": True}} }]} .. note:: Do not modify the existing change steps when making changes. Each modification requires a new step to be added and for `ceilometer-upgrade` to be run to apply the change to Gnocchi. With accomplishing sections above, don't forget to add a new resource type or attributes of a resource type into the :file:`ceilometer/publisher/data/gnocchi_resources.yaml`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2583566 ceilometer-27.0.0.0rc1/doc/source/contributor/overview.rst0000664000175000017500000000276315250267452022355 0ustar00zuulzuul======== Overview ======== Objectives ========== The Ceilometer project was started in 2012 with one simple goal in mind: to provide an infrastructure to collect any information needed regarding OpenStack projects. It was designed so that rating engines could use this single source to transform events into billable items which we label as "metering". As the project started to come to life, collecting an `increasing number of meters`_ across multiple projects, the OpenStack community started to realize that a secondary goal could be added to Ceilometer: become a standard way to meter, regardless of the purpose of the collection. This data can then be pushed to any set of targets using provided publishers mentioned in `pipeline-publishers` section. .. _increasing number of meters: https://docs.openstack.org/ceilometer/latest/contributor/measurements.html Metering ======== If you divide a billing process into a 3 step process, as is commonly done in the telco industry, the steps are: 1. :term:`metering` 2. :term:`rating` 3. :term:`billing` Ceilometer's initial goal was, and still is, strictly limited to step one. This is a choice made from the beginning not to go into rating or billing, as the variety of possibilities seemed too large for the project to ever deliver a solution that would fit everyone's needs, from private to public clouds. This means that if you are looking at this project to solve your billing needs, this is the right way to go, but certainly not the end of the road for you. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2593567 ceilometer-27.0.0.0rc1/doc/source/contributor/plugins.rst0000664000175000017500000001207415250267452022164 0ustar00zuulzuul.. Copyright 2012 Nicolas Barcet for Canonical Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ===================== Writing Agent Plugins ===================== This documentation gives you some clues on how to write a new agent or plugin for Ceilometer if you wish to instrument a measurement which has not yet been covered by an existing plugin. Plugin Framework ================ Although we have described a list of the meters Ceilometer should collect, we cannot predict all of the ways deployers will want to measure the resources their customers use. This means that Ceilometer needs to be easy to extend and configure so it can be tuned for each installation. A plugin system based on `setuptools entry points`_ makes it easy to add new monitors in the agents. In particular, Ceilometer now uses Stevedore_, and you should put your entry point definitions in the :file:`entry_points.txt` file of your Ceilometer egg. .. _setuptools entry points: http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins .. _Stevedore: https://docs.openstack.org/stevedore/latest/ Installing a plugin automatically activates it the next time the ceilometer daemon starts. Rather than running and reporting errors or simply consuming cycles for no-ops, plugins may disable themselves at runtime based on configuration settings defined by other components (for example, the plugin for polling libvirt does not run if it sees that the system is configured using some other virtualization tool). Additionally, if no valid resources can be discovered the plugin will be disabled. Polling Agents ============== The polling agent is implemented in :file:`ceilometer/polling/manager.py`. As you will see in the manager, the agent loads all plugins defined in the ``ceilometer.poll.*`` and ``ceilometer.builder.poll.*`` namespaces, then periodically calls their :func:`get_samples` method. Currently we keep separate namespaces - ``ceilometer.poll.compute`` and ``ceilometer.poll.central`` for quick separation of what to poll depending on where is polling agent running. For example, this will load, among others, the :class:`ceilometer.compute.pollsters.instance_stats.CPUPollster` Pollster -------- All pollsters are subclasses of :class:`ceilometer.polling.plugin_base.PollsterBase` class. Pollsters must implement one method: ``get_samples(self, manager, cache, resources)``, which returns a sequence of ``Sample`` objects as defined in the :file:`ceilometer/sample.py` file. Compute plugins are defined as subclasses of the :class:`ceilometer.compute.pollsters.GenericComputePollster` class as defined in the :file:`ceilometer/compute/pollsters/__init__.py` file. For example, in the ``CPUPollster`` plugin, the ``get_samples`` method takes in a given list of resources representing instances on the local host, loops through them and retrieves the `cpu time` details from resource. Similarly, other metrics are built by pulling the appropriate value from the given list of resources. Notifications ============= Notifications in OpenStack are consumed by the notification agent and passed through `pipelines` to be normalised and re-published to specified targets. The existing normalisation pipelines are defined in the namespace ``ceilometer.notification.pipeline``. Each normalisation pipeline are defined as subclass of :class:`ceilometer.pipeline.base.PipelineManager` which interprets and builds pipelines based on a given configuration file. Pipelines are required to define `Source` and `Sink` permutations to describe how to process notification. Additionally, it must set ``get_main_endpoints`` which provides endpoints to be added to the main queue listener in the notification agent. This main queue endpoint inherits :class:`ceilometer.pipeline.base.NotificationEndpoint` and defines which notification priorities to listen, normalises the data, and redirects the data for pipeline processing. Notification endpoints should implement: ``event_types`` A sequence of strings defining the event types the endpoint should handle ``process_notifications(self, priority, notifications)`` Receives an event message from the list provided to ``event_types`` and returns a sequence of objects. Using the SampleEndpoint, it should yield ``Sample`` objects as defined in the :file:`ceilometer/sample.py` file. Two pipeline configurations exist and can be found under ``ceilometer.pipeline.*``. The `sample` pipeline loads in multiple endpoints defined in ``ceilometer.sample.endpoint`` namespace. Each of the endpoints normalises a given notification into different samples. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2593567 ceilometer-27.0.0.0rc1/doc/source/contributor/testing.rst0000664000175000017500000000327115250267452022157 0ustar00zuulzuul.. Copyright 2012 New Dream Network, LLC (DreamHost) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================= Running the Tests ================= Ceilometer includes an extensive set of automated unit tests which are run through tox_. 1. Install ``tox``:: $ sudo pip install tox 2. Run the unit and code-style tests:: $ cd /opt/stack/ceilometer $ tox -e py3,pep8 As tox is a wrapper around stestr, it also accepts the same flags as stestr. See the `stestr documentation`_ for details about these additional flags. .. _stestr documentation: https://stestr.readthedocs.io/en/latest/ Use a double hyphen to pass options to stestr. For example, to run only tests under tests/unit/image:: $ tox -e py3 -- image To debug tests (ie. break into pdb debugger), you can use ''debug'' tox environment. Here's an example, passing the name of a test since you'll normally only want to run the test that hits your breakpoint:: $ tox -e debug ceilometer.tests.unit.test_bin For reference, the ``debug`` tox environment implements the instructions here: https://docs.openstack.org/oslotest/latest/user/debugging.html .. _tox: https://tox.readthedocs.io/en/latest/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2593567 ceilometer-27.0.0.0rc1/doc/source/glossary.rst0000664000175000017500000001064215250267452017773 0ustar00zuulzuul.. Copyright 2012 New Dream Network (DreamHost) Copyright 2013 eNovance Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ======== Glossary ======== .. glossary:: agent Software service running on the OpenStack infrastructure measuring usage and sending the results to any number of target using the :term:`publisher`. billing Billing is the process to assemble bill line items into a single per customer bill, emitting the bill to start the payment collection. bus listener agent Bus listener agent which takes events generated on the Oslo notification bus and transforms them into Ceilometer samples. This is the preferred method of data collection. polling agent Software service running either on a central management node within the OpenStack infrastructure or compute node measuring usage and sending the results to a queue. notification agent The different OpenStack services emit several notifications about the various types of events. The notification agent consumes them from respective queues and filters them by the event_type. data store Storage system for recording data collected by ceilometer. meter The measurements tracked for a resource. For example, an instance has a number of meters, such as duration of instance, CPU time used, number of disk io requests, etc. Three types of meters are defined in ceilometer: * Cumulative: Increasing over time (e.g. disk I/O) * Gauge: Discrete items (e.g. floating IPs, image uploads) and fluctuating values (e.g. number of Swift objects) * Delta: Incremental change to a counter over time (e.g. bandwidth delta) metering Metering is the process of collecting information about what, who, when and how much regarding anything that can be billed. The result of this is a collection of "tickets" (a.k.a. samples) which are ready to be processed in any way you want. notification A message sent via an external OpenStack system (e.g Nova, Glance, etc) using the Oslo notification mechanism [#]_. These notifications are usually sent to and received by Ceilometer through the notifier RPC driver. non-repudiable "Non-repudiation refers to a state of affairs where the purported maker of a statement will not be able to successfully challenge the validity of the statement or contract. The term is often seen in a legal setting wherein the authenticity of a signature is being challenged. In such an instance, the authenticity is being "repudiated"." (Wikipedia, [#]_) project The OpenStack tenant or project. polling agents The polling agent is collecting measurements by polling some API or other tool at a regular interval. publisher The publisher is publishing samples to a specific target. push agents The push agent is the only solution to fetch data within projects, which do not expose the required data in a remotely usable way. This is not the preferred method as it makes deployment a bit more complex having to add a component to each of the nodes that need to be monitored. rating Rating is the process of analysing a series of tickets, according to business rules defined by marketing, in order to transform them into bill line items with a currency value. resource The OpenStack entity being metered (e.g. instance, volume, image, etc). sample Data sample for a particular meter. source The origin of metering data. This field is set to "openstack" by default. It can be configured to a different value using the sample_source field in the ceilometer.conf file. user An OpenStack user. .. [#] https://opendev.org/openstack/oslo.messaging/src/branch/master/oslo_messaging/notify/notifier.py .. [#] http://en.wikipedia.org/wiki/Non-repudiation ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2603567 ceilometer-27.0.0.0rc1/doc/source/index.rst0000664000175000017500000000312115250267452017231 0ustar00zuulzuul.. Copyright 2012 Nicolas Barcet for Canonical Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ====================================== Welcome to Ceilometer's documentation! ====================================== The `Ceilometer` project is a data collection service that provides the ability to normalise and transform data across all current OpenStack core components with work underway to support future OpenStack components. Ceilometer is a component of the Telemetry project. Its data can be used to provide customer billing, resource tracking, and alarming capabilities across all OpenStack core components. This documentation offers information on how Ceilometer works and how to contribute to the project. Overview ======== .. toctree:: :maxdepth: 2 install/index contributor/index admin/index configuration/index cli/index Appendix ======== .. toctree:: :maxdepth: 1 releasenotes/index glossary .. update index .. only:: html Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2881274 ceilometer-27.0.0.0rc1/doc/source/install/0000775000175000017500000000000015250267567017050 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2891276 ceilometer-27.0.0.0rc1/doc/source/install/cinder/0000775000175000017500000000000015250267567020314 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2603567 ceilometer-27.0.0.0rc1/doc/source/install/cinder/install-cinder-config-common.inc0000664000175000017500000000115415250267452026442 0ustar00zuulzuul* Enable periodic usage statistics relating to block storage. To use it, you must run this command in the following format: .. code-block:: console $ cinder-volume-usage-audit --start_time='YYYY-MM-DD HH:MM:SS' \ --end_time='YYYY-MM-DD HH:MM:SS' --send_actions This script outputs what volumes or snapshots were created, deleted, or exists in a given period of time and some information about these volumes or snapshots. Using this script via cron you can get notifications periodically, for example, every 5 minutes:: */5 * * * * /path/to/cinder-volume-usage-audit --send_actions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2603567 ceilometer-27.0.0.0rc1/doc/source/install/cinder/install-cinder-rdo.rst0000664000175000017500000000207615250267452024536 0ustar00zuulzuulEnable Block Storage meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Block Storage service meters. Perform these steps on the controller and Block Storage nodes. .. note:: Your environment must include the Block Storage service. Configure Cinder to use Telemetry --------------------------------- Edit the ``/etc/cinder/cinder.conf`` file and complete the following actions: * In the ``[oslo_messaging_notifications]`` section, configure notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 .. include:: install-cinder-config-common.inc Finalize installation --------------------- #. Restart the Block Storage services on the controller node: .. code-block:: console # systemctl restart openstack-cinder-api.service openstack-cinder-scheduler.service #. Restart the Block Storage services on the storage nodes: .. code-block:: console # systemctl restart openstack-cinder-volume.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2613566 ceilometer-27.0.0.0rc1/doc/source/install/cinder/install-cinder-ubuntu.rst0000664000175000017500000000174515250267452025276 0ustar00zuulzuulEnable Block Storage meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Block Storage service meters. Perform these steps on the controller and Block Storage nodes. .. note:: Your environment must include the Block Storage service. Configure Cinder to use Telemetry --------------------------------- Edit the ``/etc/cinder/cinder.conf`` file and complete the following actions: * In the ``[oslo_messaging_notifications]`` section, configure notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 .. include:: install-cinder-config-common.inc Finalize installation --------------------- #. Restart the Block Storage services on the controller node: .. code-block:: console # systemctl restart apache2 # systemctl restart cinder-scheduler #. Restart the Block Storage services on the storage nodes: .. code-block:: console # systemctl restart cinder-volume ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2613566 ceilometer-27.0.0.0rc1/doc/source/install/get_started.rst0000664000175000017500000000344415250267452022105 0ustar00zuulzuul========================================== Telemetry Data Collection service overview ========================================== The Telemetry Data Collection services provide the following functions: * Efficiently polls metering data related to OpenStack services. * Collects event and metering data by monitoring notifications sent from services. * Publishes collected data to various targets including data stores and message queues. The Telemetry service consists of the following components: A compute agent (``ceilometer-agent-compute``) Runs on each compute node and polls for resource utilization statistics. This is actually the polling agent ``ceilometer-polling`` running with parameter ``--polling-namespace compute``. A central agent (``ceilometer-agent-central``) Runs on a central management server to poll for resource utilization statistics for resources not tied to instances or compute nodes. Multiple agents can be started to scale service horizontally. This is actually the polling agent ``ceilometer-polling`` running with parameter ``--polling-namespace central``. A notification agent (``ceilometer-agent-notification``) Runs on a central management server(s) and consumes messages from the message queue(s) to build event and metering data. Data is then published to defined targets. By default, data is pushed to Gnocchi_. These services communicate by using the OpenStack messaging bus. Ceilometer data is designed to be published to various endpoints for storage and analysis. .. note:: Ceilometer previously provided a storage and API solution. As of Newton, this functionality is officially deprecated and discouraged. For efficient storage and statistical analysis of Ceilometer data, Gnocchi_ is recommended. .. _Gnocchi: https://gnocchi.osci.io ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2901275 ceilometer-27.0.0.0rc1/doc/source/install/glance/0000775000175000017500000000000015250267567020301 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2613566 ceilometer-27.0.0.0rc1/doc/source/install/glance/install-glance-rdo.rst0000664000175000017500000000177715250267452024517 0ustar00zuulzuulEnable Image service meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Image service meters. Perform these steps on the controller node. Configure the Image service to use Telemetry -------------------------------------------- * Edit the ``/etc/glance/glance-api.conf`` file and complete the following actions: * In the ``[DEFAULT]``, ``[oslo_messaging_notifications]`` sections, configure notifications and RabbitMQ message broker access: .. code-block:: ini [DEFAULT] ... transport_url = rabbit://openstack:RABBIT_PASS@controller [oslo_messaging_notifications] ... driver = messagingv2 Replace ``RABBIT_PASS`` with the password you chose for the ``openstack`` account in ``RabbitMQ``. Finalize installation --------------------- * Restart the Image service: .. code-block:: console # systemctl restart openstack-glance-api.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2623568 ceilometer-27.0.0.0rc1/doc/source/install/glance/install-glance-ubuntu.rst0000664000175000017500000000166315250267452025247 0ustar00zuulzuulEnable Image service meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Image service meters. Perform these steps on the controller node. Configure the Image service to use Telemetry -------------------------------------------- * Edit the ``/etc/glance/glance-api.conf`` file and complete the following actions: * In the ``[DEFAULT]``, ``[oslo_messaging_notifications]`` sections, configure notifications and RabbitMQ message broker access: .. code-block:: ini [DEFAULT] ... transport_url = rabbit://openstack:RABBIT_PASS@controller [oslo_messaging_notifications] ... driver = messagingv2 Replace ``RABBIT_PASS`` with the password you chose for the ``openstack`` account in ``RabbitMQ``. Finalize installation --------------------- * Restart the Image service: .. code-block:: console # systemctl restart glance-api ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2901275 ceilometer-27.0.0.0rc1/doc/source/install/heat/0000775000175000017500000000000015250267567017771 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2623568 ceilometer-27.0.0.0rc1/doc/source/install/heat/install-heat-rdo.rst0000664000175000017500000000153515250267452023667 0ustar00zuulzuulEnable Orchestration service meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Orchestration service meters. Perform these steps on the controller node. Configure the Orchestration service to use Telemetry ---------------------------------------------------- * Edit the ``/etc/heat/heat.conf`` and complete the following actions: * In the ``[oslo_messaging_notifications]`` sections, enable notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 Finalize installation --------------------- * Restart the Orchestration service: .. code-block:: console # systemctl restart openstack-heat-api.service \ openstack-heat-api-cfn.service openstack-heat-engine.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2623568 ceilometer-27.0.0.0rc1/doc/source/install/heat/install-heat-ubuntu.rst0000664000175000017500000000142615250267452024424 0ustar00zuulzuulEnable Orchestration service meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Orchestration service meters. Perform these steps on the controller node. Configure the Orchestration service to use Telemetry ---------------------------------------------------- * Edit the ``/etc/heat/heat.conf`` and complete the following actions: * In the ``[oslo_messaging_notifications]`` sections, enable notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 Finalize installation --------------------- * Restart the Orchestration service: .. code-block:: console # systemctl restart heat-api # systemctl restart heat-api-cfn # systemctl restart heat-engine ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2633567 ceilometer-27.0.0.0rc1/doc/source/install/index.rst0000664000175000017500000000053115250267452020701 0ustar00zuulzuul================== Installation Guide ================== .. toctree:: :maxdepth: 2 get_started.rst install-controller.rst install-compute.rst verify.rst next-steps.rst This chapter assumes a working setup of OpenStack following the `OpenStack Installation Tutorials and Guides `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2633567 ceilometer-27.0.0.0rc1/doc/source/install/install-base-config-common.inc0000664000175000017500000000313315250267452024643 0ustar00zuulzuul2. Edit the ``/etc/ceilometer/pipeline.yaml`` file and complete the following section: * Configure Gnocchi connection: .. code-block:: yaml publishers: # set address of Gnocchi # + filter out Gnocchi-related activity meters (Swift driver), # or use enable_filter_project=false to disable filtering # if not required # + set default archive policy - gnocchi://?filter_project=service&archive_policy=low 3. Edit the ``/etc/ceilometer/ceilometer.conf`` file and complete the following actions: * In the ``[DEFAULT]`` section, configure ``RabbitMQ`` message queue access: .. code-block:: ini [DEFAULT] ... transport_url = rabbit://openstack:RABBIT_PASS@controller Replace ``RABBIT_PASS`` with the password you chose for the ``openstack`` account in ``RabbitMQ``. * In the ``[service_credentials]`` section, configure service credentials: .. code-block:: ini [service_credentials] ... auth_type = password auth_url = http://controller:5000/v3 project_domain_id = default user_domain_id = default project_name = service username = ceilometer password = CEILOMETER_PASS interface = internalURL region_name = RegionOne Replace ``CEILOMETER_PASS`` with the password you chose for the ``ceilometer`` user in the Identity service. 4. Create Ceilometer resources in Gnocchi. Gnocchi should be running by this stage: .. code-block:: console # ceilometer-upgrade ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2643566 ceilometer-27.0.0.0rc1/doc/source/install/install-base-prereq-common.inc0000664000175000017500000001375115250267452024703 0ustar00zuulzuul1. Source the ``admin`` credentials to gain access to admin-only CLI commands: .. code-block:: console $ . admin-openrc 2. To create the service credentials, complete these steps: * Create the ``ceilometer`` user: .. code-block:: console $ openstack user create --domain default --password-prompt ceilometer User Password: Repeat User Password: +-----------+----------------------------------+ | Field | Value | +-----------+----------------------------------+ | domain_id | e0353a670a9e496da891347c589539e9 | | enabled | True | | id | c859c96f57bd4989a8ea1a0b1d8ff7cd | | name | ceilometer | +-----------+----------------------------------+ * Add the ``admin`` role to the ``ceilometer`` user. .. code-block:: console $ openstack role add --project service --user ceilometer admin .. note:: This command provides no output. * Create the ``ceilometer`` service entity: .. code-block:: console $ openstack service create --name ceilometer \ --description "Telemetry" metering +-------------+----------------------------------+ | Field | Value | +-------------+----------------------------------+ | description | Telemetry | | enabled | True | | id | 5fb7fd1bb2954fddb378d4031c28c0e4 | | name | ceilometer | | type | metering | +-------------+----------------------------------+ 3. Register Gnocchi service in Keystone: * Create the ``gnocchi`` user: .. code-block:: console $ openstack user create --domain default --password-prompt gnocchi User Password: Repeat User Password: +-----------+----------------------------------+ | Field | Value | +-----------+----------------------------------+ | domain_id | e0353a670a9e496da891347c589539e9 | | enabled | True | | id | 8bacd064f6434ef2b6bbfbedb79b0318 | | name | gnocchi | +-----------+----------------------------------+ * Create the ``gnocchi`` service entity: .. code-block:: console $ openstack service create --name gnocchi \ --description "Metric Service" metric +-------------+----------------------------------+ | Field | Value | +-------------+----------------------------------+ | description | Metric Service | | enabled | True | | id | 205978b411674e5a9990428f81d69384 | | name | gnocchi | | type | metric | +-------------+----------------------------------+ * Add the ``admin`` role to the ``gnocchi`` user. .. code-block:: console $ openstack role add --project service --user gnocchi admin .. note:: This command provides no output. * Create the Metric service API endpoints: .. code-block:: console $ openstack endpoint create --region RegionOne \ metric public http://controller:8041 +--------------+----------------------------------+ | Field | Value | +--------------+----------------------------------+ | enabled | True | | id | b808b67b848d443e9eaaa5e5d796970c | | interface | public | | region | RegionOne | | region_id | RegionOne | | service_id | 205978b411674e5a9990428f81d69384 | | service_name | gnocchi | | service_type | metric | | url | http://controller:8041 | +--------------+----------------------------------+ $ openstack endpoint create --region RegionOne \ metric internal http://controller:8041 +--------------+----------------------------------+ | Field | Value | +--------------+----------------------------------+ | enabled | True | | id | c7009b1c2ee54b71b771fa3d0ae4f948 | | interface | internal | | region | RegionOne | | region_id | RegionOne | | service_id | 205978b411674e5a9990428f81d69384 | | service_name | gnocchi | | service_type | metric | | url | http://controller:8041 | +--------------+----------------------------------+ $ openstack endpoint create --region RegionOne \ metric admin http://controller:8041 +--------------+----------------------------------+ | Field | Value | +--------------+----------------------------------+ | enabled | True | | id | b2c00566d0604551b5fe1540c699db3d | | interface | admin | | region | RegionOne | | region_id | RegionOne | | service_id | 205978b411674e5a9990428f81d69384 | | service_name | gnocchi | | service_type | metric | | url | http://controller:8041 | +--------------+----------------------------------+ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2643566 ceilometer-27.0.0.0rc1/doc/source/install/install-base-rdo.rst0000664000175000017500000000473715250267452022746 0ustar00zuulzuul.. _install_rdo: Install and configure for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This section describes how to install and configure the Telemetry service, code-named ceilometer, on the controller node. Prerequisites ------------- Before you install and configure the Telemetry service, you must configure a target to send metering data to. The recommended endpoint is Gnocchi_. .. _Gnocchi: https://gnocchi.osci.io .. include:: install-base-prereq-common.inc Install Gnocchi --------------- #. Install the Gnocchi packages. Alternatively, Gnocchi can be install using pip: .. code-block:: console # dnf install gnocchi-api gnocchi-metricd python3-gnocchiclient .. note:: Depending on your environment size, consider installing Gnocchi separately as it makes extensive use of the cpu. #. Install the uWSGI packages. The following method uses operating system provided packages. Another alternative would be to use pip(or pip3, depending on the distribution); using pip is not described in this doc: .. code-block:: console # dnf install uwsgi-plugin-common uwsgi-plugin-python3 uwsgi .. note:: Since the provided gnocchi-api wraps around uwsgi, you need to make sure that uWSGI is installed if you want to use gnocchi-api to run Gnocchi API. As Gnocchi API tier runs using WSGI, it can also alternatively be run using Apache httpd and mod_wsgi, or any other HTTP daemon. .. include:: install-gnocchi.inc Finalize Gnocchi installation ----------------------------- #. Start the Gnocchi services and configure them to start when the system boots: .. code-block:: console # systemctl enable gnocchi-api.service gnocchi-metricd.service # systemctl start gnocchi-api.service gnocchi-metricd.service Install and configure components -------------------------------- #. Install the Ceilometer packages: .. code-block:: console # dnf install openstack-ceilometer-notification \ openstack-ceilometer-central .. include:: install-base-config-common.inc Finalize installation --------------------- #. Start the Telemetry services and configure them to start when the system boots: .. code-block:: console # systemctl enable openstack-ceilometer-notification.service \ openstack-ceilometer-central.service # systemctl start openstack-ceilometer-notification.service \ openstack-ceilometer-central.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2653568 ceilometer-27.0.0.0rc1/doc/source/install/install-base-ubuntu.rst0000664000175000017500000000417715250267452023502 0ustar00zuulzuul.. _install_ubuntu: Install and configure for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This section describes how to install and configure the Telemetry service, code-named ceilometer, on the controller node. Prerequisites ------------- Before you install and configure the Telemetry service, you must configure a target to send metering data to. The recommended endpoint is Gnocchi_. .. _Gnocchi: https://gnocchi.osci.io .. include:: install-base-prereq-common.inc Install Gnocchi --------------- #. Install the Gnocchi packages. Alternatively, Gnocchi can be installed using pip: .. code-block:: console # apt-get install gnocchi-api gnocchi-metricd python3-gnocchiclient .. note:: Depending on your environment size, consider installing Gnocchi separately as it makes extensive use of the cpu. #. Install the uWSGI packages. The following method uses operating system provided packages. Another alternative would be to use pip(or pip3, depending on the distribution); using pip is not described in this doc: .. code-block:: console # apt-get install uwsgi-plugin-python3 uwsgi .. note:: Since the provided gnocchi-api wraps around uwsgi, you need to make sure that uWSGI is installed if you want to use gnocchi-api to run Gnocchi API. As Gnocchi API tier runs using WSGI, it can also alternatively be run using Apache httpd and mod_wsgi, or any other HTTP daemon. .. include:: install-gnocchi.inc Finalize Gnocchi installation ----------------------------- #. Restart the Gnocchi services: .. code-block:: console # systemctl restart apache2 # systemctl restart gnocchi-metricd Install and configure components -------------------------------- #. Install the ceilometer packages: .. code-block:: console # apt-get install ceilometer-agent-notification \ ceilometer-agent-central .. include:: install-base-config-common.inc Finalize installation --------------------- #. Restart the Telemetry services: .. code-block:: console # systemctl restart ceilometer-agent-central # systemctl restart ceilometer-agent-notification ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2653568 ceilometer-27.0.0.0rc1/doc/source/install/install-compute-common.inc0000664000175000017500000000341715250267452024147 0ustar00zuulzuul2. Edit the ``/etc/ceilometer/ceilometer.conf`` file and complete the following actions: * In the ``[DEFAULT]`` section, configure ``RabbitMQ`` message queue access: .. code-block:: ini [DEFAULT] ... transport_url = rabbit://openstack:RABBIT_PASS@controller Replace ``RABBIT_PASS`` with the password you chose for the ``openstack`` account in ``RabbitMQ``. * In the ``[service_credentials]`` section, configure service credentials: .. code-block:: ini [service_credentials] ... auth_url = http://controller:5000 project_domain_id = default user_domain_id = default auth_type = password username = ceilometer project_name = service password = CEILOMETER_PASS interface = internalURL region_name = RegionOne Replace ``CEILOMETER_PASS`` with the password you chose for the ``ceilometer`` user in the Identity service. Configure Compute to use Telemetry ---------------------------------- * Edit the ``/etc/nova/nova.conf`` file and configure notifications in the ``[DEFAULT]`` section: .. code-block:: ini [DEFAULT] ... instance_usage_audit = True instance_usage_audit_period = hour [notifications] ... notify_on_state_change = vm_and_task_state [oslo_messaging_notifications] ... driver = messagingv2 Configure Compute to poll IPMI meters ------------------------------------- .. note:: To enable IPMI meters, ensure IPMITool is installed and the host supports IPMI. * Edit the ``/etc/ceilometer/polling.yaml`` to include the required meters, for example: .. code-block:: yaml - name: ipmi interval: 300 meters: - hardware.ipmi.temperature ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2663567 ceilometer-27.0.0.0rc1/doc/source/install/install-compute-rdo.rst0000664000175000017500000000204615250267452023477 0ustar00zuulzuulEnable Compute service meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses a combination of notifications and an agent to collect Compute meters. Perform these steps on each compute node. Install and configure components -------------------------------- #. Install the packages: .. code-block:: console # dnf install openstack-ceilometer-compute # dnf install openstack-ceilometer-ipmi (optional) .. include:: install-compute-common.inc Finalize installation --------------------- #. Start the agent and configure it to start when the system boots: .. code-block:: console # systemctl enable openstack-ceilometer-compute.service # systemctl start openstack-ceilometer-compute.service # systemctl enable openstack-ceilometer-ipmi.service (optional) # systemctl start openstack-ceilometer-ipmi.service (optional) #. Restart the Compute service: .. code-block:: console # systemctl restart openstack-nova-compute.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2663567 ceilometer-27.0.0.0rc1/doc/source/install/install-compute-ubuntu.rst0000664000175000017500000000142415250267452024234 0ustar00zuulzuulEnable Compute service meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses a combination of notifications and an agent to collect Compute meters. Perform these steps on each compute node. Install and configure components -------------------------------- #. Install the packages: .. code-block:: console # apt-get install ceilometer-agent-compute # apt-get install ceilometer-agent-ipmi (optional) .. include:: install-compute-common.inc Finalize installation --------------------- #. Restart the agent: .. code-block:: console # systemctl restart ceilometer-agent-compute # systemctl restart ceilometer-agent-ipmi (optional) #. Restart the Compute service: .. code-block:: console # systemctl restart nova-compute ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2673566 ceilometer-27.0.0.0rc1/doc/source/install/install-compute.rst0000664000175000017500000000063415250267452022716 0ustar00zuulzuul.. _install_compute: Install and Configure Compute Services ====================================== This section assumes that you already have a working OpenStack environment with at least the following components installed: Compute, Image Service, Identity. Note that installation and configuration vary by distribution. .. toctree:: :maxdepth: 1 install-compute-rdo.rst install-compute-ubuntu.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2673566 ceilometer-27.0.0.0rc1/doc/source/install/install-controller.rst0000664000175000017500000000270015250267452023421 0ustar00zuulzuul.. _install_controller: Install and Configure Controller Services ========================================= This section assumes that you already have a working OpenStack environment with at least the following components installed: Compute, Image Service, Identity. Note that installation and configuration vary by distribution. Ceilometer ---------- .. toctree:: :maxdepth: 1 install-base-rdo.rst install-base-ubuntu.rst Additional steps are required to configure services to interact with ceilometer: Cinder ------ .. toctree:: :maxdepth: 1 cinder/install-cinder-rdo.rst cinder/install-cinder-ubuntu.rst Glance ------ .. toctree:: :maxdepth: 1 glance/install-glance-rdo.rst glance/install-glance-ubuntu.rst Heat ---- .. toctree:: :maxdepth: 1 heat/install-heat-rdo.rst heat/install-heat-ubuntu.rst Keystone -------- To enable auditing of API requests, Keystone provides middleware which captures API requests to a service and emits data to Ceilometer. Instructions to enable this functionality is available in `Keystone's developer documentation `_. Ceilometer will captures this information as ``audit.http.*`` events. Neutron ------- .. toctree:: :maxdepth: 1 neutron/install-neutron-rdo.rst neutron/install-neutron-ubuntu.rst Swift ----- .. toctree:: :maxdepth: 1 swift/install-swift-rdo.rst swift/install-swift-ubuntu.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2683566 ceilometer-27.0.0.0rc1/doc/source/install/install-gnocchi.inc0000664000175000017500000000455515250267452022623 0ustar00zuulzuul3. Create the database for Gnocchi's indexer: * Use the database access client to connect to the database server as the ``root`` user: .. code-block:: console $ mysql -u root -p * Create the ``gnocchi`` database: .. code-block:: console CREATE DATABASE gnocchi; * Grant proper access to the ``gnocchi`` database: .. code-block:: console GRANT ALL PRIVILEGES ON gnocchi.* TO 'gnocchi'@'localhost' \ IDENTIFIED BY 'GNOCCHI_DBPASS'; GRANT ALL PRIVILEGES ON gnocchi.* TO 'gnocchi'@'%' \ IDENTIFIED BY 'GNOCCHI_DBPASS'; Replace ``GNOCCHI_DBPASS`` with a suitable password. * Exit the database access client. 4. Edit the ``/etc/gnocchi/gnocchi.conf`` file and add Keystone options: * In the ``[api]`` section, configure gnocchi to use keystone: .. code-block:: ini [api] auth_mode = keystone port = 8041 uwsgi_mode = http-socket * In the ``[keystone_authtoken]`` section, configure keystone authentication: .. code-block:: ini [keystone_authtoken] ... auth_type = password auth_url = http://controller:5000/v3 project_domain_name = Default user_domain_name = Default project_name = service username = gnocchi password = GNOCCHI_PASS interface = internalURL region_name = RegionOne Replace ``GNOCCHI_PASS`` with the password you chose for the ``gnocchi`` user in the Identity service. * In the ``[indexer]`` section, configure database access: .. code-block:: ini [indexer] url = mysql+pymysql://gnocchi:GNOCCHI_DBPASS@controller/gnocchi Replace ``GNOCCHI_DBPASS`` with the password you chose for Gnocchi's indexer database. * In the ``[storage]`` section, configure location to store metric data. In this case, we will store it to the local file system. See Gnocchi documentation for a list of more durable and performant drivers: .. code-block:: ini [storage] # coordination_url is not required but specifying one will improve # performance with better workload division across workers. coordination_url = redis://controller:6379 file_basepath = /var/lib/gnocchi driver = file 5. Initialize Gnocchi: .. code-block:: console gnocchi-upgrade ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2901275 ceilometer-27.0.0.0rc1/doc/source/install/neutron/0000775000175000017500000000000015250267567020542 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2683566 ceilometer-27.0.0.0rc1/doc/source/install/neutron/install-neutron-rdo.rst0000664000175000017500000000140715250267452025207 0ustar00zuulzuulEnable Networking service meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Networking service meters. Perform these steps on the controller node. Configure the Networking service to use Telemetry ------------------------------------------------- * Edit the ``/etc/neutron/neutron.conf`` and complete the following actions: * In the ``[oslo_messaging_notifications]`` sections, enable notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 Finalize installation --------------------- * Restart the Networking service: .. code-block:: console # systemctl restart neutron-server.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2693567 ceilometer-27.0.0.0rc1/doc/source/install/neutron/install-neutron-ubuntu.rst0000664000175000017500000000130515250267452025742 0ustar00zuulzuulEnable Networking service meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses notifications to collect Networking service meters. Perform these steps on the controller node. Configure the Networking service to use Telemetry ------------------------------------------------- * Edit the ``/etc/neutron/neutron.conf`` and complete the following actions: * In the ``[oslo_messaging_notifications]`` sections, enable notifications: .. code-block:: ini [oslo_messaging_notifications] ... driver = messagingv2 Finalize installation --------------------- * Restart the Networking service: .. code-block:: console # systemctl restart neutron-server ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2693567 ceilometer-27.0.0.0rc1/doc/source/install/next-steps.rst0000664000175000017500000000035115250267452021704 0ustar00zuulzuul.. _next-steps: Next steps ~~~~~~~~~~ Your OpenStack environment now includes the ceilometer service. To add additional services, see the `OpenStack Installation Tutorials and Guides `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2911274 ceilometer-27.0.0.0rc1/doc/source/install/swift/0000775000175000017500000000000015250267567020204 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2693567 ceilometer-27.0.0.0rc1/doc/source/install/swift/install-swift-config-common.inc0000664000175000017500000000241615250267452026224 0ustar00zuulzuulConfigure Object Storage to use Telemetry ----------------------------------------- Perform these steps on the controller and any other nodes that run the Object Storage proxy service. * Edit the ``/etc/swift/proxy-server.conf`` file and complete the following actions: * In the ``[filter:keystoneauth]`` section, add the ``ResellerAdmin`` role: .. code-block:: ini [filter:keystoneauth] ... operator_roles = admin, user, ResellerAdmin * In the ``[pipeline:main]`` section, add ``ceilometer``: .. code-block:: ini [pipeline:main] pipeline = catch_errors gatekeeper healthcheck proxy-logging cache container_sync bulk ratelimit authtoken keystoneauth container-quotas account-quotas slo dlo versioned_writes proxy-logging ceilometer proxy-server * In the ``[filter:ceilometer]`` section, configure notifications: .. code-block:: ini [filter:ceilometer] paste.filter_factory = ceilometermiddleware.swift:filter_factory ... control_exchange = swift url = rabbit://openstack:RABBIT_PASS@controller:5672/ driver = messagingv2 topic = notifications log_level = WARN Replace ``RABBIT_PASS`` with the password you chose for the ``openstack`` account in ``RabbitMQ``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2703567 ceilometer-27.0.0.0rc1/doc/source/install/swift/install-swift-prereq-common.inc0000664000175000017500000000200615250267452026250 0ustar00zuulzuulPrerequisites ------------- The Telemetry service requires access to the Object Storage service using the ``ResellerAdmin`` role. Perform these steps on the controller node. #. Source the ``admin`` credentials to gain access to admin-only CLI commands. .. code-block:: console $ . admin-openrc #. Create the ``ResellerAdmin`` role: .. code-block:: console $ openstack role create ResellerAdmin +-----------+----------------------------------+ | Field | Value | +-----------+----------------------------------+ | domain_id | None | | id | 462fa46c13fd4798a95a3bfbe27b5e54 | | name | ResellerAdmin | +-----------+----------------------------------+ #. Add the ``ResellerAdmin`` role to the ``ceilometer`` user: .. code-block:: console $ openstack role add --project service --user ceilometer ResellerAdmin .. note:: This command provides no output. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2703567 ceilometer-27.0.0.0rc1/doc/source/install/swift/install-swift-rdo.rst0000664000175000017500000000130515250267452024310 0ustar00zuulzuulEnable Object Storage meters for Red Hat Enterprise Linux and CentOS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses a combination of polling and notifications to collect Object Storage meters. .. note:: Your environment must include the Object Storage service. .. include:: install-swift-prereq-common.inc Install components ------------------ * Install the packages: .. code-block:: console # dnf install python3-ceilometermiddleware .. include:: install-swift-config-common.inc Finalize installation --------------------- * Restart the Object Storage proxy service: .. code-block:: console # systemctl restart openstack-swift-proxy.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2703567 ceilometer-27.0.0.0rc1/doc/source/install/swift/install-swift-ubuntu.rst0000664000175000017500000000117415250267452025052 0ustar00zuulzuulEnable Object Storage meters for Ubuntu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Telemetry uses a combination of polling and notifications to collect Object Storage meters. .. note:: Your environment must include the Object Storage service. .. include:: install-swift-prereq-common.inc Install components ------------------ * Install the packages: .. code-block:: console # apt-get install python-ceilometermiddleware .. include:: install-swift-config-common.inc Finalize installation --------------------- * Restart the Object Storage proxy service: .. code-block:: console # systemctl restart swift-proxy ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2713566 ceilometer-27.0.0.0rc1/doc/source/install/verify.rst0000664000175000017500000001112315250267452021075 0ustar00zuulzuul.. _verify: Verify operation ~~~~~~~~~~~~~~~~ Verify operation of the Telemetry service. These steps only include the Image service meters to reduce clutter. Environments with ceilometer integration for additional services contain more meters. .. note:: Perform these steps on the controller node. .. note:: The following uses Gnocchi to verify data. Alternatively, data can be published to a file backend temporarily by using a ``file://`` publisher. #. Source the ``admin`` credentials to gain access to admin-only CLI commands: .. code-block:: console $ . admin-openrc #. List available resource and its metrics: .. code-block:: console $ gnocchi resource list --type image +--------------------------------------+-------+----------------------------------+---------+--------------------------------------+----------------------------------+----------+----------------------------------+--------------+ | id | type | project_id | user_id | original_resource_id | started_at | ended_at | revision_start | revision_end | +--------------------------------------+-------+----------------------------------+---------+--------------------------------------+----------------------------------+----------+----------------------------------+--------------+ | a6b387e1-4276-43db-b17a-e10f649d85a3 | image | 6fd9631226e34531b53814a0f39830a9 | None | a6b387e1-4276-43db-b17a-e10f649d85a3 | 2017-01-25T23:50:14.423584+00:00 | None | 2017-01-25T23:50:14.423601+00:00 | None | +--------------------------------------+-------+----------------------------------+---------+--------------------------------------+----------------------------------+----------+----------------------------------+--------------+ $ gnocchi resource show a6b387e1-4276-43db-b17a-e10f649d85a3 +-----------------------+-------------------------------------------------------------------+ | Field | Value | +-----------------------+-------------------------------------------------------------------+ | created_by_project_id | aca4db3db9904ecc9c1c9bb1763da6a8 | | created_by_user_id | 07b0945689a4407dbd1ea72c3c5b8d2f | | creator | 07b0945689a4407dbd1ea72c3c5b8d2f:aca4db3db9904ecc9c1c9bb1763da6a8 | | ended_at | None | | id | a6b387e1-4276-43db-b17a-e10f649d85a3 | | metrics | image.download: 839afa02-1668-4922-a33e-6b6ea7780715 | | | image.serve: 1132e4a0-9e35-4542-a6ad-d6dc5fb4b835 | | | image.size: 8ecf6c17-98fd-446c-8018-b741dc089a76 | | original_resource_id | a6b387e1-4276-43db-b17a-e10f649d85a3 | | project_id | 6fd9631226e34531b53814a0f39830a9 | | revision_end | None | | revision_start | 2017-01-25T23:50:14.423601+00:00 | | started_at | 2017-01-25T23:50:14.423584+00:00 | | type | image | | user_id | None | +-----------------------+-------------------------------------------------------------------+ #. Download the CirrOS image from the Image service: .. code-block:: console $ IMAGE_ID=$(glance image-list | grep 'cirros' | awk '{ print $2 }') $ glance image-download $IMAGE_ID > /tmp/cirros.img #. List available meters again to validate detection of the image download: .. code-block:: console $ gnocchi measures show 839afa02-1668-4922-a33e-6b6ea7780715 +---------------------------+-------------+-----------+ | timestamp | granularity | value | +---------------------------+-------------+-----------+ | 2017-01-26T15:35:00+00:00 | 300.0 | 3740163.0 | +---------------------------+-------------+-----------+ #. Remove the previously downloaded image file ``/tmp/cirros.img``: .. code-block:: console $ rm /tmp/cirros.img ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2921276 ceilometer-27.0.0.0rc1/doc/source/releasenotes/0000775000175000017500000000000015250267567020073 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2713566 ceilometer-27.0.0.0rc1/doc/source/releasenotes/folsom.rst0000664000175000017500000000503615250267452022121 0ustar00zuulzuul.. Copyright 2012 Nicolas Barcet for Canonical Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _folsom: ==================== Folsom ==================== This is the first release (Version 0.1) of Ceilometer. Please take all appropriate caution in using it, as it is a technology preview at this time. Version of OpenStack It is currently tested to work with OpenStack 2012.2 Folsom. Due to its use of openstack-common, and the modification that were made in term of notification to many other components (glance, cinder, quantum), it will not easily work with any prior version of OpenStack. Components Currently covered components are: Nova, Nova-network, Glance, Cinder and Quantum. Notably, there is no support yet for Swift and it was decided not to support nova-volume in favor of Cinder. A detailed list of meters covered per component can be found at in :ref:`measurements`. Nova with libvirt only Most of the Nova meters will only work with libvirt fronted hypervisors at the moment, and our test coverage was mostly done on KVM. Contributors are welcome to implement other virtualization backends' meters. Quantum delete events Quantum delete notifications do not include the same metadata as the other messages, so we ignore them for now. This isn't ideal, since it may mean we miss charging for some amount of time, but it is better than throwing away the existing metadata for a resource when it is deleted. Database backend The only tested and complete database backend is currently MongoDB, the SQLAlchemy one is still work in progress. Installation The current best source of information on how to deploy this project is found as the devstack implementation but feel free to come to #openstack-metering on OFTC for more info. Volume of data Please note that metering can generate lots of data very quickly. Have a look at the following spreadsheet to evaluate what you will end up with. https://wiki.openstack.org/wiki/EfficientMetering#Volume_of_data ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2713566 ceilometer-27.0.0.0rc1/doc/source/releasenotes/index.rst0000664000175000017500000000274115250267452021731 0ustar00zuulzuul.. Copyright 2012 New Dream Network, LLC (DreamHost) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ============================ Release Notes ============================ .. toctree:: :hidden: folsom * :ref:`folsom` * `Havana`_ * `Icehouse`_ * `Juno`_ * `Kilo`_ * `Liberty`_ Since Mitaka development cycle, we start to host release notes on `Ceilometer Release Notes`_ .. _Havana: https://wiki.openstack.org/wiki/ReleaseNotes/Havana#OpenStack_Metering_.28Ceilometer.29 .. _IceHouse: https://wiki.openstack.org/wiki/ReleaseNotes/Icehouse#OpenStack_Telemetry_.28Ceilometer.29 .. _Juno: https://wiki.openstack.org/wiki/ReleaseNotes/Juno#OpenStack_Telemetry_.28Ceilometer.29 .. _Kilo: https://wiki.openstack.org/wiki/ReleaseNotes/Kilo#OpenStack_Telemetry_.28Ceilometer.29 .. _Liberty: https://wiki.openstack.org/wiki/ReleaseNotes/Liberty#OpenStack_Telemetry_.28Ceilometer.29 .. _Ceilometer Release Notes: https://docs.openstack.org/releasenotes/ceilometer/ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2211277 ceilometer-27.0.0.0rc1/etc/0000775000175000017500000000000015250267567014110 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2921276 ceilometer-27.0.0.0rc1/etc/ceilometer/0000775000175000017500000000000015250267567016240 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2723567 ceilometer-27.0.0.0rc1/etc/ceilometer/ceilometer-config-generator.conf0000664000175000017500000000045615250267452024464 0ustar00zuulzuul[DEFAULT] output_file = etc/ceilometer/ceilometer.conf wrap_width = 79 namespace = ceilometer namespace = ceilometer-auth namespace = cotyledon namespace = oslo.cache namespace = oslo.concurrency namespace = oslo.log namespace = oslo.messaging namespace = oslo.reports namespace = oslo.service.service ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2921276 ceilometer-27.0.0.0rc1/etc/ceilometer/examples/0000775000175000017500000000000015250267567020056 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2723567 ceilometer-27.0.0.0rc1/etc/ceilometer/examples/osprofiler_event_definitions.yaml0000664000175000017500000000130215250267452026707 0ustar00zuulzuul--- - event_type: profiler.* traits: project: fields: payload.project service: fields: payload.service name: fields: payload.name base_id: fields: payload.base_id trace_id: fields: payload.trace_id parent_id: fields: payload.parent_id timestamp: fields: payload.timestamp host: fields: payload.info.host path: fields: payload.info.request.path query: fields: payload.info.request.query method: fields: payload.info.request.method scheme: fields: payload.info.request.scheme db.statement: fields: payload.info.db.statement db.params: fields: payload.info.db.params ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2723567 ceilometer-27.0.0.0rc1/etc/ceilometer/polling.yaml0000664000175000017500000000100615250267452020556 0ustar00zuulzuul--- sources: - name: some_pollsters interval: 300 meters: - power.state - cpu - memory.available - memory.usage - network.incoming.bytes - network.incoming.packets - network.outgoing.bytes - network.outgoing.packets - disk.device.read.bytes - disk.device.read.requests - disk.device.write.bytes - disk.device.write.requests - volume.size - volume.snapshot.size - volume.backup.size ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2723567 ceilometer-27.0.0.0rc1/etc/ceilometer/polling_all.yaml0000664000175000017500000000012715250267452021411 0ustar00zuulzuul--- sources: - name: all_pollsters interval: 300 meters: - "*" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2733567 ceilometer-27.0.0.0rc1/etc/ceilometer/rootwrap.conf0000664000175000017500000000172615250267452020763 0ustar00zuulzuul# Configuration for ceilometer-rootwrap # This file should be owned by (and only-writeable by) the root user [DEFAULT] # List of directories to load filter definitions from (separated by ','). # These directories MUST all be only writeable by root ! filters_path=/etc/ceilometer/rootwrap.d,/usr/share/ceilometer/rootwrap # List of directories to search executables in, in case filters do not # explicitly specify a full path (separated by ',') # If not specified, defaults to system PATH environment variable. # These directories MUST all be only writeable by root ! exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin,/usr/local/sbin,/usr/local/bin # Enable logging to syslog # Default value is False use_syslog=False # Which syslog facility to use. # Valid values include auth, authpriv, syslog, user0, user1... # Default value is 'syslog' syslog_log_facility=syslog # Which messages to log. # INFO means log all usage # ERROR means only log unsuccessful attempts syslog_log_level=ERROR ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2931275 ceilometer-27.0.0.0rc1/etc/ceilometer/rootwrap.d/0000775000175000017500000000000015250267567020337 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2733567 ceilometer-27.0.0.0rc1/etc/ceilometer/rootwrap.d/ipmi.filters0000664000175000017500000000052115250267452022656 0ustar00zuulzuul# ceilometer-rootwrap command filters for IPMI capable nodes # This file should be owned by (and only-writeable by) the root user [Filters] privsep-rootwrap-sys_admin: RegExpFilter, privsep-helper, root, privsep-helper, --config-file, /etc/(?!\.\.).*, --privsep_context, ceilometer.privsep.sys_admin_pctxt, --privsep_sock_path, /tmp/.* ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2733567 ceilometer-27.0.0.0rc1/pyproject.toml0000664000175000017500000000212715250267452016244 0ustar00zuulzuul[build-system] requires = ["pbr>=6.0.0", "setuptools>=64.0.0"] build-backend = "pbr.build" [tool.autopep8] aggressive = 3 in-place = true recursive = true ignore = ["E731"] [tool.codespell] skip = "*.po,*.js,*.css,*.html,*.svg,HACKING.py,*hacking*,*build*,*_static*,doc/dictionary.txt,*.pyc,*.inv,*.gz,*.jpg,*.png,*.vsd,*.graffle,*.json,*telemetry-measurements.rst" count = true quiet-level = 4 [tool.ruff] line-length = 79 [tool.ruff.lint] select = [ # mcabe complexity checks "C90", # multiple-imports-on-one-line, module-import-not-at-top-of-file "E4", # line-too-long, redundant-backslash "E5", "E7", "E9", # pyflakes "F", "G", "LOG", "S", "UP", "W", ] external = ["H"] ignore = [ # UP031 % format — defer f-string migration to a later pass "UP031", # UP032 f-string — defer migration to a later pass "UP032", ] [tool.ruff.lint.per-file-ignores] "doc/*" = ["E501"] "ceilometer/tests/*" = ["S"] "tools/*" = ["S"] [tool.ruff.lint.mccabe] # Flag errors (`C901`) whenever the complexity level exceeds 25. max-complexity = 25 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2221277 ceilometer-27.0.0.0rc1/releasenotes/0000775000175000017500000000000015250267567016026 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3431275 ceilometer-27.0.0.0rc1/releasenotes/notes/0000775000175000017500000000000015250267567017156 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2733567 ceilometer-27.0.0.0rc1/releasenotes/notes/.placeholder0000664000175000017500000000000015250267452021420 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2743566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-aodh-metrics-afbe9b780fd137d6.yaml0000664000175000017500000000017415250267452025526 0ustar00zuulzuul--- features: - | Ceilometer is now able to poll the /metrics endpoint in Aodh to get evaluation results metrics. ././@PaxHeader0000000000000000000000000000021400000000000010212 xustar00112 path=ceilometer-27.0.0.0rc1/releasenotes/notes/add-availability_zone-gnocchi-instance-15170e4966a89d63.yaml 28 mtime=1788964650.2743566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-availability_zone-gnocchi-instance-15170e4966a89d63.ya0000664000175000017500000000064615250267452031165 0ustar00zuulzuul--- features: - | Add availability_zone attribute to gnocchi instance resources. Populates this attribute by consuming instance.create.end events. upgrade: - | To take advantage of this new feature you will need to update your gnocchi_resources.yaml file. See the example file for an example. You will need to ensure all required attributes of an instance are specified in the event_attributes.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2743566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-db-legacy-clean-tool-7b3e3714f414c448.yaml0000664000175000017500000000026215250267452026522 0ustar00zuulzuul--- fixes: - > [`bug 1578128 `_] Add a tool that allow users to drop the legacy alarm and alarm_history tables. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2743566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-designate-dns-metrics-8f4a2c7e9b1d3e5a.yaml0000664000175000017500000000074115250267452027333 0ustar00zuulzuul--- features: - | Add Designate DNS zone metrics polling support. This includes a new dns_zone resource type for Gnocchi and the following meters: ``dns.zone.status`` (zone status as numeric value), ``dns.zone.recordsets`` (number of recordsets in a zone), ``dns.zone.ttl`` (zone TTL value), and ``dns.zone.serial`` (zone serial number). A new discovery plugin ``dns_zones`` is also added to discover DNS zones from Designate across all projects. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2743566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-disk-latency-metrics-9e5c05108a78c3d9.yaml0000664000175000017500000000023215250267452026755 0ustar00zuulzuul--- features: - | Add `disk.device.read.latency` and `disk.device.write.latency` meters to capture total time used by read or write operations. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-disk-size-pollsters-6b819d067f9cf736.yaml0000664000175000017500000000043115250267452026666 0ustar00zuulzuul--- features: - | The ``disk.ephemeral.size`` meter is now published as a compute pollster, in addition to the existing notification meter. - | The ``disk.root.size`` meter is now published as a compute pollster, in addition to the existing notification meter. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-full-snmpv3-usm-support-ab540c902fa89b9d.yaml0000664000175000017500000000022415250267452027557 0ustar00zuulzuul--- fixes: - > [`bug 1597618 `_] Add the full support of snmp v3 user security model. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-image-meta-to-samples-72d62016f5ab0043.yaml0000664000175000017500000000203015250267452026702 0ustar00zuulzuul--- features: - | The ``image_meta`` metadata structure for compute meters was formerly only available via the notification meters. When using Nova 2025.2 Flamingo or later, ``image_meta`` is now also supplied by the compute pollsters. This is now possible due to the addition of the relevant metadata to the libvirt guest XML. - | The built-in ``meters.yaml`` has been updated to publish the ``image_meta`` metadata attribute for compute notification meter samples by default. upgrade: - | ``meters.yaml`` has been updated to add ``image_meta`` to compute meter samples by default. - | In order for the new image metadata attributes to start being populated from libvirt metadata in pollster samples, Nova must be upgraded to 2025.2 Flamingo or later (older versions are still backwards compatible, but the new attributes will not be available via pollster samples). Existing instances will need to be shelved-and-unshelved or cold migrated for the metadata to be populated. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-ipmi-sensor-data-gnocchi-70573728499abe86.yaml0000664000175000017500000000036715250267452027373 0ustar00zuulzuul--- upgrade: - | `ceilometer-upgrade` must be run to build IPMI sensor resource in Gnocchi. fixes: - | Ceilometer previously did not create IPMI sensor data from IPMI agent or Ironic in Gnocchi. This data is now pushed to Gnocchi. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-json-output-to-file-publisher-786380cb7e21b56b.yaml0000664000175000017500000000012515250267452030556 0ustar00zuulzuul--- features: - > Add new json output option for the existing file publisher. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-loadbalancer-resource-type-a73c29594b72f012.yaml0000664000175000017500000000036615250267452030055 0ustar00zuulzuul--- fixes: - | [`bug 1848286 `_] Enable load balancer metrics by adding the loadbalancer resource type, allowing Gnocchi to capture measurement data for Octavia load balancers. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2753568 ceilometer-27.0.0.0rc1/releasenotes/notes/add-magnum-event-4c75ed0bb268d19c.yaml0000664000175000017500000000014415250267452025461 0ustar00zuulzuul--- features: - > Added support for magnum bay CRUD events, event_type is 'magnum.bay.*'. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2763567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-manila-share-polling-metrics-ede1c8507d684f.yaml0000664000175000017500000000054415250267452030312 0ustar00zuulzuul--- features: - | Added Manila shared file system metrics polling via openstacksdk. New metrics: * ``manila.share.status`` - Numeric status of the share * ``manila.share.size`` - Size of the share in GiB (polling-based) These metrics complement the existing notification-based ``manila.share.size`` metric with polling support. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2763567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-map-trait-plugin-0d969f5cc7b18175.yaml0000664000175000017500000000057315250267452026131 0ustar00zuulzuul--- features: - | A ``map`` event trait plugin has been added. This allows notification meter attributes to be created by mapping one set of values from an attribute to another set of values defined in the meter definition. Additional options are also available for controlling how to handle edge cases, such as unknown values and case sensitivity. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2763567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-memory-available-metric-d59dc787c485efd3.yaml0000664000175000017500000000046515250267452027617 0ustar00zuulzuul--- features: - | Added a new ``memory.available`` compute pollster metric, for tracking the amount of memory available within the instance, as seen by the instance. This can be combined with ``memory.usage`` in Gnocchi aggregate queries to get memory usage for an instance as a percentage. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2773566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-memory-pollster-7689d354c45cc740.yaml0000664000175000017500000000022315250267452026020 0ustar00zuulzuul--- features: - | The ``memory`` meter now has measures published by a compute pollster, in addition to the existing notification meter. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2773566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-memory-swap-metric-f1633962ab2cf0f6.yaml0000664000175000017500000000015015250267452026531 0ustar00zuulzuul--- features: - Add memory swap metric for VM, including 'memory.swap.in' and 'memory.swap.out'. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2773566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-octavia-loadbalancer-metrics-8f4e7d2c1a3b5e69.yaml0000664000175000017500000000121015250267452030556 0ustar00zuulzuul--- features: - | Ceilometer now supports polling Octavia load balancer metrics using openstacksdk. Two new meters are available: * ``loadbalancer.operating``: Reports the operating status of load balancers (ONLINE=1, DRAINING=2, OFFLINE=3, DEGRADED=4, ERROR=5, NO_MONITOR=6, unknown=-1). * ``loadbalancer.provisioning``: Reports the provisioning status of load balancers (ACTIVE=1, DELETED=2, ERROR=3, PENDING_CREATE=4, PENDING_UPDATE=5, PENDING_DELETE=6, unknown=-1). A new ``loadbalancer`` resource type has been added to gnocchi_resources.yaml to enable publishing these metrics to Gnocchi. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2773566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-parameter-for-disabled-projects-381da4543fff071d.yaml0000664000175000017500000000033615250267452031137 0ustar00zuulzuul--- features: - | The ``[polling] ignore_disabled_projects`` option has been added. This option allows polling agent to only parse enabled projects, to reduce procese time in case many projects are disabled. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2783566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-pool-size-metrics-cdecb979135bba85.yaml0000664000175000017500000000056015250267452026531 0ustar00zuulzuul--- features: - | Added the following meters to the central agent to capture these metrics for each storage pool by API. - `volume.provider.pool.capacity.total` - `volume.provider.pool.capacity.free` - `volume.provider.pool.capacity.provisioned` - `volume.provider.pool.capacity.virtual_free` - `volume.provider.pool.capacity.allocated` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2783566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-power-state-metric-cdfbb3098b50a704.yaml0000664000175000017500000000011715250267452026600 0ustar00zuulzuul--- features: - | Added the new power.state metric from virDomainState. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2783566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-rgw-client-tls-version-1e24dc968255abcb.yaml0000664000175000017500000000032515250267452027412 0ustar00zuulzuul--- features: - | Added ``tls_min_version`` and ``tls_max_version`` options to the ``[rgw_client]`` section to control the TLS protocol versions used for HTTPS connections to the Ceph RGW Admin API. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2783566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-swift-storage_policy-attribute-322fbb5716c5bb10.yaml0000664000175000017500000000216015250267452031134 0ustar00zuulzuul--- features: - | The ``storage_policy`` resource metadata attribute has been added to the ``swift.containers.objects`` and ``swift.containers.objects.size`` meters, populated from already performed Swift account ``GET`` requests. This functionality requires using a new version of Swift that adds the ``storage_policy`` attribute when listing containers in an account. Ceilometer is backwards compatible with Swift versions that do not provide this functionality, but ``storage_policy`` will be set to ``None`` in samples and Gnocchi resources. - | An optional ``storage_policy`` attribute has been added to the ``swift_account`` Gnocchi resource type, to store the storage policy for Swift containers in Gnocchi. For Swift accounts, ``storage_policy`` will be set to ``None``. upgrade: - | To publish the ``storage_policy`` attribute for Swift containers, ``gnocchi_resources.yaml`` will need to be updated to the latest version. Swift in the target OpenStack cloud will also need upgrading to add support for providing the storage policy when listing containers. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-tenant-name-discovery-668260bb4b2b0e8c.yaml0000664000175000017500000000034215250267452027203 0ustar00zuulzuul--- features: - | Identify user and projects names with the help of their UUIDs in the polled samples. If they are identified, set "project_name" and "user_name" fields in the sample to the corresponding values. ././@PaxHeader0000000000000000000000000000021400000000000010212 xustar00112 path=ceilometer-27.0.0.0rc1/releasenotes/notes/add-tool-for-migrating-data-to-gnocchi-cea8d4db68ce03d0.yaml 28 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-tool-for-migrating-data-to-gnocchi-cea8d4db68ce03d0.ya0000664000175000017500000000034115250267452031336 0ustar00zuulzuul--- upgrade: - > Add a tool for migrating metrics data from Ceilometer's native storage to Gnocchi. Since we have deprecated Ceilometer API and the Gnocchi will be the recommended metrics data storage backend. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-upgrade-check-framework-d78858c54cb85f91.yaml0000664000175000017500000000074715250267452027451 0ustar00zuulzuul--- prelude: > Added new tool ``ceilometer-status upgrade check``. features: - | New framework for ``ceilometer-status upgrade check`` command is added. This framework allows adding various checks which can be run before a Ceilometer upgrade to ensure if the upgrade can be performed safely. upgrade: - | Operator can now use new CLI tool ``ceilometer-status upgrade check`` to check if Ceilometer deployment can be safely upgraded from N-1 to N release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-vcpus-pollster-23cfa683d07092b3.yaml0000664000175000017500000000022215250267452025707 0ustar00zuulzuul--- features: - | The ``vcpus`` meter now has measures published by a compute pollster, in addition to the existing notification meter. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-volume-pollster-metadata-d7b435fed9aac0aa.yaml0000664000175000017500000000067115250267452030221 0ustar00zuulzuul--- features: - > Add volume.volume_type_id and backup.is_incremental metadata for cinder pollsters. Also user_id information is now included for backups with the generated samples. upgrade: - > The cinder api microversion has been increased from Pike to Wallaby version (3.64) for volume/snapshot/backup related pollsters. These might not work until the cinder API has been upgraded up to this microversion. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2793567 ceilometer-27.0.0.0rc1/releasenotes/notes/add-volume-service-health-metric-f6ecac5486da168f.yaml0000664000175000017500000000044215250267452030640 0ustar00zuulzuul--- features: - | Added ``volume.service.health`` meter to the central agent to monitor the health of Cinder services (volume, scheduler, backup) by polling the Cinder services API. The metric value is 1 (up) or 0 (down), with binary, host, and zone as resource metadata. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/add-volume_type_id-attr-f29af86534907941.yaml0000664000175000017500000000125315250267452026573 0ustar00zuulzuul--- features: - | Added the ``volume_type_id`` attribute to ``volume.size`` notification samples, which stores the ID for the volume type of the given volume. - | Added the ``volume_type_id`` attribute to ``volume`` resources in Gnocchi, which stores the ID for the volume type of the given volume. upgrade: - | ``meters.yaml`` has been updated with changes to the ``volume.size`` notification meter. If you override this file in your deployment, it needs to be updated. - | ``gnocchi_resources.yaml`` has been updated with changes to the ``volume`` resource type. If you override this file in your deployment, it needs to be updated. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/aggregator-transformer-timeout-e0f42b6c96aa7ada.yaml0000664000175000017500000000025215250267452030627 0ustar00zuulzuul--- fixes: - > [`bug 1531626 `_] Ensure aggregator transformer timeout is honoured if size is not provided. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/allow-disable-gnocchi-project-filter-c7e08c74d57b0224.yaml0000664000175000017500000000110715250267452031244 0ustar00zuulzuul--- features: - | Filtering out metrics from the Gnocchi service project in the Gnocchi publisher can now be disabled using the ``enable_filter_project`` publisher option, e.g. ``gnocchi://?enable_filter_project=false`` (``true`` by default). This ensures that the Gnocchi service project always has metrics published to Gnocchi itself, which is usually the desired behaviour when the Swift storage driver is not being used (filtering should be **enabled** when using the Swift storage driver, to eliminate feedback loops during metric generation). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/always-requeue-7a2df9243987ab67.yaml0000664000175000017500000000112015250267452025151 0ustar00zuulzuul--- critical: - > The previous configuration options default for ``requeue_sample_on_dispatcher_error`` and ``requeue_event_on_dispatcher_error`` allowed to lose data very easily: if the dispatcher failed to send data to the backend (e.g. Gnocchi is down), then the dispatcher raised and the data were lost forever. This was completely unacceptable, and nobody should be able to configure Ceilometer in that way." upgrade: - > The options ``requeue_event_on_dispatcher_error`` and ``requeue_sample_on_dispatcher_error`` have been enabled and removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/batch-messaging-d126cc525879d58e.yaml0000664000175000017500000000100615250267452025236 0ustar00zuulzuul--- features: - > Add support for batch processing of messages from queue. This will allow the collector and notification agent to grab multiple messages per thread to enable more efficient processing. upgrade: - > batch_size and batch_timeout configuration options are added to both [notification] and [collector] sections of configuration. The batch_size controls the number of messages to grab before processing. Similarly, the batch_timeout defines the wait time before processing. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/bug-1929178-a8243526ce2311f7.yaml0000664000175000017500000000020315250267452023515 0ustar00zuulzuul--- deprecations: - | The ``[coordination] check_watchers`` parameter has been deprecated since it has been ineffective. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2803566 ceilometer-27.0.0.0rc1/releasenotes/notes/bug-2007108-dba7163b245ad8fd.yaml0000664000175000017500000000027615250267452024005 0ustar00zuulzuul--- fixes: - | [`bug 2007108 `_] The retired metrics dependent on SNMP have been removed from the default ``polling.yaml``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/bug-2113768-a2db3a59c8e13558.yaml0000664000175000017500000000032715250267452023661 0ustar00zuulzuul--- fixes: - | Fixed `bug #2113768 `__ where the Libvirt inspector did not catch exceptions thrown when calling interfaceStats function on a domain. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/cache-json-parsers-888307f3b6b498a2.yaml0000664000175000017500000000035115250267452025613 0ustar00zuulzuul--- fixes: - > [`bug 1550436 `_] Cache json parsers when building parsing logic to handle event and meter definitions. This will improve agent startup and setup time. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/ceilometer-api-deprecate-862bfaa54e80fa01.yaml0000664000175000017500000000020415250267452027145 0ustar00zuulzuul--- deprecations: - Ceilometer API is deprecated. Use the APIs from Aodh (alarms), Gnocchi (metrics), and/or Panko (events). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/ceilometer-api-removal-6bd44d3eab05e593.yaml0000664000175000017500000000010715250267452026666 0ustar00zuulzuul--- upgrade: - | The deprecated Ceilometer API has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/ceilometer-event-api-removed-49c57835e307b997.yaml0000664000175000017500000000031115250267452027526 0ustar00zuulzuul--- other: - >- The Events API (exposed at /v2/events) which was deprecated has been removed. The Panko project is now responsible for providing this API and can be installed separately. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/cinder-capacity-samples-de94dcfed5540b6c.yaml0000664000175000017500000000025215250267452027175 0ustar00zuulzuul--- features: - | Add support to capture volume capacity usage details from cinder. This data is extracted from notifications sent by Cinder starting in Ocata. ././@PaxHeader0000000000000000000000000000022100000000000010210 xustar00117 path=ceilometer-27.0.0.0rc1/releasenotes/notes/cinder-volume-size-poller-availability_zone-2d20a7527e2341b9.yaml 28 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/cinder-volume-size-poller-availability_zone-2d20a7527e23410000664000175000017500000000017615250267452031427 0ustar00zuulzuul--- features: - | The resource metadata for the Cinder volume size poller now includes the availability zone field. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/compute-discovery-interval-d19f7c9036a8c186.yaml0000664000175000017500000000063515250267452027512 0ustar00zuulzuul--- features: - > To minimise load on Nova API, an additional configuration option was added to control discovery interval vs metric polling interval. If resource_update_interval option is configured in compute section, the compute agent will discover new instances based on defined interval. The agent will continue to poll the discovered instances at the interval defined by pipeline. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/configurable-data-collector-e247aadbffb85243.yaml0000664000175000017500000000054515250267452027745 0ustar00zuulzuul--- features: - > [`bug 1480333 `_] Support ability to configure collector to capture events or meters mutually exclusively, rather than capturing both always. other: - > Configure individual dispatchers by specifying meter_dispatchers and event_dispatchers in configuration file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/cors-support-70c33ba1f6825a7b.yaml0000664000175000017500000000052415250267452024722 0ustar00zuulzuul--- features: - > Support for CORS is added. More information can be found [`here `_] upgrade: - > The api-paste.ini file can be modified to include or exclude the CORs middleware. Additional configurations can be made to middleware as well. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2813566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-aggregated-disk-metrics-54a395c05e74d685.yaml0000664000175000017500000000035515250267452030545 0ustar00zuulzuul--- deprecations: - | disk.* aggregated metrics for instance are deprecated, in favor of the per disk metrics (disk.device.*). Now, it's up to the backend to provide such aggregation feature. Gnocchi already provides this. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-ceilometer-collector-b793b91cd28b9e7f.yaml0000664000175000017500000000070215250267452030414 0ustar00zuulzuul--- features: - | Because of deprecating the collector, the default publishers in pipeline.yaml and event_pipeline.yaml are now changed using database instead of notifier. deprecations: - | Collector is no longer supported in this release. The collector introduces lags in pushing data to backend. To optimize the architecture, Ceilometer push data through dispatchers using publishers in notification agent directly. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-contrail-256177299deb6926.yaml0000664000175000017500000000024515250267452025620 0ustar00zuulzuul--- deprecations: - | Support for OpenContrail, which is currently known as Tungsten Fabric, has been deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-events-6561f4059fa25c02.yaml0000664000175000017500000000020715250267452025342 0ustar00zuulzuul--- deprecations: - | The Ceilometer event subsystem and pipeline is now deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-file-dispatcher-2aff376db7609136.yaml0000664000175000017500000000037315250267452027175 0ustar00zuulzuul--- deprecations: - With collector service being deprecated, we now have to address the duplication between dispatchers and publishers. The file dispatcher is now marked as deprecated. Use the file publisher to push samples into a file. ././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00121 path=ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-generic-hardware-declarative-pollstar-dfa418bf6a5e0459.yaml 28 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-generic-hardware-declarative-pollstar-dfa418bf6a0000664000175000017500000000110415250267452032072 0ustar00zuulzuul--- deprecations: - | ``GenericHardwareDeclarativePollster`` has been deprecated and will be removed in a future release. This pollster was designed to be used in TripleO deployment to gather hardware metrics from overcloud nodes but Telemetry services are no longer deployed in undercloud in current TripleO. - | The ``NodesDiscoveryTripleO`` discovery plugin has been deprecated and will be removed in a future release. This plugin is designed for TripleO deployment but no longer used since Telemetry services were removed from undercloud. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-http-control-exchanges-026a8de6819841f8.yaml0000664000175000017500000000056515250267452030465 0ustar00zuulzuul--- deprecations: - | Allow users to add additional exchanges in ceilometer.conf instead of hardcoding exchanges. Now original http_control_exchanges is being deprecated and renamed notification_control_exchanges. Besides, the new option is integrated with other exchanges in default EXCHANGE_OPTS to make it available to extend additional exchanges. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2823567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-http-dispatcher-dbbaacee8182b550.yaml0000664000175000017500000000113515250267452027513 0ustar00zuulzuul--- upgrade: - Configuration values can passed in via the querystring of publisher in pipeline. For example, rather than setting target, timeout, verify_ssl, and batch_mode under [dispatcher_http] section of conf, you can specify http:///?verify_ssl=True&batch=True&timeout=10. Use `raw_only=1` if only the raw details of event are required. deprecations: - As the collector service is being deprecated, the duplication of publishers and dispatchers is being addressed. The http dispatcher is now marked as deprecated and the recommended path is to use http publisher. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2833567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-http_timeout-ce98003e4949f9d9.yaml0000664000175000017500000000016015250267452026673 0ustar00zuulzuul--- deprecations: - | The ``[DEFAULT] http_timeout`` option has been deprecated because it is unused. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2833567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-kafka-publisher-17b4f221758e15da.yaml0000664000175000017500000000072615250267452027174 0ustar00zuulzuul--- features: - | Ceilometer supports generic notifier to publish data and allow user to customize parameters such as topic, transport driver and priority. The publisher configuration in pipeline.yaml can be notifier://[notifier_ip]:[notifier_port]?topic=[topic]&driver=driver&max_retry=100 Not only rabbit driver, but also other driver like kafka can be used. deprecations: - | Kafka publisher is deprecated to use generic notifier instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2833567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-neutron-fwaas-e985afe956240c08.yaml0000664000175000017500000000024415250267452026727 0ustar00zuulzuul--- deprecations: - | Support for Neutron FWaaS has been officially deprecated. The feature has been useless since the Neutron FWaaS project was retired. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2833567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-neutron-lbaas-5a36406cbe44bbe3.yaml0000664000175000017500000000024415250267452027023 0ustar00zuulzuul--- deprecations: - | Support for Neutron LBaaS has been officially deprecated. The feature has been useless since the Neutron LBaaS project was retired. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2833567 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-odl-07e3f59165612566.yaml0000664000175000017500000000016615250267452024474 0ustar00zuulzuul--- deprecations: - | Support for OpenDaylight has been deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-pollster-builder-d481966e497959a3.yaml0000664000175000017500000000023315250267452027302 0ustar00zuulzuul--- deprecations: - | Support for pollster builder has been deprecated, because ceilometer no longer provides any built-in pollster builder now. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-pollster-list-ccf22b0dea44f043.yaml0000664000175000017500000000024715250267452027143 0ustar00zuulzuul--- deprecations: - | Deprecating support for enabling pollsters via command line. Meter and pollster enablement should be configured via polling.yaml file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-vmware-ae49e07e40e74577.yaml0000664000175000017500000000030315250267452025432 0ustar00zuulzuul--- deprecations: - | Support for VMWare vSphere has been deprecated, because the vmwareapi virt driver in nova has been marked experimental and may be removed in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-windows-support-d784b975ce878864.yaml0000664000175000017500000000033515250267452027306 0ustar00zuulzuul--- deprecations: - | Support for running Ceilometer in Windows operating systems has been deprecated because of retirement of the Winstackers project. Because of this, Hyper-V inspector is also deprecated. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecate-xen-support-27600e2bf7be548c.yaml0000664000175000017500000000020615250267452026504 0ustar00zuulzuul--- deprecations: - | Support for XenServer/Xen Cloud Platform has been deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000022000000000000010207 xustar00116 path=ceilometer-27.0.0.0rc1/releasenotes/notes/deprecated_database_event_dispatcher_panko-607d558c86a90f17.yaml 28 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/deprecated_database_event_dispatcher_panko-607d558c86a90f10000664000175000017500000000025215250267452031616 0ustar00zuulzuul--- deprecations: - The event database dispatcher is now deprecated. It has been moved to a new project, alongside the Ceilometer API for /v2/events, called Panko. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/disabled-projects-api-filter-4dee304fcd523356.yaml0000664000175000017500000000122215250267452027673 0ustar00zuulzuul--- features: - | When ``[polling]/ignore_disabled_projects`` is enabled, project filtering is now performed server-side using API filters. This improves the performance of project discovery (used by the Swift object storage pollsters), particularly when Keystone has a lot of disabled projects (e.g. retained for record-keeping purposes, since deleting projects in Keystone removes them completely). upgrade: - | When ``[polling]/ignore_disabled_projects`` is enabled, projects that are part of disabled domains are also ignored, regardless of whether or not the projects inside those domains are marked as disabled. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2843566 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-collector-4c207b35d67b2977.yaml0000664000175000017500000000055315250267452025051 0ustar00zuulzuul--- upgrade: - | The collector service is removed. From Ocata, it's possible to edit the pipeline.yaml and event_pipeline.yaml files and modify the publisher to provide the same functionality as collector dispatcher. You may change publisher to 'gnocchi', 'http', 'panko', or any combination of available publishers listed in documentation. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-image-meter-9c9b6cebd546dae7.yaml0000664000175000017500000000074415250267452025646 0ustar00zuulzuul--- prelude: > In an effort to minimise the noise, Ceilometer will no longer produce meters which have no measurable data associated with it. Image meter only captures state information which is already captured in events and other meters. upgrade: - Any existing commands utilising `image` meter should be switched to `image.size` meter which will provide equivalent functionality deprecations: - The `image` meter is dropped in favour of `image.size` meter. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-instance-meter-1b657717b21a0f55.yaml0000664000175000017500000000066715250267452025773 0ustar00zuulzuul--- prelude: > Samples are required to measure some aspect of a resource. Samples not measuring anything will be dropped. upgrade: - The `instance` meter no longer will be generated. For equivalent functionality, perform the exact same query on any compute meter such as `cpu`, `disk.read.requests`, `memory.usage`, `network.incoming.bytes`, etc... deprecations: - The `instance` meter no longer will be generated. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-kwapi-b687bc476186d01b.yaml0000664000175000017500000000012015250267452024241 0ustar00zuulzuul--- deprecations: - | Previously deprecated kwapi meters are not removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-py-2-7-87352d5763131c13.yaml0000664000175000017500000000031515250267452023642 0ustar00zuulzuul--- upgrade: - | Python 2.7 support has been dropped. Last release of ceilometer to support py2.7 is OpenStack Train. The minimum version of Python now supported by ceilometer is Python 3.6. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/drop-python-3-6-and-3-7-f67097fa6894da52.yaml0000664000175000017500000000020115250267452026034 0ustar00zuulzuul--- upgrade: - | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now supported is Python 3.8. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/dynamic-pollster-system-6b45c8c973201b2b.yaml0000664000175000017500000000025415250267452026774 0ustar00zuulzuul--- features: - | Add dynamic pollster system. The dynamic pollster system enables operators to gather new metrics on the fly (without needing to code pollsters).././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00120 path=ceilometer-27.0.0.0rc1/releasenotes/notes/dynamic-pollster-system-for-non-openstack-apis-4e06694f223f34f3.yaml 28 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/dynamic-pollster-system-for-non-openstack-apis-4e06694f2230000664000175000017500000000033615250267452031513 0ustar00zuulzuul--- features: - | Add the support for non-OpenStack APIs in the dynamic pollster system. This extension enables operators to create pollster on the fly to handle metrics from systems such as the RadosGW API. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2853568 ceilometer-27.0.0.0rc1/releasenotes/notes/dynamic-pollster-url-joins-6cdb01c4015976f7.yaml0000664000175000017500000000135515250267452027402 0ustar00zuulzuul--- upgrade: - | When using dynamic pollsters to query OpenStack APIs, if the endpoint URL returned by Keystone does not have a trailing slash and ``url_path`` is a relative path, the ``url_path`` configured in the dynamic pollster would replace sections of the endpoint URL instead of being appended to the end of the URL. This behaviour has now been changed so that ``url_path`` values that do not start with a ``/`` are always appended to the end of endpoint URLs. This change may require existing dynamic pollsters that rely on this behaviour to be changed, but this allows dynamic pollsters to be added for OpenStack services that append the active project ID to the API endpoint URL (e.g. Trove). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/enable-promethus-exporter-tls-76e78d4f4a52c6c4.yaml0000664000175000017500000000014615250267452030204 0ustar00zuulzuul--- features: - | Enhanced the Prometheus exporter to support TLS for exposing metrics securely.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/event-type-race-c295baf7f1661eab.yaml0000664000175000017500000000024515250267452025420 0ustar00zuulzuul--- fixes: - > [`bug 1254800 `_] Add better support to catch race conditions when creating event_types ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fetch_metadata-default-false-b03d0a1db61439f7.yaml0000664000175000017500000000223515250267452027702 0ustar00zuulzuul--- upgrade: - | The ``[compute]/fetch_extra_metadata`` configuration option, introduced in the 2025.2 Flamingo release, allows configuring whether or not instance user metadata is fetched by compute pollsters via Nova API queries. This is used, for example, to fetch the ``metering.server_group`` metadata attribute, which sets the ``server_group`` attribute on Gnocchi resources. ``[compute]/fetch_extra_metadata`` was previously set to ``True`` by default. To reduce load on Nova API, as of the 2026.2 Hibiscus release this option is now set to ``False`` by default. If you are using Gnocchi and you require user metadata for some attributes (e.g. ``server_group``), enabling this option should not be necessary in most cases as the notification meters will already set these on resources, even without compute pollsters fetching them. If you use Prometheus and want this metadata included with all samples (e.g. for querying metrics by server group, Aodh alarms or Heat auto-scaling), you should explicitly enable this option by setting ``[compute]/fetch_extra_metadata`` to ``True`` in ``ceilometer.conf``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-1940660-5226988f2e7ae1bd.yaml0000664000175000017500000000041415250267452023675 0ustar00zuulzuul--- fixes: - > [`bug 1940660 `_] Fixes an issue with the Swift pollster where the ``[service_credentials] cafile`` option was not used. This could prevent communication with TLS-enabled Swift APIs. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-agent-coordination-a7103a78fecaec24.yaml0000664000175000017500000000066415250267452026757 0ustar00zuulzuul--- critical: - > [`bug 1533787 `_] Fix an issue where agents are not properly getting registered to group when multiple notification agents are deployed. This can result in bad transformation as the agents are not coordinated. It is still recommended to set heartbeat_timeout_threshold = 0 in [oslo_messaging_rabbit] section when deploying multiple agents. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-aggregation-transformer-9472aea189fa8f65.yaml0000664000175000017500000000037415250267452027706 0ustar00zuulzuul--- fixes: - > [`bug 1539163 `_] Add ability to define whether to use first or last timestamps when aggregating samples. This will allow more flexibility when chaining transformers. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-floatingip-pollster-f5172060c626b19e.yaml0000664000175000017500000000061215250267452026666 0ustar00zuulzuul--- fixes: - > [`bug 1536338 `_] Patch was added to fix the broken floatingip pollster that polled data from nova api, but since the nova api filtered the data by tenant, ceilometer was not getting any data back. The fix changes the pollster to use the neutron api instead to get the floating ip info. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-network-lb-bytes-sample-5dec2c6f3a8ae174.yaml0000664000175000017500000000031415250267452027661 0ustar00zuulzuul--- fixes: - > [`bug 1530793 `_] network.services.lb.incoming.bytes meter was previous set to incorrect type. It should be a gauge meter. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-notification-batch-9bb42cbdf817e7f9.yaml0000664000175000017500000000043015250267452026753 0ustar00zuulzuul--- fixes: - | The ``[notification] batch_size`` parameter now takes effect to enable batch processing of notifications. The ``[notification] batch_timeout`` parameter has been restored at the same time to determine how much and how long notifications are kept. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2863567 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-radosgw-name-6de6899ddcd7e06d.yaml0000664000175000017500000000103415250267452025602 0ustar00zuulzuul--- upgrade: - | Use `radosgw.*` to enable/disable radosgw meters explicitly rather than `rgw.*` deprecations: - | Previously, to enable/disable radosgw.* meters, you must define entry_point name rather than meter name. This is corrected so you do not need to be aware of entry_point naming. Use `radosgw.*` to enable/disable radosgw meters explicitly rather than `rgw.*`. `rgw.*` support is deprecated and will be removed in Rocky. fixes: - | Fix ability to enable/disable radosgw.* meters explicitly ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2873566 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-size-metric-units-e6028b4b4fc3e6aa.yaml0000664000175000017500000000364715250267452026573 0ustar00zuulzuul--- upgrade: - | The reported units for the following metrics were changed from ``MB`` and ``GB`` to ``MiB`` and ``GiB`` respectively, as the metrics are actually in **mebibytes**/**gibibytes**: * ``memory``/``memory.*`` * ``disk.root.size`` * ``disk.ephemeral.size`` * ``volume.size`` * ``volume.snapshot.size``/``snapshot.size`` * ``volume.backup.size``/``backup.size`` * ``volume.provider.capacity.*``/``volume.provider.pool.capacity.*`` * ``manila.share.size`` Following the upgrade, the storage backends Ceilometer publishes to will go through an intermediary period where metrics using both the old and new units will exist at the same time: * In Gnocchi, newly created metrics will set ``unit`` to the newer values. Existing metrics on existing resources, however, will not have their unit updated automatically. They will need to be changed manually, if required. * In Prometheus, the ``unit`` label will change for the above metrics, causing Prometheus to treat them as separate metrics (though with otherwise identical labels) for non-aggregated queries. These separate metrics will co-exist until the old metrics expire, but the overlap between the old and new metrics should be small unless your query window is wide. If you perform any PromQL queries overlapping the changeover period that **must** have a single metric per resource, you could use aggregations like ``max without (unit) (...)`` to take into account this change. Regarding the values of the metrics themselves, please note that the **actual values have not changed**, only the reported unit names. There is no action needed unless you are converting the metrics to other units (or referencing the reported units in some way), in which case we would recommend double checking that the values are being handled correctly. ././@PaxHeader0000000000000000000000000000021700000000000010215 xustar00115 path=ceilometer-27.0.0.0rc1/releasenotes/notes/fix-volume-provider-pool-capacity-metrics-7b8b0de29a513cea.yaml 28 mtime=1788964650.2873566 ceilometer-27.0.0.0rc1/releasenotes/notes/fix-volume-provider-pool-capacity-metrics-7b8b0de29a513cea0000664000175000017500000000023315250267452031570 0ustar00zuulzuul--- fixes: - | [`bug 2113903 `_] Fix volume provider pool capacity metrics for ceph backend. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2873566 ceilometer-27.0.0.0rc1/releasenotes/notes/get-more-flavor-info-from-libvirt-c8db26fe410abe6e.yaml0000664000175000017500000000351515250267452031041 0ustar00zuulzuul--- features: - | When using Nova 2025.2 and later, the flavor ID for an instance is now available from the libvirt domain metadata. Ceilometer now takes advantage of this and populates the flavor ID from metadata instead of querying Nova, when the value is available. If not available from metadata, Ceilometer will fallback to querying Nova API for the flavor ID. - | When using Nova 2025.2 and later, the extra specs for the flavor and instance is running is now available from the libvirt domain metadata. Ceilometer now adds the flavor's extra specs to compute sample metadata when found. - | Added the ``[compute]/fetch_extra_metadata`` configuration option, which allows configuration of whether or not Ceilometer fetches additional compute instance metadata attributes that require Nova API queries. This mainly affects the ``user_metadata`` attributes populated with metering-related values such as the server group an instance is part of. When ``fetch_extra_metadata`` is set to ``False``, Ceilometer Compute Agent will not query Nova API for anything unless absolutely necessary. upgrade: - | After Nova has been upgraded to 2025.2 or later, new instances will start providing additional flavor metadata for Ceilometer to use. Instances already running at the time of the upgrade are not updated as part of the process; to update those instances they will need to be cold restarted, cold migrated or shelved-and-unshelved (until this happens, Nova API queries will continue to be performed for those instances). deprecations: - | The newly added ``[compute]/fetch_extra_metadata`` option is set to ``True`` by default, but to reduce the amount of load Ceilometer places on Nova this will be changed to ``False`` by default in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2873566 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-cache-1d8025dfc954f281.yaml0000664000175000017500000000054215250267452024650 0ustar00zuulzuul--- features: - > Support resource caching in Gnocchi dispatcher to improve write performance to avoid additional queries. other: - > A dogpile.cache supported backend is required to enable cache. Additional configuration `options `_ are also required. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-cache-b9ad4d85a1da8d3f.yaml0000664000175000017500000000031215250267452025140 0ustar00zuulzuul--- fixes: - > [`bug 255569 `_] Fix caching support in Gnocchi dispatcher. Added better locking support to enable smoother cache access. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-client-42cd992075ee53ab.yaml0000664000175000017500000000026715250267452025145 0ustar00zuulzuul--- features: - > Gnocchi dispatcher now uses client rather than direct http requests upgrade: - > gnocchiclient library is now a requirement if using ceilometer+gnocchi. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-host-metrics-829bcb965d8f2533.yaml0000664000175000017500000000032215250267452026233 0ustar00zuulzuul--- features: - > [`bug 1518338 `_] Add support for storing SNMP metrics in Gnocchi.This functionality requires Gnocchi v2.1.0 to be installed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-no-metric-by-default-b643e09f5ffef2c4.yaml0000664000175000017500000000031415250267452027755 0ustar00zuulzuul--- issues: - | Ceilometer created metrics that could never get measures depending on the polling configuration. Metrics are now created only if Ceilometer gets at least a measure for them. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-orchestration-3497c689268df0d1.yaml0000664000175000017500000000023615250267452026425 0ustar00zuulzuul--- upgrade: - > gnocchi_resources.yaml in Ceilometer should be updated. fixes: - > Fix samples from Heat to map to correct Gnocchi resource type ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-publisher-hash-blake2-514c0ca1882b4423.yaml0000664000175000017500000000115615250267452027562 0ustar00zuulzuul--- features: - | The hashing algorithm for the resource attributes cache in the Gnocchi publisher has been changed to BLAKE2 to provide a deterministic and low collision risk hash that is stable between processes, hosts and restarts. upgrade: - | The default hashing for the resource attributes cache in the Gnocchi publisher has been changed from the builtin Python hash function to using the BLAKE2 algorithm, this will cause the cache for attributes for Gnocchi resources to be rebuild that can cause a temporary higher load on the Gnocchi API due to more resource update requests. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-udp-collector-00415e6674b5cc0f.yaml0000664000175000017500000000021715250267452026345 0ustar00zuulzuul--- fixes: - > [`bug 1523124 `_] Fix gnocchi dispatcher to support UDP collector ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/gnocchi-update-project-id-c8355fc5901e7aa3.yaml0000664000175000017500000000115415250267452027202 0ustar00zuulzuul--- upgrade: - | If resource metadata caching is enabled in Ceilometer Notification Agent, the Gnocchi resource metadata cache will be invalidated and refreshed upon upgrading due to the fix for updating resource project IDs. fixes: - | An issue has been fixed where when publishing resource metadata changes to Gnocchi, the project ID field was not updated when changed (e.g. when transferring volumes from one project to another). Any existing resources that have changed ownership since being added to Gnocchi will have their ``project_id`` attribute updated to the correct value. ././@PaxHeader0000000000000000000000000000021300000000000010211 xustar00111 path=ceilometer-27.0.0.0rc1/releasenotes/notes/handle-malformed-resource-definitions-ad4f69f898ced34d.yaml 28 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/handle-malformed-resource-definitions-ad4f69f898ced34d.yam0000664000175000017500000000054515250267452031621 0ustar00zuulzuul--- fixes: - > [`bug 1542189 `_] Handle malformed resource definitions in gnocchi_resources.yaml gracefully. Currently we raise an exception once we hit a bad resource and skip the rest. Instead the patch skips the bad resource and proceeds with rest of the definitions. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2883568 ceilometer-27.0.0.0rc1/releasenotes/notes/http-dispatcher-batching-4e17fce46a196b07.yaml0000664000175000017500000000036415250267452027136 0ustar00zuulzuul--- features: - | In the [dispatcher_http] section of ceilometer.conf, batch_mode can be set to True to activate sending meters and events in batches, or False (default value) to send each meter and event with a fresh HTTP call. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2893567 ceilometer-27.0.0.0rc1/releasenotes/notes/http-dispatcher-verify-ssl-551d639f37849c6f.yaml0000664000175000017500000000112115250267452027334 0ustar00zuulzuul--- features: - In the [dispatcher_http] section of ceilometer.conf, verify_ssl can be set to True to use system-installed certificates (default value) or False to ignore certificate verification (use in development only!). verify_ssl can also be set to the location of a certificate file e.g. /some/path/cert.crt (use for self-signed certs) or to a directory of certificates. The value is passed as the 'verify' option to the underlying requests method, which is documented at http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2893567 ceilometer-27.0.0.0rc1/releasenotes/notes/http-publisher-authentication-6371c5a9aa8d4c03.yaml0000664000175000017500000000140715250267452030234 0ustar00zuulzuul--- features: - In the 'publishers' section of a meter/event pipeline definition, https:// can now be used in addition to http://. Furthermore, either Basic or client-certificate authentication can be used (obviously, client cert only makes sense in the https case). For Basic authentication, use the form http://username:password@hostname/. For client certificate authentication pass the client certificate's path (and the key file path, if the key is not in the certificate file) using the parameters 'clientcert' and 'clientkey', e.g. https://hostname/path?clientcert=/path/to/cert&clientkey=/path/to/key. Any parameters or credentials used for http(s) publishers are removed from the URL before the actual HTTP request is made. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2893567 ceilometer-27.0.0.0rc1/releasenotes/notes/http_proxy_to_wsgi_enabled-616fa123809e1600.yaml0000664000175000017500000000132715250267452027447 0ustar00zuulzuul--- features: - Ceilometer sets up the HTTPProxyToWSGI middleware in front of Ceilometer. The purpose of this middleware is to set up the request URL correctly in case there is a proxy (for instance, a loadbalancer such as HAProxy) in front of Ceilometer. So, for instance, when TLS connections are being terminated in the proxy, and one tries to get the versions from the / resource of Ceilometer, one will notice that the protocol is incorrect; It will show 'http' instead of 'https'. So this middleware handles such cases. Thus helping Keystone discovery work correctly. The HTTPProxyToWSGI is off by default and needs to be enabled via a configuration value. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2893567 ceilometer-27.0.0.0rc1/releasenotes/notes/improve-events-rbac-support-f216bd7f34b02032.yaml0000664000175000017500000000060015250267452027550 0ustar00zuulzuul--- upgrade: - > To utilize the new policy support. The policy.json file should be updated accordingly. The pre-existing policy.json file will continue to function as it does if policy changes are not required. fixes: - > [`bug 1504495 `_] Configure ceilometer to handle policy.json rules when possible. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2893567 ceilometer-27.0.0.0rc1/releasenotes/notes/include-monasca-publisher-1f47dde52af50feb.yaml0000664000175000017500000000047615250267452027533 0ustar00zuulzuul--- features: - | Include a publisher for the Monasca API. A ``monasca://`` pipeline sink will send data to a Monasca instance, using credentials configured in ceilometer.conf. This functionality was previously available in the Ceilosca project (https://github.com/openstack/monasca-ceilometer). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/index-events-mongodb-63cb04200b03a093.yaml0000664000175000017500000000033215250267452026110 0ustar00zuulzuul--- upgrade: - > Run db-sync to add new indices. fixes: - > [`bug 1526793 `_] Additional indices were added to better support querying of event data. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/instance-discovery-new-default-7f9b451a515dddf4.yaml0000664000175000017500000000051415250267452030356 0ustar00zuulzuul--- upgrade: - | Ceilometer legacy backends and Ceilometer API are now deprecated. Polling all nova instances from compute agent is no more required with Gnocchi. So we switch the [compute]instance_discovery_method to libvirt_metadata. To switch back to the old deprecated behavior you can set it back to 'naive'. ././@PaxHeader0000000000000000000000000000021600000000000010214 xustar00114 path=ceilometer-27.0.0.0rc1/releasenotes/notes/instance-record-launched-created-deleted-d7f44df3bbcf0790.yaml 28 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/instance-record-launched-created-deleted-d7f44df3bbcf0790.0000664000175000017500000000014315250267452031400 0ustar00zuulzuul--- features: - | `launched_at`/`created_at`/`deleted_at` of Nova instances are now tracked. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/keystone-v3-fab1e257c5672965.yaml0000664000175000017500000000010315250267452024361 0ustar00zuulzuul--- features: - > Add support for Keystone v3 authentication ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/kwapi_deprecated-c92b9e72c78365f0.yaml0000664000175000017500000000017215250267452025500 0ustar00zuulzuul--- deprecations: - The Kwapi pollsters are deprecated and will be removed in the next major version of Ceilometer. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/less-nova-polling-ac56687da3f8b1a3.yaml0000664000175000017500000000225615250267452025707 0ustar00zuulzuul--- features: - The Ceilometer compute agent can now retrieve some instance metadata from the metadata libvirt API instead of polling the Nova API. Since Mitaka, Nova fills this metadata with some information about the instance. To enable this feature you should set [compute]/instance_discovery_method = libvirt_metadata in the configuration file. The only downside of this method is that user_metadata (and some other instance attributes) are no longer part of the samples created by the agent. But when Gnocchi is used as backend, this is not an issue since Gnocchi doesn't store resource metadata aside of the measurements. And the missing information is still retrieved through the Nova notifications and will fully update the resource information in Gnocchi. upgrade: - If you are using Gnocchi as backend it's strongly recommended to switch [compute]/instance_discovery_method to libvirt_metadata. This will reduce the load on the Nova API especially if you have many compute nodes. deprecations: - The [compute]/workload_partitioning = True is deprecated in favor of [compute]/instance_discovery_method = workload_partitioning ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/log-error-alarm-rgw-pollsters-c53e01b411864840.yaml0000664000175000017500000000121115250267452027636 0ustar00zuulzuul--- fixes: - | The alarm discovery pollster now logs an error instead of raising an exception with a full traceback when the aodh metric endpoint is disabled (aodh returns 403 Forbidden). Similarly, the RGW pollster now logs an error instead of raising a ``RuntimeError`` when neither ``rgw_client.rgw_service_name`` nor ``service_types.radosgw`` is configured. This better aligns their behavior with other pollsters such as Manila and Designate, and significantly reduces log noise when all pollsters are enabled via the ``*`` wildcard. See `bug 2161437 `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2903566 ceilometer-27.0.0.0rc1/releasenotes/notes/lookup-meter-def-vol-correctly-0122ae429275f2a6.yaml0000664000175000017500000000054115250267452030060 0ustar00zuulzuul--- fixes: - > [`bug 1536699 `_] Patch to fix volume field lookup in meter definition file. In case the field is missing in the definition, it raises a keyerror and aborts. Instead we should skip the missing field meter and continue with the rest of the definitions. ././@PaxHeader0000000000000000000000000000021300000000000010211 xustar00111 path=ceilometer-27.0.0.0rc1/releasenotes/notes/make-image-format-attributes-optional-cc7acd492f791553.yaml 28 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/make-image-format-attributes-optional-cc7acd492f791553.yam0000664000175000017500000000057015250267452031400 0ustar00zuulzuul--- upgrade: - | The ``container_format`` and ``disk_format`` attributes have been made optional for ``image`` resources in Gnocchi. This fixes an issue where image resources would fail to be created in Gnocchi because ``container_format`` and ``disk_format`` are set to ``null`` (which is possible on images that haven't had data uploaded to them yet). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/make-instance-host-optional-972fa14405c1e2f6.yaml0000664000175000017500000000046515250267452027510 0ustar00zuulzuul--- upgrade: - | The ``instance`` resource type has been updated to make the ``host`` resource attribute optional. This allows the hypervisor a compute instance is running on to be withheld from Gnocchi's resource metadata, which may be required for security reasons e.g. for public clouds. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/manager-based-ipc-queues-85e3bf59ffdfb0ac.yaml0000664000175000017500000000154115250267452027324 0ustar00zuulzuul--- features: - | Workload partitioning of notification agent is now split into queues based on pipeline type (sample, event, etc...) rather than per individual pipeline. This will save some memory usage specifically for pipeline definitions with many source/sink combinations. upgrade: - | If workload partitioning of the notification agent is enabled, the notification agent should not run alongside pre-Queens agents. Doing so may result in missed samples when leveraging transformations. To upgrade without loss of data, set `notification_control_exchanges` option to empty so only existing `ceilometer-pipe-*` queues are processed. Once cleared, reset `notification_control_exchanges` option and launch the new notification agent(s). If `workload_partitioning` is not enabled, no special steps are required. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/memory-bandwidth-meter-f86cf01178573671.yaml0000664000175000017500000000025215250267452026426 0ustar00zuulzuul--- features: - Add two new meters, including memory.bandwidth.total and memory.bandwidth.local, to get memory bandwidth statistics based on Intel CMT feature. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/mongodb-handle-large-numbers-7c235598ca700f2d.yaml0000664000175000017500000000050615250267452027611 0ustar00zuulzuul--- fixes: - > [`bug 1532661 `_] Fix statistics query failures due to large numbers stored in MongoDB. Data from MongoDB is returned as Int64 for big numbers when int and float types are expected. The data is cast to appropriate type to handle large data. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/network-statistics-from-opendaylight-787df77484d8d751.yaml0000664000175000017500000000042215250267452031450 0ustar00zuulzuul--- prelude: > Network Statistics From OpenDaylight. features: - Add a ceilometer driver to collect network statistics information using REST APIs exposed by network-statistics module in OpenDaylight. - Add support for network statistics meters with gnocchi ././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00121 path=ceilometer-27.0.0.0rc1/releasenotes/notes/openstack-dynamic-pollsters-metadata-enrichment-703cf5914cf0c578.yaml 28 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/openstack-dynamic-pollsters-metadata-enrichment-703cf5914c0000664000175000017500000000015115250267452031743 0ustar00zuulzuul--- features: - | OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's data. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/parallel_requests_option-a3f901b6001e26e4.yaml0000664000175000017500000000036615250267452027277 0ustar00zuulzuul--- features: - | A new option named `max_parallel_requests` is available to control the maximum number of parallel requests that can be executed by the agents. This option also replaces the `poolsize` option of the HTTP publisher. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2913566 ceilometer-27.0.0.0rc1/releasenotes/notes/parallels-virt_type-ee29c4802fdf5c8e.yaml0000664000175000017500000000012415250267452026422 0ustar00zuulzuul--- fixes: - | The ``[DEFAULT] virt_type`` option now supports ``parallels``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/pecan-debug-removed-dc737efbf911bde7.yaml0000664000175000017500000000007615250267452026314 0ustar00zuulzuul--- upgrade: - The api.pecan_debug option has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/perf-events-meter-b06c2a915c33bfaf.yaml0000664000175000017500000000042515250267452025746 0ustar00zuulzuul--- features: - Add four new meters, including perf.cpu.cycles for the number of cpu cycles one instruction needs, perf.instructions for the count of instructions, perf.cache_references for the count of cache hits and cache_misses for the count of caches misses. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/pipeline-fallback-polling-3d962a0fff49ccdd.yaml0000664000175000017500000000030515250267452027500 0ustar00zuulzuul--- upgrade: - | The deprecated support of configure polling in the `pipeline.yaml` file has been removed. Ceilometer now only uses the `polling.yaml` file for polling configuration. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/polling-batch-size-7fe11925df8d1221.yaml0000664000175000017500000000064415250267452025667 0ustar00zuulzuul--- features: - > Add support for configuring the size of samples the poller will send in each batch. upgrade: - > batch_size option added to [polling] section of configuration. Use batch_size=0 to disable batching of samples. deprecations: - > The option batch_polled_samples in the [DEFAULT] section is deprecated. Use batch_size option in [polling] to configure and/or disable batching. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/polling-definition-efffb92e3810e571.yaml0000664000175000017500000000112015250267452026123 0ustar00zuulzuul--- upgrade: - Pipeline processing in polling agents was removed in Liberty cycle. A new polling specific definition file is created to handle polling functionality and pipeline definition file is now reserved exclusively for transformations and routing. The polling.yaml file follows the same syntax as the pipeline.yaml but only handles polling attributes such as interval, discovery, resources, meter matching. It is configured by setting cfg_file under the polling section.If no polling definition file is found, it will fallback to reuse pipeline_cfg_file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/polling-deprecation-4d5b83180893c053.yaml0000664000175000017500000000022715250267452025772 0ustar00zuulzuul--- deprecations: - | Usage of pipeline.yaml for polling configuration is now deprecated. The dedicated polling.yaml should be used instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2923567 ceilometer-27.0.0.0rc1/releasenotes/notes/prometheus-bcb201cfe46d5778.yaml0000664000175000017500000000014015250267452024517 0ustar00zuulzuul--- features: - | A new publisher have been added to push data to Prometheus Pushgateway. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2933567 ceilometer-27.0.0.0rc1/releasenotes/notes/prometheus-pushgateway-grouping-a1b2c3d4e5f6a7b8.yaml0000664000175000017500000000121515250267452030765 0ustar00zuulzuul--- features: - | The Prometheus Pushgateway publisher now supports grouping labels in the URL path. When grouping labels are present after the job segment (e.g. ``prometheus://gateway/metrics/job/ceilometer/instance/myhost``), a single push is performed preserving operator-defined grouping. Without grouping labels, metrics are pushed per resource using ``resource_id`` as the grouping label to avoid overwriting time series across resources. fixes: - | Metric names containing hyphens are now sanitized to underscores before being pushed to the Prometheus Pushgateway, matching Prometheus naming requirements. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2933567 ceilometer-27.0.0.0rc1/releasenotes/notes/publish-fw-with-invalid-state-e961372c95791bcd.yaml0000664000175000017500000000116215250267452027777 0ustar00zuulzuul--- upgrade: - | The ``network.services.firewall`` pollster now publishes samples for all found firewalls, even if they are known to have an unknown state, when they would previously be dropped. The volume of samples for such firewalls will be set to ``-1``. This improves visibility of firewalls with unknown states, allowing them to be monitored via samples and the Gnocchi/Prometheus metrics, making it easier to discover such resources for troubleshooting. It also moves some of the "business logic" for downstream rating/billing services such as CloudKitty out of Ceilometer itself. ././@PaxHeader0000000000000000000000000000022200000000000010211 xustar00118 path=ceilometer-27.0.0.0rc1/releasenotes/notes/publish-network-resources-with-invalid-state-6693c6fa1fefa097.yaml 28 mtime=1788964650.2933567 ceilometer-27.0.0.0rc1/releasenotes/notes/publish-network-resources-with-invalid-state-6693c6fa1fefa0000664000175000017500000000154115250267452032023 0ustar00zuulzuul--- upgrade: - | The ``ip.floating`` and ``network.services.vpn`` pollsters now publish samples for all found floating IPs and VPNs, even if they are known to have an unknown state, when they would previously be dropped. The volume of samples for such floating IPs and VPNs will be set to ``-1``. This improves visibility of floating IPs and VPNs with unknown states, allowing them to be monitored via samples and the Gnocchi metrics, making it easier to discover such resources for troubleshooting. It also moves some of the "business logic" for downstream rating/billing services such as CloudKitty out of Ceilometer itself. - | The ``network.services.vpn`` now publishes samples for VPNs with status ``ERROR``, when they would previously be dropped. The sample volume for VPNs in ``ERROR`` state is ``7``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2933567 ceilometer-27.0.0.0rc1/releasenotes/notes/refresh-legacy-cache-e4dbbd3e2eeca70b.yaml0000664000175000017500000000065415250267452026564 0ustar00zuulzuul--- fixes: - | A local cache is used when polling instance metrics to minimise calls Nova API. A new option is added `resource_cache_expiry` to configure a time to live for cache before it expires. This resolves issue where migrated instances are not removed from cache. This is only relevant when `instance_discovery_method` is set to `naive`. It is recommended to use `libvirt_metadata` if possible. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-alarms-4df3cdb4f1fb5faa.yaml0000664000175000017500000000020515250267452025371 0ustar00zuulzuul--- features: - > Ceilometer alarms code is now fully removed from code base. Equivalent functionality is handled by Aodh. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-aliases-for-telemetry_secret-5112a88118973427.yaml0000664000175000017500000000036315250267452030753 0ustar00zuulzuul--- upgrade: - | The following deprecated aliases of the ``[publisher] telemetry_secret`` option have been removed. - ``[DEFAULT] metering_secret`` - ``[publisher_rpc] metering_secret`` - ``[publisher] metering_secret`` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-batch_polled_samples-b40241c8aad3667d.yaml0000664000175000017500000000011015250267452027667 0ustar00zuulzuul--- upgrade: - | Remove deprecated option `batch_polled_samples`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-cadf-http-f8449ced3d2a29d4.yaml0000664000175000017500000000037315250267452025510 0ustar00zuulzuul--- features: - > Support for CADF-only payload in HTTP dispatcher is dropped as audit middleware in pyCADF was dropped in Kilo cycle. upgrade: - > audit middleware in keystonemiddleware library should be used for similar support. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-ceilometer-dbsync-53aa1b529f194f15.yaml0000664000175000017500000000014615250267452027071 0ustar00zuulzuul--- other: - The deprecated ceilometer-dbsync has been removed. Use ceilometer-upgrade instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-check_watchers-a7c955703b6d9f57.yaml0000664000175000017500000000013115250267452026452 0ustar00zuulzuul--- upgrade: - | The ``[coordination] check_watchers`` parameter has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2943566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-compute-disk-meters-264e686622886ff0.yaml0000664000175000017500000000016515250267452027247 0ustar00zuulzuul--- upgrade: - | The deprecated `disk.*` meters have been removed. Use the `disk.device.*` meters instead. ././@PaxHeader0000000000000000000000000000021300000000000010211 xustar00111 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-compute-rate-deprecated-meters-201893c6b686b04a.yaml 28 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-compute-rate-deprecated-meters-201893c6b686b04a.yam0000664000175000017500000000053315250267452031225 0ustar00zuulzuul--- upgrade: - | The deprecated meter for compute where removed: - disk.read.requests.rate - disk.write.requests.rate - disk.read.bytes.rate - disk.write.bytes.rate - disk.device.read.requests.rate - disk.device.write.requests.rate - disk.device.read.bytes.rate - disk.device.write.bytes.rate ././@PaxHeader0000000000000000000000000000022100000000000010210 xustar00117 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-compute-workload-partitioning-option-26538bc1e80500e3.yaml 28 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-compute-workload-partitioning-option-26538bc1e805000000664000175000017500000000022315250267452031506 0ustar00zuulzuul--- upgrade: - | The deprecated `compute.workload_partitioning` option has been removed in favor of `compute.instance_discovery_method`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-direct-publisher-5785ee7edd16c4d9.yaml0000664000175000017500000000012715250267452027112 0ustar00zuulzuul--- upgrade: - | Remove direct publisher and use the explicit publisher instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-eventlet-6738321434b60c78.yaml0000664000175000017500000000012715250267452025074 0ustar00zuulzuul--- features: - > Remove eventlet from Ceilometer in favour of threaded approach ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-exchange-control-options-75ecd49423639068.yaml0000664000175000017500000000012215250267452030265 0ustar00zuulzuul--- upgrade: - | The deprecated control exchange options have been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-file-dispatcher-56ba1066c20d314a.yaml0000664000175000017500000000011015250267452026473 0ustar00zuulzuul--- upgrade: - | The deprecated file dispatcher has been removed. ././@PaxHeader0000000000000000000000000000022200000000000010211 xustar00118 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-generic-hardware-declarative-pollster-e05c614f273ab149.yaml 28 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-generic-hardware-declarative-pollster-e05c614f273ab0000664000175000017500000000036715250267452031617 0ustar00zuulzuul--- upgrade: - | ``GenericHardwareDeclarativePollster`` has been removed. Because of this removal all metrics gathered by SNMP daemon have been removed as well. - | The ``NodesDiscoveryTripleO`` discovery plugin has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-gnocchi-dispatcher-dd588252976c2abb.yaml0000664000175000017500000000055415250267452027313 0ustar00zuulzuul--- upgrade: - | The Gnocchi dispatcher has been removed and replaced by a native Gnocchi publisher. The configuration options from the `[dispatcher_gnocchi]` has been removed and should be passed via the URL in `pipeline.yaml`. The service authentication override can be done by adding specific credentials to a `[gnocchi]` section instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-gnocchi-dispatcher-options-4f4ba2a155c1a766.yaml0000664000175000017500000000013215250267452030755 0ustar00zuulzuul--- upgrade: - | The deprecated `gnocchi_dispatcher` option group has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-http-dispatcher-1afdce1d1dc3158d.yaml0000664000175000017500000000011015250267452027044 0ustar00zuulzuul--- upgrade: - | The deprecated http dispatcher has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2953568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-http_timeout-5bced56beb771f2d.yaml0000664000175000017500000000013215250267452026506 0ustar00zuulzuul--- upgrade: - | The deprecated ``[DEFAULT] http_timeout`` option has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-intel-cmt-perf-meters-15d0fe72b2804f48.yaml0000664000175000017500000000042315250267452027605 0ustar00zuulzuul--- upgrade: - | The following meters were removed. Nova removed support for Intel CMT perf events in 22.0.0, and these meters can no longer be measured since then. - ``cpu_l3_cache_usage`` - ``memory_bandwidth_local`` - ``memory_bandwidth_total`` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-intel-node-manager-0889de66dede9ab0.yaml0000664000175000017500000000010315250267452027357 0ustar00zuulzuul--- upgrade: - | Support for Intel Node Manager was removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-kafka-broker-publisher-7026b370cfc831db.yaml0000664000175000017500000000014715250267452030070 0ustar00zuulzuul--- upgrade: - | The deprecated kafka publisher has been removed, use NotifierPublisher instead. ././@PaxHeader0000000000000000000000000000021600000000000010214 xustar00114 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-meter-definitions-cfg-file-config-476596fc86c36a81.yaml 28 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-meter-definitions-cfg-file-config-476596fc86c36a81.0000664000175000017500000000022115250267452031077 0ustar00zuulzuul--- upgrade: - | Remove deprecated option meter_definitions_cfg_file, use meter_definitions_dirs to configure meter notification file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-meter-definitions-cfg-file-d57c726d563d805f.yaml0000664000175000017500000000013415250267452030573 0ustar00zuulzuul--- upgrade: - | The deprecated `meter_definitions_cfg_file` option has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-monasca-d5ceda231839d43d.yaml0000664000175000017500000000011415250267452025237 0ustar00zuulzuul--- upgrade: - | Remove integration with the inactive Monasca project ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-neutron-lbaas-d3d4a5327f6a167a.yaml0000664000175000017500000000011515250267452026307 0ustar00zuulzuul--- upgrade: - | Support for neutron-lbaas resources has been removed. ././@PaxHeader0000000000000000000000000000021700000000000010215 xustar00115 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-notification-workload-partitioning-2cef114fb2478e39.yaml 28 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-notification-workload-partitioning-2cef114fb2478e390000664000175000017500000000014515250267452031621 0ustar00zuulzuul--- upgrade: - | The deprecated workload partitioning for notification agent has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2963567 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-nova-http-log-option-64e97a511e58da5d.yaml0000664000175000017500000000012515250267452027555 0ustar00zuulzuul--- upgrade: - | The deprecated `nova_http_log_debug` option has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-opencontrail-88656a9354179299.yaml0000664000175000017500000000025615250267452025717 0ustar00zuulzuul--- upgrade: - | Support for Open Contrail has been removed. Because no SDN is supported after the removal, the mechanism to pull metrics from SDN is also removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-opendaylight-c3839bbe9aa2a227.yaml0000664000175000017500000000010215250267452026277 0ustar00zuulzuul--- upgrade: - | Support for OpenDaylight has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-pollster-list-bda30d747fb87c9e.yaml0000664000175000017500000000011715250267452026527 0ustar00zuulzuul--- upgrade: - | The deprecated `pollster-list` option has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-publisher-topic-options-7a40787a3998921d.yaml0000664000175000017500000000031415250267452030143 0ustar00zuulzuul--- upgrade: - | The notifier publisher options `metering_topic` and `event_topic` are deprecated and will be removed. Use the `topic` query parameter in the notifier publisher URL instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-py38-80670bdcfd4dd135.yaml0000664000175000017500000000016615250267452024426 0ustar00zuulzuul--- upgrade: - | Python 3.8 support was dropped. The minimum version of Python now supported is Python 3.9. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-py39-8c39f81f856bee9f.yaml0000664000175000017500000000016615250267452024464 0ustar00zuulzuul--- upgrade: - | Support for Python 3.9 has been removed. Now Python 3.10 is the minimum version supported. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-refresh-pipeline-618af089c5435db7.yaml0000664000175000017500000000065115250267452026736 0ustar00zuulzuul--- deprecations: - | The pipeline dynamic refresh code has been removed. Ceilometer relies on the cotyledon library for a few releases which provides reload functionality by sending the SIGHUP signal to the process. This achieves the same feature while making sure the reload is explicit once the file is correctly and entirely written to the disk, avoiding the failing load of half-written files. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2973566 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-rpc-collector-d0d0a354140fd107.yaml0000664000175000017500000000051015250267452026203 0ustar00zuulzuul--- features: - > RPC collector support is dropped. The queue-based notifier publisher and collector was added as the recommended alternative as of Icehouse cycle. upgrade: - > Pipeline.yaml files for agents should be updated to notifier:// or udp:// publishers. The rpc:// publisher is no longer supported. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-sahara-9254593d4fb137b9.yaml0000664000175000017500000000042215250267452024653 0ustar00zuulzuul--- upgrade: - | Default value of the ``[notification] notification_control_exchanges`` option has been updated and ``sahara`` is no longer included by default. - | The default event definition has been updated and no longer includes events for sahara. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-service-type-volume-v2-08c81098dc7c0922.yaml0000664000175000017500000000022415250267452027655 0ustar00zuulzuul--- features: - | The deprecated ``[service_types] cinderv2`` option has been removed. Use the ``[service_types] cinder`` option instead. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00120 path=ceilometer-27.0.0.0rc1/releasenotes/notes/remove-shuffle_time_before_polling_task-option-05a4d225236c64b1.yaml 28 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-shuffle_time_before_polling_task-option-05a4d2252360000664000175000017500000000024415250267452031734 0ustar00zuulzuul--- deprecations: - | The `shuffle_time_before_polling_task` option has been removed. This option never worked in the way it was originally intended too. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-transformers-14e00a789dedd76b.yaml0000664000175000017500000000013015250267452026345 0ustar00zuulzuul--- upgrade: - | The support for transformers has been removed from the pipeline. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-uml-e86feeabdd16c628.yaml0000664000175000017500000000022215250267452024564 0ustar00zuulzuul--- upgrade: - | The ``[DEFAULT] virt_type`` option no longer supports ``uml``. UML support by nova was removed in nova 23.3.0 release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-vsphere-support-411c97b66bdcd264.yaml0000664000175000017500000000042415250267452026731 0ustar00zuulzuul--- upgrade: - | Support for VMware vSphere has been removed. deprecations: - | The ``[DEFAULT] hypervisor_inspector`` option has been deprecated, because libvirt is the only supported hypervisor currently. The option will be removed in a future release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-windows-support-0d280cc7c7fffc61.yaml0000664000175000017500000000025215250267452027107 0ustar00zuulzuul--- upgrade: - | Support for running ceilometer in Windows operating systems has been removed. Because of the removal, Hyper-V inspector has also been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/remove-xen-support-7cb932b7bc621269.yaml0000664000175000017500000000012215250267452025764 0ustar00zuulzuul--- upgrade: - | Support for XenServer/Xen Cloud Platform has been removed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/removed-rgw-ae3d80c2eafc9319.yaml0000664000175000017500000000013515250267452024644 0ustar00zuulzuul--- upgrade: - | Deprecated `rgw.*` meters have been removed. Use `radosgw.*` instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2983568 ceilometer-27.0.0.0rc1/releasenotes/notes/rename-ceilometer-dbsync-eb7a1fa503085528.yaml0000664000175000017500000000106615250267452027043 0ustar00zuulzuul--- prelude: > Ceilometer backends are no more only databases but also REST API like Gnocchi. So ceilometer-dbsync binary name doesn't make a lot of sense and have been renamed ceilometer-upgrade. The new binary handles database schema upgrade like ceilometer-dbsync does, but it also handle any changes needed in configured ceilometer backends like Gnocchi. deprecations: - For backward compatibility reason we temporary keep ceilometer-dbsync, at least for one major version to ensure deployer have time update their tooling. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/rename-tenant_name_discovery-1675a236bb51176b.yaml0000664000175000017500000000024515250267452027727 0ustar00zuulzuul--- deprecations: - | The ``[polling] tenant_name_discovery`` option has been deprecated in favor of the new ``[polling] identity_name_discovery`` option. ././@PaxHeader0000000000000000000000000000021400000000000010212 xustar00112 path=ceilometer-27.0.0.0rc1/releasenotes/notes/replace-cinderclient-with-openstacksdk-c129982a98ecbdf2.yaml 28 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/replace-cinderclient-with-openstacksdk-c129982a98ecbdf2.ya0000664000175000017500000000107115250267452031524 0ustar00zuulzuul--- upgrade: - | The Cinder integration now uses ``openstacksdk`` instead of ``python-cinderclient``. The ``python-cinderclient`` package is no longer a dependency of Ceilometer and can be removed from environments that do not require it for other purposes. fixes: - | ``VolumeProviderPoolCapacityTotal`` and ``VolumeProviderPoolCapacityFree`` no longer raise an exception or emit a sample with a ``None`` volume when a pool does not report the corresponding capacity field in its capabilities. Such pools are now silently skipped. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/report-metrics-shutoff-vms-b041bea41b39dca4.yaml0000664000175000017500000000104315250267452027622 0ustar00zuulzuul--- features: - | A new ``[compute] report_stopped_instance_metrics`` option (disabled by default) allows the libvirt compute pollster to report a minimal set of metrics for stopped (SHUTOFF) VMs: ``power.state``, ``vcpus``, ``memory``, ``disk.device.capacity``, and ``disk.device.usage``. Runtime metrics (such as ``cpu``, ``memory.usage``, ``memory.resident``, network and disk I/O stats) are never reported for stopped VMs. Shelved-offloaded VMs are not affected as Nova removes their libvirt domain entirely. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/rgw-tls-config-options-d76125440231dbd2.yaml0000664000175000017500000000207215250267452026427 0ustar00zuulzuul--- features: - | Added ``insecure`` and ``cafile`` configuration options to the ``[rgw_client]`` section for controlling HTTPS certificate verification when connecting to the Ceph RGW Admin API. The ``insecure`` option disables certificate verification, and ``cafile`` specifies a PEM encoded Certificate Authority to use instead of the system CA bundle. Previously, RGW Admin API connections always skipped certificate verification. deprecations: - | The ``[rgw_client] insecure`` option currently defaults to ``True`` to preserve backward compatibility. This will be changed to ``False`` in a future release. Deployments using HTTPS for RGW should set ``insecure`` to ``False`` and configure ``cafile`` if a custom CA is needed. security: - | The RGW Admin API client previously hardcoded ``verify=False``, unconditionally skipping HTTPS certificate verification. New ``[rgw_client] insecure`` and ``[rgw_client] cafile`` options allow operators to enable proper certificate verification for RGW connections. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/save-rate-in-gnocchi-66244262bc4b7842.yaml0000664000175000017500000000145215250267452025735 0ustar00zuulzuul--- features: - | Archive policies can now be configured per metrics in gnocchi_resources.yaml. A default list of archive policies is now created by Ceilometer. They are called "ceilometer-low-rate" for all IOs metrics and "ceilometer-low" for others. upgrade: - | Ceilometer now creates it own archive policies in Gnocchi and use them to create metrics in Gnocchi. Old metrics kept their current archive policies and will not be updated with ceilometer-upgrade. Only newly created metrics will be impacted. Archive policy can still be overridden with the publisher url (e.g: gnocchi://archive_policy=high). deprecations: - | cpu_util and \*.rate meters are deprecated and will be removed in future release in favor of the Gnocchi rate calculation equivalent. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/scan-domains-for-tenants-8f8c9edcb74cc173.yaml0000664000175000017500000000017715250267452027243 0ustar00zuulzuul--- features: - The tenant (project) discovery code in the polling agent now scans for tenants in all available domains. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/selective-pipeline-notification-47e8a390b1c7dcc4.yaml0000664000175000017500000000063215250267452030603 0ustar00zuulzuul--- features: - | The notification-agent can now be configured to either build meters or events. By default, the notification agent will continue to load both pipelines and build both data models. To selectively enable a pipeline, configure the `pipelines` option under the `[notification]` section. Addition pipelines can be created following the format used by existing pipelines. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/ship-yaml-files-33aa5852bedba7f0.yaml0000664000175000017500000000036515250267452025406 0ustar00zuulzuul--- other: - | Ship YAML files to ceilometer/pipeline/data/ make it convenient to update all the files on upgrade. Users can copy yaml files from /usr/share/ceilometer and customise their own files located in /etc/ceilometer/. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/single-thread-pipelines-f9e6ac4b062747fe.yaml0000664000175000017500000000107015250267452027053 0ustar00zuulzuul--- upgrade: - Batching is enabled by default now when coordinated workers are enabled. Depending on load, it is recommended to scale out the number of `pipeline_processing_queues` to improve distribution. `batch_size` should also be configured accordingly. fixes: - Fix to improve handling messages in environments heavily backed up. Previously, notification handlers greedily grabbed messages from queues which could cause ordering issues. A fix was applied to sequentially process messages in a single thread to prevent ordering issues. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/skip-duplicate-meter-def-0420164f6a95c50c.yaml0000664000175000017500000000055415250267452026667 0ustar00zuulzuul --- fixes: - > [`bug 1536498 `_] Patch to fix duplicate meter definitions causing duplicate samples. If a duplicate is found, log a warning and skip the meter definition. Note that the first occurrence of a meter will be used and any following duplicates will be skipped from processing. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.2993567 ceilometer-27.0.0.0rc1/releasenotes/notes/snmp-cpu-util-055cd7704056c1ce.yaml0000664000175000017500000000072315250267452024701 0ustar00zuulzuul--- features: - | new metrics are available for snmp polling hardware.cpu.user, hardware.cpu.nice, hardware.cpu.system, hardware.cpu.idle, hardware.cpu.wait, hardware.cpu.kernel, hardware.cpu.interrupt. They replace deprecated hardware.cpu.util and hardware.system_stats.cpu.idle. deprecations: - | metrics hardware.cpu.util and hardware.system_stats.cpu.idle are now deprecated. Other hardware.cpu.* metrics should be used instead. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/snmp-diskio-samples-fc4b5ed5f19c096c.yaml0000664000175000017500000000016215250267452026315 0ustar00zuulzuul--- features: - | Add hardware.disk.read.* and hardware.disk.write.* metrics to capture diskio details. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/sql-query-optimisation-ebb2233f7a9b5d06.yaml0000664000175000017500000000033515250267452027003 0ustar00zuulzuul--- fixes: - > [`bug 1506738 `_] [`bug 1509677 `_] Optimise SQL backend queries to minimise query load ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-None-query-45abaae45f08eda4.yaml0000664000175000017500000000023715250267452026236 0ustar00zuulzuul--- fixes: - > [`bug 1388680 `_] Suppose ability to query for None value when using SQL backend. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00120 path=ceilometer-27.0.0.0rc1/releasenotes/notes/support-cinder-volume-snapshot-backup-metering-d0a93b86bd53e803.yaml 28 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-cinder-volume-snapshot-backup-metering-d0a93b86bd50000664000175000017500000000022115250267452032011 0ustar00zuulzuul--- features: - Add support of metering the size of cinder volume/snapshot/backup. Like other meters, these are useful for billing system. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-lbaasv2-polling-c830dd49bcf25f64.yaml0000664000175000017500000000117415250267452027046 0ustar00zuulzuul--- features: - > Support for polling Neutron's LBaaS v2 API was added as v1 API in Neutron is deprecated. The same metrics are available between v1 and v2. issues: - > Neutron API is not designed to be polled against. When polling against Neutron is enabled, Ceilometer's polling agents may generate a significant load against the Neutron API. It is recommended that a dedicated API be enabled for polling while Neutron's API is improved to handle polling. upgrade: - > By default, Ceilometer will poll the v2 API. To poll legacy v1 API, add neutron_lbaas_version=v1 option to configuration file. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-meter-batch-recording-mongo-6c2bdf4fbb9764eb.yaml0000664000175000017500000000042715250267452031475 0ustar00zuulzuul--- features: - Add support of batch recording metering data to mongodb backend, since the pymongo support *insert_many* interface which can be used to batch record items, in "big-data" scenarios, this change can improve the performance of metering data recording. ././@PaxHeader0000000000000000000000000000021500000000000010213 xustar00113 path=ceilometer-27.0.0.0rc1/releasenotes/notes/support-multiple-meter-definition-files-e3ce1fa73ef2e1de.yaml 28 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-multiple-meter-definition-files-e3ce1fa73ef2e1de.y0000664000175000017500000000036215250267452031762 0ustar00zuulzuul--- features: - | Support loading multiple meter definition files and allow users to add their own meter definitions into several files according to different types of metrics under the directory of /etc/ceilometer/meters.d.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-snmp-cpu-util-5c1c7afb713c1acd.yaml0000664000175000017500000000022115250267452026675 0ustar00zuulzuul--- features: - > [`bug 1513731 `_] Add support for hardware cpu_util in snmp.yaml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/support-unique-meter-query-221c6e0c1dc1b726.yaml0000664000175000017500000000041215250267452027522 0ustar00zuulzuul--- features: - > [`bug 1506959 `_] Add support to query unique set of meter names rather than meters associated with each resource. The list is available by adding unique=True option to request. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/switch-to-oslo-privsep-b58f20a279f31bc0.yaml0000664000175000017500000000113715250267452026624 0ustar00zuulzuul--- security: - | Privsep transitions. Ceilometer is transitioning from using the older style rootwrap privilege escalation path to the new style Oslo privsep path. This should improve performance and security of Ceilometer in the long term. - | Privsep daemons are now started by Ceilometer when required. These daemons can be started via rootwrap if required. rootwrap configs therefore need to be updated to include new privsep daemon invocations. upgrade: - | The following commands are no longer required to be listed in your rootwrap configuration: ipmitool. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3003566 ceilometer-27.0.0.0rc1/releasenotes/notes/thread-safe-matching-4a635fc4965c5d4c.yaml0000664000175000017500000000034015250267452026227 0ustar00zuulzuul--- critical: - > [`bug 1519767 `_] fnmatch functionality in python <= 2.7.9 is not threadsafe. this issue and its potential race conditions are now patched. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3013566 ceilometer-27.0.0.0rc1/releasenotes/notes/threeads-process-pollsters-cbd22cca6f2effc4.yaml0000664000175000017500000000027115250267452030122 0ustar00zuulzuul--- features: - | Introduce ``threads_to_process_pollsters`` to enable operators to define the number of pollsters that can be executed in parallel inside a polling task. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3013566 ceilometer-27.0.0.0rc1/releasenotes/notes/tooz-coordination-system-d1054b9d1a5ddf32.yaml0000664000175000017500000000034715250267452027326 0ustar00zuulzuul--- upgrade: - | Ceilometer now leverages the latest distribution mechanism provided by the tooz library. Therefore the options `coordination.retry_backoff` and `coordination.max_retry_interval` do not exist anymore. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3013566 ceilometer-27.0.0.0rc1/releasenotes/notes/transformer-ed4b1ea7d1752576.yaml0000664000175000017500000000075215250267452024620 0ustar00zuulzuul--- deprecations: - | Usage of transformers in Ceilometer pipelines is deprecated. Transformers in Ceilometer have never computed samples correctly when you have multiple workers. This functionality can be done by the storage backend easily without all issues that Ceilometer has. For example, the rating is already computed in Gnocchi today. - | Pipeline Partitioning is also deprecated. This was only useful to workaround of some issues that transformers has. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/undeprecate-neutron-fwaas-9ad1b7fb10056e38.yaml0000664000175000017500000000020515250267452027327 0ustar00zuulzuul--- other: - | Support for Neutron FWaaS has been un-deprecated, because FWaaS project was restored and is now maintained. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/unify-timestamp-of-polled-data-fbfcff43cd2d04bc.yaml0000664000175000017500000000037615250267452030551 0ustar00zuulzuul--- fixes: - > [`bug 1491509 `_] Patch to unify timestamp in samples polled by pollsters. Set the time point polling starts as timestamp of samples, and drop timetamping in pollsters. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/use-glance-v2-in-image-pollsters-137a315577d5dc4c.yaml0000664000175000017500000000036215250267452030251 0ustar00zuulzuul--- features: - Since the Glance v1 APIs won't be maintained any more, this change add the support of glance v2 in images pollsters. upgrade: - > The option ``glance_page_size`` has been removed because it's not actually needed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/use-notification-transport-url-489f3d31dc66c4d2.yaml0000664000175000017500000000024515250267452030371 0ustar00zuulzuul--- fixes: - The transport_url defined in [oslo_messaging_notifications] was never used, which contradicts the oslo_messaging documentation. This is now fixed.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/use-usable-metric-if-available-970ee58e8fdeece6.yaml0000664000175000017500000000012215250267452030351 0ustar00zuulzuul--- features: - use memory usable metric from libvirt memoryStats if available. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/volume-metrics-01ddde0180bc21cb.yaml0000664000175000017500000000020615250267452025331 0ustar00zuulzuul--- upgrade: - | The default ``polling.yaml`` file has been updated and now it enables meters related to cinder by default. ././@PaxHeader0000000000000000000000000000021400000000000010212 xustar00112 path=ceilometer-27.0.0.0rc1/releasenotes/notes/volume-provider-pool-provider-metadata-3f1c2a9d7e5b4c60.yaml 28 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/volume-provider-pool-provider-metadata-3f1c2a9d7e5b4c60.ya0000664000175000017500000000106215250267452031507 0ustar00zuulzuul--- fixes: - | The ``volume.provider.pool.capacity.*`` pollsters now set the ``provider`` resource metadata (the pool name up to ``#``), matching what the ``capacity.pool`` notification meters produce. Without it the Gnocchi publisher could not create ``volume_provider_pool`` resources from polled samples, since that resource type declares ``provider`` as a required attribute mapped from ``resource_metadata.provider``, and every polled pool capacity sample was dropped with ``required key not provided @ data['provider']``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/notes/zaqar-publisher-f7efa030b71731f4.yaml0000664000175000017500000000012615250267452025353 0ustar00zuulzuul--- features: - Add a new publisher for pushing samples or events to a Zaqar queue. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3481274 ceilometer-27.0.0.0rc1/releasenotes/source/0000775000175000017500000000000015250267567017326 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/source/2023.1.rst0000664000175000017500000000021015250267452020567 0ustar00zuulzuul=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3023567 ceilometer-27.0.0.0rc1/releasenotes/source/2023.2.rst0000664000175000017500000000020215250267452020571 0ustar00zuulzuul=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/2024.1.rst0000664000175000017500000000021015250267452020570 0ustar00zuulzuul=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2024.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/2024.2.rst0000664000175000017500000000020215250267452020572 0ustar00zuulzuul=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/2025.1.rst0000664000175000017500000000020215250267452020572 0ustar00zuulzuul=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/2025.2.rst0000664000175000017500000000020215250267452020573 0ustar00zuulzuul=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/2026.1.rst0000664000175000017500000000020215250267452020573 0ustar00zuulzuul=========================== 2026.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2026.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3491275 ceilometer-27.0.0.0rc1/releasenotes/source/_static/0000775000175000017500000000000015250267567020754 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/_static/.placeholder0000664000175000017500000000000015250267452023216 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/conf.py0000664000175000017500000002145715250267452020627 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Ceilometer Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Ceilometer Release Notes' copyright = '2015, Ceilometer Developers' # Release notes do not need a version number in the title, they # cover multiple releases. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # openstackdocstheme options openstackdocs_repo_name = 'openstack/ceilometer' openstackdocs_auto_name = False openstackdocs_bug_project = 'ceilometer' openstackdocs_bug_tag = '' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'CeilometerReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'CeilometerReleaseNotes.tex', 'Ceilometer Release Notes Documentation', 'Ceilometer Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'ceilometerreleasenotes', 'Ceilometer Release Notes Documentation', ['Ceilometer Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'CeilometerReleaseNotes', 'Ceilometer Release Notes Documentation', 'Ceilometer Developers', 'CeilometerReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/index.rst0000664000175000017500000000051615250267452021162 0ustar00zuulzuul========================= Ceilometer Release Notes ========================= .. toctree:: :maxdepth: 1 unreleased 2026.1 2025.2 2025.1 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka liberty ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3033566 ceilometer-27.0.0.0rc1/releasenotes/source/liberty.rst0000664000175000017500000000022015250267452021515 0ustar00zuulzuul============================= Liberty Series Release Notes ============================= .. release-notes:: :branch: origin/stable/liberty ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2231276 ceilometer-27.0.0.0rc1/releasenotes/source/locale/0000775000175000017500000000000015250267567020565 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2221277 ceilometer-27.0.0.0rc1/releasenotes/source/locale/en_GB/0000775000175000017500000000000015250267567021537 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3491275 ceilometer-27.0.0.0rc1/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000015250267567023324 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3043566 ceilometer-27.0.0.0rc1/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000035011215250267452026350 0ustar00zuulzuul# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2019. #zanata # Andi Chandler , 2020. #zanata # Andi Chandler , 2021. #zanata # Andi Chandler , 2022. #zanata # Andi Chandler , 2023. #zanata # Andi Chandler , 2024. #zanata # Andi Chandler , 2025. #zanata # Andi Chandler , 2026. #zanata msgid "" msgstr "" "Project-Id-Version: Ceilometer Release Notes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-08-07 09:58+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2026-08-08 09:49+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "10.0.0" msgstr "10.0.0" msgid "10.0.1" msgstr "10.0.1" msgid "10.0.1-14" msgstr "10.0.1-14" msgid "11.0.0" msgstr "11.0.0" msgid "11.1.0" msgstr "11.1.0" msgid "11.1.0-6" msgstr "11.1.0-6" msgid "12.0.0" msgstr "12.0.0" msgid "12.1.0" msgstr "12.1.0" msgid "13.0.0" msgstr "13.0.0" msgid "13.0.0.0rc1" msgstr "13.0.0.0rc1" msgid "13.1.0" msgstr "13.1.0" msgid "14.0.0" msgstr "14.0.0" msgid "14.1.0-4" msgstr "14.1.0-4" msgid "15.1.0" msgstr "15.1.0" msgid "16.0.0" msgstr "16.0.0" msgid "16.0.1" msgstr "16.0.1" msgid "16.0.1-12" msgstr "16.0.1-12" msgid "17.0.0" msgstr "17.0.0" msgid "17.0.2-6" msgstr "17.0.2-6" msgid "18.0.0" msgstr "18.0.0" msgid "18.1.0" msgstr "18.1.0" msgid "19.0.0" msgstr "19.0.0" msgid "19.1.0" msgstr "19.1.0" msgid "20.0.0" msgstr "20.0.0" msgid "2023.1 Series Release Notes" msgstr "2023.1 Series Release Notes" msgid "2023.2 Series Release Notes" msgstr "2023.2 Series Release Notes" msgid "2024.1 Series Release Notes" msgstr "2024.1 Series Release Notes" msgid "2024.2 Series Release Notes" msgstr "2024.2 Series Release Notes" msgid "2025.1 Series Release Notes" msgstr "2025.1 Series Release Notes" msgid "2025.2 Series Release Notes" msgstr "2025.2 Series Release Notes" msgid "2026.1 Series Release Notes" msgstr "2026.1 Series Release Notes" msgid "21.0.0" msgstr "21.0.0" msgid "22.0.0" msgstr "22.0.0" msgid "22.0.1" msgstr "22.0.1" msgid "23.0.0" msgstr "23.0.0" msgid "23.0.1" msgstr "23.0.1" msgid "24.0.0" msgstr "24.0.0" msgid "24.0.1" msgstr "24.0.1" msgid "25.0.0" msgstr "25.0.0" msgid "26.0.0" msgstr "26.0.0" msgid "26.0.0-81" msgstr "26.0.0-81" msgid "5.0.1" msgstr "5.0.1" msgid "5.0.2" msgstr "5.0.2" msgid "5.0.3" msgstr "5.0.3" msgid "6.0.0" msgstr "6.0.0" msgid "7.0.0" msgstr "7.0.0" msgid "7.0.0.0b2" msgstr "7.0.0.0b2" msgid "7.0.0.0b3" msgstr "7.0.0.0b3" msgid "7.0.0.0rc1" msgstr "7.0.0.0rc1" msgid "7.0.1" msgstr "7.0.1" msgid "7.0.5" msgstr "7.0.5" msgid "8.0.0" msgstr "8.0.0" msgid "9.0.0" msgstr "9.0.0" msgid "" "A ``map`` event trait plugin has been added. This allows notification meter " "attributes to be created by mapping one set of values from an attribute to " "another set of values defined in the meter definition. Additional options " "are also available for controlling how to handle edge cases, such as unknown " "values and case sensitivity." msgstr "" "A ``map`` event trait plugin has been added. This allows notification meter " "attributes to be created by mapping one set of values from an attribute to " "another set of values defined in the meter definition. Additional options " "are also available for controlling how to handle edge cases, such as unknown " "values and case sensitivity." msgid "" "A dogpile.cache supported backend is required to enable cache. Additional " "configuration `options `_ are also required." msgstr "" "A dogpile.cache supported backend is required to enable cache. Additional " "configuration `options `_ are also required." msgid "" "A local cache is used when polling instance metrics to minimise calls Nova " "API. A new option is added `resource_cache_expiry` to configure a time to " "live for cache before it expires. This resolves issue where migrated " "instances are not removed from cache." msgstr "" "A local cache is used when polling instance metrics to minimise calls Nova " "API. A new option is added `resource_cache_expiry` to configure a time to " "live for cache before it expires. This resolves issue where migrated " "instances are not removed from cache." msgid "" "A local cache is used when polling instance metrics to minimise calls Nova " "API. A new option is added `resource_cache_expiry` to configure a time to " "live for cache before it expires. This resolves issue where migrated " "instances are not removed from cache. This is only relevant when " "`instance_discovery_method` is set to `naive`. It is recommended to use " "`libvirt_metadata` if possible." msgstr "" "A local cache is used when polling instance metrics to minimise calls Nova " "API. A new option is added `resource_cache_expiry` to configure a time to " "live for cache before it expires. This resolves issue where migrated " "instances are not removed from cache. This is only relevant when " "`instance_discovery_method` is set to `naive`. It is recommended to use " "`libvirt_metadata` if possible." msgid "" "A new ``loadbalancer`` resource type has been added to gnocchi_resources." "yaml to enable publishing these metrics to Gnocchi." msgstr "" "A new ``loadbalancer`` resource type has been added to gnocchi_resources." "yaml to enable publishing these metrics to Gnocchi." msgid "" "A new option named `max_parallel_requests` is available to control the " "maximum number of parallel requests that can be executed by the agents. This " "option also replaces the `poolsize` option of the HTTP publisher." msgstr "" "A new option named `max_parallel_requests` is available to control the " "maximum number of parallel requests that can be executed by the agents. This " "option also replaces the `poolsize` option of the HTTP publisher." msgid "A new pulisher have been added to push data to Prometheus Pushgateway." msgstr "" "A new publisher have been added to push data to Prometheus Pushgateway." msgid "" "Add Designate DNS zone metrics polling support. This includes a new dns_zone " "resource type for Gnocchi and the following meters: ``dns.zone.status`` " "(zone status as numeric value), ``dns.zone.recordsets`` (number of " "recordsets in a zone), ``dns.zone.ttl`` (zone TTL value), and ``dns.zone." "serial`` (zone serial number). A new discovery plugin ``dns_zones`` is also " "added to discover DNS zones from Designate across all projects." msgstr "" "Add Designate DNS zone metrics polling support. This includes a new dns_zone " "resource type for Gnocchi and the following meters: ``dns.zone.status`` " "(zone status as numeric value), ``dns.zone.recordsets`` (number of " "recordsets in a zone), ``dns.zone.ttl`` (zone TTL value), and ``dns.zone." "serial`` (zone serial number). A new discovery plugin ``dns_zones`` is also " "added to discover DNS zones from Designate across all projects." msgid "" "Add `disk.device.read.latency` and `disk.device.write.latency` meters to " "capture total time used by read or write operations." msgstr "" "Add `disk.device.read.latency` and `disk.device.write.latency` meters to " "capture total time used by read or write operations." msgid "" "Add a ceilometer driver to collect network statistics information using REST " "APIs exposed by network-statistics module in OpenDaylight." msgstr "" "Add a Ceilometer driver to collect network statistics information using REST " "APIs exposed by network-statistics module in OpenDaylight." msgid "Add a new publisher for pushing samples or events to a Zaqar queue." msgstr "Add a new publisher for pushing samples or events to a Zaqar queue." msgid "" "Add a tool for migrating metrics data from Ceilometer's native storage to " "Gnocchi. Since we have deprecated Ceilometer API and the Gnocchi will be the " "recommended metrics data storage backend." msgstr "" "Add a tool for migrating metrics data from Ceilometer's native storage to " "Gnocchi. Since we have deprecated Ceilometer API and the Gnocchi will be the " "recommended metrics data storage backend." msgid "" "Add availability_zone attribute to gnocchi instance resources. Populates " "this attribute by consuming instance.create.end events." msgstr "" "Add availability_zone attribute to gnocchi instance resources. Populates " "this attribute by consuming instance.create.end events." msgid "" "Add dynamic pollster system. The dynamic pollster system enables operators " "to gather new metrics on the fly (without needing to code pollsters)." msgstr "" "Add dynamic pollster system. The dynamic pollster system enables operators " "to gather new metrics on the fly (without needing to code pollsters)." msgid "" "Add four new meters, including perf.cpu.cycles for the number of cpu cycles " "one instruction needs, perf.instructions for the count of instructions, perf." "cache_references for the count of cache hits and cache_misses for the count " "of caches misses." msgstr "" "Add four new meters, including perf.cpu.cycles for the number of cpu cycles " "one instruction needs, perf.instructions for the count of instructions, perf." "cache_references for the count of cache hits and cache_misses for the count " "of caches misses." msgid "" "Add hardware.disk.read.* and hardware.disk.write.* metrics to capture diskio " "details." msgstr "" "Add hardware.disk.read.* and hardware.disk.write.* metrics to capture diskio " "details." msgid "" "Add memory swap metric for VM, including 'memory.swap.in' and 'memory.swap." "out'." msgstr "" "Add memory swap metric for VM, including 'memory.swap.in' and 'memory.swap." "out'." msgid "Add new json output option for the existing file publisher." msgstr "Add new JSON output option for the existing file publisher." msgid "Add support for Keystone v3 authentication" msgstr "Add support for Keystone v3 authentication" msgid "" "Add support for batch processing of messages from queue. This will allow the " "collector and notification agent to grab multiple messages per thread to " "enable more efficient processing." msgstr "" "Add support for batch processing of messages from queue. This will allow the " "collector and notification agent to grab multiple messages per thread to " "enable more efficient processing." msgid "" "Add support for configuring the size of samples the poller will send in each " "batch." msgstr "" "Add support for configuring the size of samples the poller will send in each " "batch." msgid "Add support for network statistics meters with gnocchi" msgstr "Add support for network statistics meters with Gnocchi" msgid "" "Add support of batch recording metering data to mongodb backend, since the " "pymongo support *insert_many* interface which can be used to batch record " "items, in \"big-data\" scenarios, this change can improve the performance of " "metering data recording." msgstr "" "Add support of batch recording metering data to MongoDB backend, since the " "pymongo support *insert_many* interface which can be used to batch record " "items, in \"big-data\" scenarios, this change can improve the performance of " "metering data recording." msgid "" "Add support of metering the size of cinder volume/snapshot/backup. Like " "other meters, these are useful for billing system." msgstr "" "Add support of metering the size of Cinder volume/snapshot/backup. Like " "other meters, these are useful for billing system." msgid "" "Add support to capture volume capacity usage details from cinder. This data " "is extracted from notifications sent by Cinder starting in Ocata." msgstr "" "Add support to capture volume capacity usage details from Cinder. This data " "is extracted from notifications sent by Cinder starting in Ocata." msgid "" "Add the support for non-OpenStack APIs in the dynamic pollster system. This " "extension enables operators to create pollster on the fly to handle metrics " "from systems such as the RadosGW API." msgstr "" "Add the support for non-OpenStack APIs in the dynamic pollster system. This " "extension enables operators to create pollster on the fly to handle metrics " "from systems such as the RadosGW API." msgid "" "Add two new meters, including memory.bandwidth.total and memory.bandwidth." "local, to get memory bandwidth statistics based on Intel CMT feature." msgstr "" "Add two new meters, including memory.bandwidth.total and memory.bandwidth." "local, to get memory bandwidth statistics based on Intel CMT feature." msgid "" "Add volume.volume_type_id and backup.is_incremental metadata for cinder " "pollsters. Also user_id information is now included for backups with the " "generated samples." msgstr "" "Add volume.volume_type_id and backup.is_incremental metadata for Cinder " "pollsters. Also user_id information is now included for backups with the " "generated samples." msgid "" "Added Manila shared file system metrics polling via openstacksdk. New " "metrics:" msgstr "" "Added Manila shared file system metrics polling via openstacksdk. New " "metrics:" msgid "" "Added a new ``memory.available`` compute pollster metric, for tracking the " "amount of memory available within the instance, as seen by the instance. " "This can be combined with ``memory.usage`` in Gnocchi aggregate queries to " "get memory usage for an instance as a percentage." msgstr "" "Added a new ``memory.available`` compute pollster metric, for tracking the " "amount of memory available within the instance, as seen by the instance. " "This can be combined with ``memory.usage`` in Gnocchi aggregate queries to " "get memory usage for an instance as a percentage." msgid "Added new tool ``ceilometer-status upgrade check``." msgstr "Added new tool ``ceilometer-status upgrade check``." msgid "Added support for magnum bay CRUD events, event_type is 'magnum.bay.*'." msgstr "" "Added support for Magnum bay CRUD events, event_type is 'magnum.bay.*'." msgid "" "Added the ``[compute]/fetch_extra_metadata`` configuration option, which " "allows configuration of whether or not Ceilometer fetches additional compute " "instance metadata attributes that require Nova API queries. This mainly " "affects the ``user_metadata`` attributes populated with metering-related " "values such as the server group an instance is part of. When " "``fetch_extra_metadata`` is set to ``False``, Ceilometer Compute Agent will " "not query Nova API for anything unless absolutely necessary." msgstr "" "Added the ``[compute]/fetch_extra_metadata`` configuration option, which " "allows configuration of whether or not Ceilometer fetches additional compute " "instance metadata attributes that require Nova API queries. This mainly " "affects the ``user_metadata`` attributes populated with metering-related " "values such as the server group an instance is part of. When " "``fetch_extra_metadata`` is set to ``False``, Ceilometer Compute Agent will " "not query Nova API for anything unless absolutely necessary." msgid "" "Added the ``volume_type_id`` attribute to ``volume.size`` notification " "samples, which stores the ID for the volume type of the given volume." msgstr "" "Added the ``volume_type_id`` attribute to ``volume.size`` notification " "samples, which stores the ID for the volume type of the given volume." msgid "" "Added the ``volume_type_id`` attribute to ``volume`` resources in Gnocchi, " "which stores the ID for the volume type of the given volume." msgstr "" "Added the ``volume_type_id`` attribute to ``volume`` resources in Gnocchi, " "which stores the ID for the volume type of the given volume." msgid "" "Added the following meters to the central agent to capture these metrics for " "each storage pool by API." msgstr "" "Added the following meters to the central agent to capture these metrics for " "each storage pool by API." msgid "Added the new power.state metric from virDomainState." msgstr "Added the new power.state metric from virDomainState." msgid "" "Addition pipelines can be created following the format used by existing " "pipelines." msgstr "" "Addition pipelines can be created following the format used by existing " "pipelines." msgid "" "After Nova has been upgraded to 2025.2 or later, new instances will start " "providing additional flavor metadata for Ceilometer to use. Instances " "already running at the time of the upgrade are not updated as part of the " "process; to update those instances they will need to be cold restarted, cold " "migrated or shelved-and-unshelved (until this happens, Nova API queries will " "continue to be performed for those instances)." msgstr "" "After Nova has been upgraded to 2025.2 or later, new instances will start " "providing additional flavour metadata for Ceilometer to use. Instances " "already running at the time of the upgrade are not updated as part of the " "process; to update those instances, they will need to be cold restarted, " "cold migrated or shelved-and-unshelved (until this happens, Nova API queries " "will continue to be performed for those instances)." msgid "" "Allow users to add additional exchanges in ceilometer.conf instead of " "hardcoding exchanges. Now original http_control_exchanges is being " "deprecated and renamed notification_control_exchanges. Besides, the new " "option is integrated with other exchanges in default EXCHANGE_OPTS to make " "it available to extend additional exchanges." msgstr "" "Allow users to add additional exchanges in ceilometer.conf instead of " "hardcoding exchanges. Now original http_control_exchanges is being " "deprecated and renamed notification_control_exchanges. Besides, the new " "option is integrated with other exchanges in default EXCHANGE_OPTS to make " "it available to extend additional exchanges." msgid "" "An optional ``storage_policy`` attribute has been added to the " "``swift_account`` Gnocchi resource type, to store the storage policy for " "Swift containers in Gnocchi. For Swift accounts, ``storage_policy`` will be " "set to ``None``." msgstr "" "An optional ``storage_policy`` attribute has been added to the " "``swift_account`` Gnocchi resource type, to store the storage policy for " "Swift containers in Gnocchi. For Swift accounts, ``storage_policy`` will be " "set to ``None``." msgid "" "Any existing commands utilising `image` meter should be switched to `image." "size` meter which will provide equivalent functionality" msgstr "" "Any existing commands utilising `image` meter should be switched to `image." "size` meter which will provide equivalent functionality" msgid "" "Archive policies can now be configured per metrics in gnocchi_resources." "yaml. A default list of archive policies is now created by Ceilometer. They " "are called \"ceilometer-low-rate\" for all IOs metrics and \"ceilometer-" "low\" for others." msgstr "" "Archive policies can now be configured per metrics in gnocchi_resources." "yaml. A default list of archive policies is now created by Ceilometer. They " "are called \"ceilometer-low-rate\" for all IO metrics and \"ceilometer-low\" " "for others." msgid "" "As the collector service is being deprecated, the duplication of publishers " "and dispatchers is being addressed. The http dispatcher is now marked as " "deprecated and the recommended path is to use http publisher." msgstr "" "As the collector service is being deprecated, the duplication of publishers " "and dispatchers is being addressed. The http dispatcher is now marked as " "deprecated and the recommended path is to use http publisher." msgid "" "Batching is enabled by default now when coordinated workers are enabled. " "Depending on load, it is recommended to scale out the number of " "`pipeline_processing_queues` to improve distribution. `batch_size` should " "also be configured accordingly." msgstr "" "Batching is enabled by default now when coordinated workers are enabled. " "Depending on load, it is recommended to scale out the number of " "`pipeline_processing_queues` to improve distribution. `batch_size` should " "also be configured accordingly." msgid "" "Because of deprecating the collector, the default publishers in pipeline." "yaml and event_pipeline.yaml are now changed using database instead of " "notifier." msgstr "" "Because of deprecating the collector, the default publishers in pipeline." "yaml and event_pipeline.yaml are now changed using database instead of " "notifier." msgid "Bug Fixes" msgstr "Bug Fixes" msgid "" "By default, Ceilometer will poll the v2 API. To poll legacy v1 API, add " "neutron_lbaas_version=v1 option to configuration file." msgstr "" "By default, Ceilometer will poll the v2 API. To poll legacy v1 API, add " "neutron_lbaas_version=v1 option to configuration file." msgid "" "Ceilometer API is deprecated. Use the APIs from Aodh (alarms), Gnocchi " "(metrics), and/or Panko (events)." msgstr "" "Ceilometer API is deprecated. Use the APIs from Aodh (alarms), Gnocchi " "(metrics), and/or Panko (events)." msgid "Ceilometer Release Notes" msgstr "Ceilometer Release Notes" msgid "" "Ceilometer alarms code is now fully removed from code base. Equivalent " "functionality is handled by Aodh." msgstr "" "Ceilometer alarms code is now fully removed from code base. Equivalent " "functionality is handled by Aodh." msgid "" "Ceilometer backends are no more only databases but also REST API like " "Gnocchi. So ceilometer-dbsync binary name doesn't make a lot of sense and " "have been renamed ceilometer-upgrade. The new binary handles database schema " "upgrade like ceilometer-dbsync does, but it also handle any changes needed " "in configured ceilometer backends like Gnocchi." msgstr "" "Ceilometer backends are no more only databases but also REST API like " "Gnocchi. So ceilometer-dbsync binary name doesn't make a lot of sense and " "have been renamed ceilometer-upgrade. The new binary handles database schema " "upgrade like ceilometer-dbsync does, but it also handle any changes needed " "in configured Ceilometer backends like Gnocchi." msgid "" "Ceilometer created metrics that could never get measures depending on the " "polling configuration. Metrics are now created only if Ceilometer gets at " "least a measure for them." msgstr "" "Ceilometer created metrics that could never get measures depending on the " "polling configuration. Metrics are now created only if Ceilometer gets at " "least a measure for them." msgid "" "Ceilometer is now able to poll the /metrics endpoint in Aodh to get " "evaluation results metrics." msgstr "" "Ceilometer is now able to poll the /metrics endpoint in Aodh to get " "evaluation results metrics." msgid "" "Ceilometer legacy backends and Ceilometer API are now deprecated. Polling " "all nova instances from compute agent is no more required with Gnocchi. So " "we switch the [compute]instance_discovery_method to libvirt_metadata. To " "switch back to the old deprecated behavior you can set it back to 'naive'." msgstr "" "Ceilometer legacy backends and Ceilometer API are now deprecated. Polling " "all nova instances from compute agent is no more required with Gnocchi. So " "we switch the [compute]instance_discovery_method to libvirt_metadata. To " "switch back to the old deprecated behaviour you can set it back to 'naive'." msgid "" "Ceilometer now creates it own archive policies in Gnocchi and use them to " "create metrics in Gnocchi. Old metrics kept their current archive policies " "and will not be updated with ceilometer-upgrade. Only newly created metrics " "will be impacted. Archive policy can still be overridden with the publisher " "url (e.g: gnocchi://archive_policy=high)." msgstr "" "Ceilometer now creates it own archive policies in Gnocchi and uses them to " "create metrics in Gnocchi. Old metrics keep their current archive policies " "and will not be updated with ceilometer-upgrade. Only newly created metrics " "will be impacted. Archive policy can still be overridden with the publisher " "URL (e.g: gnocchi://archive_policy=high)." msgid "" "Ceilometer now leverages the latest distribution mechanism provided by the " "tooz library. Therefore the options `coordination.retry_backoff` and " "`coordination.max_retry_interval` do not exist anymore." msgstr "" "Ceilometer now leverages the latest distribution mechanism provided by the " "tooz library. Therefore the options `coordination.retry_backoff` and " "`coordination.max_retry_interval` do not exist any more." msgid "" "Ceilometer now supports polling Octavia load balancer metrics using " "openstacksdk. Two new meters are available:" msgstr "" "Ceilometer now supports polling Octavia load balancer metrics using " "openstacksdk. Two new meters are available:" msgid "" "Ceilometer previously did not create IPMI sensor data from IPMI agent or " "Ironic in Gnocchi. This data is now pushed to Gnocchi." msgstr "" "Ceilometer previously did not create IPMI sensor data from IPMI agent or " "Ironic in Gnocchi. This data is now pushed to Gnocchi." msgid "" "Ceilometer sets up the HTTPProxyToWSGI middleware in front of Ceilometer. " "The purpose of this middleware is to set up the request URL correctly in " "case there is a proxy (for instance, a loadbalancer such as HAProxy) in " "front of Ceilometer. So, for instance, when TLS connections are being " "terminated in the proxy, and one tries to get the versions from the / " "resource of Ceilometer, one will notice that the protocol is incorrect; It " "will show 'http' instead of 'https'. So this middleware handles such cases. " "Thus helping Keystone discovery work correctly. The HTTPProxyToWSGI is off " "by default and needs to be enabled via a configuration value." msgstr "" "Ceilometer sets up the HTTPProxyToWSGI middleware in front of Ceilometer. " "The purpose of this middleware is to set up the request URL correctly in " "case there is a proxy (for instance, a load balancer such as HAProxy) in " "front of Ceilometer. So, for instance, when TLS connections are being " "terminated in the proxy, and one tries to get the versions from the / " "resource of Ceilometer, one will notice that the protocol is incorrect; It " "will show 'http' instead of 'https'. So this middleware handles such cases. " "Thus helping Keystone discovery work correctly. The HTTPProxyToWSGI is off " "by default and needs to be enabled via a configuration value." msgid "" "Ceilometer supports generic notifier to publish data and allow user to " "customize parameters such as topic, transport driver and priority. The " "publisher configuration in pipeline.yaml can be notifer://[notifier_ip]:" "[notifier_port]?topic=[topic]&driver=driver&max_retry=100 Not only rabbit " "driver, but also other driver like kafka can be used." msgstr "" "Ceilometer supports generic notifier to publish data and allow user to " "customise parameters such as topic, transport driver and priority. The " "publisher configuration in pipeline.yaml can be notifer://[notifier_ip]:" "[notifier_port]?topic=[topic]&driver=driver&max_retry=100 Not only rabbit " "driver, but also other driver like Kafka can be used." msgid "" "Collector is no longer supported in this release. The collector introduces " "lags in pushing data to backend. To optimize the architecture, Ceilometer " "push data through dispatchers using publishers in notification agent " "directly." msgstr "" "Collector is no longer supported in this release. The collector introduces " "lags in pushing data to backend. To optimise the architecture, Ceilometer " "pushes data through dispatchers using publishers in notification agent " "directly." msgid "" "Configuration values can passed in via the querystring of publisher in " "pipeline. For example, rather than setting target, timeout, verify_ssl, and " "batch_mode under [dispatcher_http] section of conf, you can specify http://" "/?verify_ssl=True&batch=True&timeout=10. Use `raw_only=1` if only " "the raw details of event are required." msgstr "" "Configuration values can passed in via the querystring of publisher in " "pipeline. For example, rather than setting target, timeout, verify_ssl, and " "batch_mode under [dispatcher_http] section of conf, you can specify http://" "/?verify_ssl=True&batch=True&timeout=10. Use `raw_only=1` if only " "the raw details of event are required." msgid "" "Configure individual dispatchers by specifying meter_dispatchers and " "event_dispatchers in configuration file." msgstr "" "Configure individual dispatchers by specifying meter_dispatchers and " "event_dispatchers in configuration file." msgid "Critical Issues" msgstr "Critical Issues" msgid "Current Series Release Notes" msgstr "Current Series Release Notes" msgid "" "Default value of the ``[notification] notification_control_exchanges`` " "option has been updated and ``sahara`` is no longer included by default." msgstr "" "Default value of the ``[notification] notification_control_exchanges`` " "option has been updated and ``sahara`` is no longer included by default." msgid "Deprecated `rgw.*` meters have been removed. Use `radosgw.*` instead." msgstr "Deprecated `rgw.*` meters have been removed. Use `radosgw.*` instead." msgid "" "Deprecating support for enabling pollsters via command line. Meter and " "pollster enablement should be configured via polling.yaml file." msgstr "" "Deprecating support for enabling pollsters via command line. Meter and " "pollster enablement should be configured via polling.yaml file." msgid "Deprecation Notes" msgstr "Deprecation Notes" msgid "" "Enhanced the Prometheus exporter to support TLS for exposing metrics " "securely." msgstr "" "Enhanced the Prometheus exporter to support TLS for exposing metrics " "securely." msgid "" "Filtering out metrics from the Gnocchi service project in the Gnocchi " "publisher can now be disabled using the ``enable_filter_project`` publisher " "option, e.g. ``gnocchi://?enable_filter_project=false`` (``true`` by " "default). This ensures that the Gnocchi service project always has metrics " "published to Gnocchi itself, which is usually the desired behaviour when the " "Swift storage driver is not being used (filtering should be **enabled** when " "using the Swift storage driver, to eliminate feedback loops during metric " "generation)." msgstr "" "Filtering out metrics from the Gnocchi service project in the Gnocchi " "publisher can now be disabled using the ``enable_filter_project`` publisher " "option, e.g. ``gnocchi://?enable_filter_project=false`` (``true`` by " "default). This ensures that the Gnocchi service project always has metrics " "published to Gnocchi itself, which is usually the desired behaviour when the " "Swift storage driver is not being used (filtering should be **enabled** when " "using the Swift storage driver, to eliminate feedback loops during metric " "generation)." msgid "Fix ability to enable/disable radosgw.* meters explicitly" msgstr "Fix ability to enable/disable radosgw.* meters explicitly" msgid "Fix samples from Heat to map to correct Gnocchi resource type" msgstr "Fix samples from Heat to map to correct Gnocchi resource type" msgid "" "Fix to improve handling messages in environments heavily backed up. " "Previously, notification handlers greedily grabbed messages from queues " "which could cause ordering issues. A fix was applied to sequentially process " "messages in a single thread to prevent ordering issues." msgstr "" "Fix to improve handling messages in environments heavily backed up. " "Previously, notification handlers greedily grabbed messages from queues " "which could cause ordering issues. A fix was applied to sequentially process " "messages in a single thread to prevent ordering issues." msgid "" "Fixed `bug #2113768 `__ " "where the Libvirt inspector did not catch exceptions thrown when calling " "interfaceStats function on a domain." msgstr "" "Fixed `bug #2113768 `__ " "where the Libvirt inspector did not catch exceptions thrown when calling " "interfaceStats function on a domain." msgid "" "Following the upgrade, the storage backends Ceilometer publishes to will go " "through an intermediary period where metrics using both the old and new " "units will exist at the same time:" msgstr "" "Following the upgrade, the storage backends Ceilometer publishes to will go " "through an intermediary period where metrics using both the old and new " "units will exist at the same time:" msgid "" "For backward compatibility reason we temporary keep ceilometer-dbsync, at " "least for one major version to ensure deployer have time update their " "tooling." msgstr "" "For backward compatibility reason we temporary keep ceilometer-dbsync, at " "least for one major version to ensure deployers have time update their " "tooling." msgid "Gnocchi dispatcher now uses client rather than direct http requests" msgstr "Gnocchi dispatcher now uses client rather than direct HTTP requests" msgid "" "Identify user and projects names with the help of their UUIDs in the polled " "samples. If they are identified, set \"project_name\" and \"user_name\" " "fields in the sample to the corresponding values." msgstr "" "Identify user and projects names with the help of their UUIDs in the polled " "samples. If they are identified, set \"project_name\" and \"user_name\" " "fields in the sample to the corresponding values." msgid "" "If workload partitioning of the notification agent is enabled, the " "notification agent should not run alongside pre-Queens agents. Doing so may " "result in missed samples when leveraging transformations. To upgrade without " "loss of data, set `notification_control_exchanges` option to empty so only " "existing `ceilometer-pipe-*` queues are processed. Once cleared, reset " "`notification_control_exchanges` option and launch the new notification " "agent(s). If `workload_partitioning` is not enabled, no special steps are " "required." msgstr "" "If workload partitioning of the notification agent is enabled, the " "notification agent should not run alongside pre-Queens agents. Doing so may " "result in missed samples when leveraging transformations. To upgrade without " "loss of data, set `notification_control_exchanges` option to empty so only " "existing `ceilometer-pipe-*` queues are processed. Once cleared, reset " "`notification_control_exchanges` option and launch the new notification " "agent(s). If `workload_partitioning` is not enabled, no special steps are " "required." msgid "" "If you are using Gnocchi as backend it's strongly recommended to switch " "[compute]/instance_discovery_method to libvirt_metadata. This will reduce " "the load on the Nova API especially if you have many compute nodes." msgstr "" "If you are using Gnocchi as backend it's strongly recommended to switch " "[compute]/instance_discovery_method to libvirt_metadata. This will reduce " "the load on the Nova API especially if you have many compute nodes." msgid "" "In Gnocchi, newly created metrics will set ``unit`` to the newer values. " "Existing metrics on existing resources, however, will not have their unit " "updated automatically. They will need to be changed manually, if required." msgstr "" "In Gnocchi, newly created metrics will set ``unit`` to the newer values. " "Existing metrics on existing resources, however, will not have their unit " "updated automatically. They will need to be changed manually, if required." msgid "" "In Prometheus, the ``unit`` label will change for the above metrics, causing " "Prometheus to treat them as separate metrics (though with otherwise " "identical labels) for non-aggregated queries. These separate metrics will co-" "exist until the old metrics expire, but the overlap between the old and new " "metrics should be small unless your query window is wide. If you perform any " "PromQL queries overlapping the changeover period that **must** have a single " "metric per resource, you could use aggregations like ``max without (unit) " "(...)`` to take into account this change." msgstr "" "In Prometheus, the ``unit`` label will change for the above metrics, causing " "Prometheus to treat them as separate metrics (though with otherwise " "identical labels) for non-aggregated queries. These separate metrics will co-" "exist until the old metrics expire, but the overlap between the old and new " "metrics should be small unless your query window is wide. If you perform any " "PromQL queries overlapping the changeover period that **must** have a single " "metric per resource, you could use aggregations like ``max without (unit) " "(...)`` to take into account this change." msgid "" "In an effort to minimise the noise, Ceilometer will no longer produce meters " "which have no measurable data associated with it. Image meter only captures " "state information which is already captured in events and other meters." msgstr "" "In an effort to minimise the noise, Ceilometer will no longer produce meters " "which have no measurable data associated with it. Image meter only captures " "state information which is already captured in events and other meters." msgid "" "In an effort to minimise the noise, Ceilometer will no longer produce meters " "which have no measureable data associated with it. Image meter only captures " "state information which is already captured in events and other meters." msgstr "" "In an effort to minimise the noise, Ceilometer will no longer produce meters " "which have no measurable data associated with it. Image meter only captures " "state information which is already captured in events and other meters." msgid "" "In order for the new image metadata attributes to start being populated from " "libvirt metadata in pollster samples, Nova must be upgraded to 2025.2 " "Flamingo or later (older versions are still backwards compatible, but the " "new attributes will not be available via pollster samples). Existing " "instances will need to be shelved-and-unshelved or cold migrated for the " "metadata to be populated." msgstr "" "In order for the new image metadata attributes to start being populated from " "libvirt metadata in pollster samples, Nova must be upgraded to 2025.2 " "Flamingo or later (older versions are still backwards compatible, but the " "new attributes will not be available via pollster samples). Existing " "instances will need to be shelved-and-unshelved or cold migrated for the " "metadata to be populated." msgid "" "In the 'publishers' section of a meter/event pipeline definition, https:// " "can now be used in addition to http://. Furthermore, either Basic or client-" "certificate authentication can be used (obviously, client cert only makes " "sense in the https case). For Basic authentication, use the form http://" "username:password@hostname/. For client certificate authentication pass the " "client certificate's path (and the key file path, if the key is not in the " "certificate file) using the parameters 'clientcert' and 'clientkey', e.g. " "https://hostname/path?clientcert=/path/to/cert&clientkey=/path/to/key. Any " "parameters or credentials used for http(s) publishers are removed from the " "URL before the actual HTTP request is made." msgstr "" "In the 'publishers' section of a meter/event pipeline definition, https:// " "can now be used in addition to http://. Furthermore, either Basic or client-" "certificate authentication can be used (obviously, client cert only makes " "sense in the https case). For Basic authentication, use the form http://" "username:password@hostname/. For client certificate authentication pass the " "client certificate's path (and the key file path, if the key is not in the " "certificate file) using the parameters 'clientcert' and 'clientkey', e.g. " "https://hostname/path?clientcert=/path/to/cert&clientkey=/path/to/key. Any " "parameters or credentials used for http(s) publishers are removed from the " "URL before the actual HTTP request is made." msgid "" "In the [dispatcher_http] section of ceilometer.conf, batch_mode can be set " "to True to activate sending meters and events in batches, or False (default " "value) to send each meter and event with a fresh HTTP call." msgstr "" "In the [dispatcher_http] section of ceilometer.conf, batch_mode can be set " "to True to activate sending meters and events in batches, or False (default " "value) to send each meter and event with a fresh HTTP call." msgid "" "In the [dispatcher_http] section of ceilometer.conf, verify_ssl can be set " "to True to use system-installed certificates (default value) or False to " "ignore certificate verification (use in development only!). verify_ssl can " "also be set to the location of a certificate file e.g. /some/path/cert.crt " "(use for self-signed certs) or to a directory of certificates. The value is " "passed as the 'verify' option to the underlying requests method, which is " "documented at http://docs.python-requests.org/en/master/user/advanced/#ssl-" "cert-verification" msgstr "" "In the [dispatcher_http] section of ceilometer.conf, verify_ssl can be set " "to True to use system-installed certificates (default value) or False to " "ignore certificate verification (use in development only!). verify_ssl can " "also be set to the location of a certificate file e.g. /some/path/cert.crt " "(use for self-signed certs) or to a directory of certificates. The value is " "passed as the 'verify' option to the underlying requests method, which is " "documented at http://docs.python-requests.org/en/master/user/advanced/#ssl-" "cert-verification" msgid "" "Include a publisher for the Monasca API. A ``monasca://`` pipeline sink will " "send data to a Monasca instance, using credentials configured in ceilometer." "conf. This functionality was previously available in the Ceilosca project " "(https://github.com/openstack/monasca-ceilometer)." msgstr "" "Include a publisher for the Monasca API. A ``monasca://`` pipeline sink will " "send data to a Monasca instance, using credentials configured in ceilometer." "conf. This functionality was previously available in the Ceilosca project " "(https://github.com/openstack/monasca-ceilometer)." msgid "" "Introduce ``threads_to_process_pollsters`` to enable operators to define the " "number of pollsters that can be executed in parallel inside a polling task." msgstr "" "Introduce ``threads_to_process_pollsters`` to enable operators to define the " "number of pollsters that can be executed in parallel inside a polling task." msgid "Kafka publisher is deprecated to use generic notifier instead." msgstr "Kafka publisher is deprecated to use generic notifier instead." msgid "Known Issues" msgstr "Known Issues" msgid "Liberty Series Release Notes" msgstr "Liberty Series Release Notes" msgid "Mitaka Release Notes" msgstr "Mitaka Release Notes" msgid "Network Statistics From OpenDaylight." msgstr "Network Statistics From OpenDaylight." msgid "" "Neutron API is not designed to be polled against. When polling against " "Neutron is enabled, Ceilometer's polling agents may generate a significant " "load against the Neutron API. It is recommended that a dedicated API be " "enabled for polling while Neutron's API is improved to handle polling." msgstr "" "Neutron API is not designed to be polled against. When polling against " "Neutron is enabled, Ceilometer's polling agents may generate a significant " "load against the Neutron API. It is recommended that a dedicated API be " "enabled for polling while Neutron's API is improved to handle polling." msgid "New Features" msgstr "New Features" msgid "" "New framework for ``ceilometer-status upgrade check`` command is added. This " "framework allows adding various checks which can be run before a Ceilometer " "upgrade to ensure if the upgrade can be performed safely." msgstr "" "New framework for ``ceilometer-status upgrade check`` command is added. This " "framework allows adding various checks which can be run before a Ceilometer " "upgrade to ensure if the upgrade can be performed safely." msgid "Newton Release Notes" msgstr "Newton Release Notes" msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "" "OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's " "data." msgstr "" "OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's " "data." msgid "" "Operator can now use new CLI tool ``ceilometer-status upgrade check`` to " "check if Ceilometer deployment can be safely upgraded from N-1 to N release." msgstr "" "Operator can now use new CLI tool ``ceilometer-status upgrade check`` to " "check if Ceilometer deployment can be safely upgraded from N-1 to N release." msgid "Other Notes" msgstr "Other Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "" "Pipeline Partitioning is also deprecated. This was only useful to workaround " "of some issues that tranformers has." msgstr "" "Pipeline Partitioning is also deprecated. This was only useful to workaround " "some issues that transformers had." msgid "" "Pipeline processing in polling agents was removed in Liberty cycle. A new " "polling specific definition file is created to handle polling functionality " "and pipeline definition file is now reserved exclusively for transformations " "and routing. The polling.yaml file follows the same syntax as the pipeline." "yaml but only handles polling attributes such as interval, discovery, " "resources, meter matching. It is configured by setting cfg_file under the " "polling section.If no polling definition file is found, it will fallback to " "reuse pipeline_cfg_file." msgstr "" "Pipeline processing in polling agents was removed in Liberty cycle. A new " "polling specific definition file is created to handle polling functionality " "and pipeline definition file is now reserved exclusively for transformations " "and routing. The polling.yaml file follows the same syntax as the pipeline." "yaml but only handles polling attributes such as interval, discovery, " "resources, meter matching. It is configured by setting cfg_file under the " "polling section.If no polling definition file is found, it will fallback to " "reuse pipeline_cfg_file." msgid "" "Pipeline.yaml files for agents should be updated to notifier:// or udp:// " "publishers. The rpc:// publisher is no longer supported." msgstr "" "Pipeline.yaml files for agents should be updated to notifier:// or udp:// " "publishers. The rpc:// publisher is no longer supported." msgid "Prelude" msgstr "Prelude" msgid "Previously deprecated kwapi meters are not removed." msgstr "Previously deprecated Kwapi meters are not removed." msgid "" "Previously, to enable/disable radosgw.* meters, you must define entry_point " "name rather than meter name. This is corrected so you do not need to be " "aware of entry_point naming. Use `radosgw.*` to enable/disable radosgw " "meters explicitly rather than `rgw.*`. `rgw.*` support is deprecated and " "will be removed in Rocky." msgstr "" "Previously, to enable/disable radosgw.* meters, you must define entry_point " "name rather than meter name. This is corrected so you do not need to be " "aware of entry_point naming. Use `radosgw.*` to enable/disable radosgw " "meters explicitly rather than `rgw.*`. `rgw.*` support is deprecated and " "will be removed in Rocky." msgid "" "Privsep daemons are now started by Ceilometer when required. These daemons " "can be started via rootwrap if required. rootwrap configs therefore need to " "be updated to include new privsep daemon invocations." msgstr "" "Privsep daemons are now started by Ceilometer when required. These daemons " "can be started via rootwrap if required. rootwrap configs therefore need to " "be updated to include new privsep daemon invocations." msgid "" "Privsep transitions. Ceilometer is transitioning from using the older style " "rootwrap privilege escalation path to the new style Oslo privsep path. This " "should improve performance and security of Ceilometer in the long term." msgstr "" "Privsep transitions. Ceilometer is transitioning from using the older style " "rootwrap privilege escalation path to the new style Oslo privsep path. This " "should improve performance and security of Ceilometer in the long term." msgid "" "Python 2.7 support has been dropped. Last release of ceilometer to support " "py2.7 is OpenStack Train. The minimum version of Python now supported by " "ceilometer is Python 3.6." msgstr "" "Python 2.7 support has been dropped. Last release of Ceilometer to support " "py2.7 is OpenStack Train. The minimum version of Python now supported by " "Ceilometer is Python 3.6." msgid "" "Python 3.6 & 3.7 support has been dropped. The minimum version of Python now " "supported is Python 3.8." msgstr "" "Python 3.6 & 3.7 support has been dropped. The minimum version of Python now " "supported is Python 3.8." msgid "" "Python 3.8 support was dropped. The minimum version of Python now supported " "is Python 3.9." msgstr "" "Python 3.8 support was dropped. The minimum version of Python now supported " "is Python 3.9." msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "" "RPC collector support is dropped. The queue-based notifier publisher and " "collector was added as the recommended alternative as of Icehouse cycle." msgstr "" "RPC collector support is dropped. The queue-based notifier publisher and " "collector was added as the recommended alternative as of Icehouse cycle." msgid "" "Regarding the values of the metrics themselves, please note that the " "**actual values have not changed**, only the reported unit names. There is " "no action needed unless you are converting the metrics to other units (or " "referencing the reported units in some way), in which case we would " "recommend double checking that the values are being handled correctly." msgstr "" "Regarding the values of the metrics themselves, please note that the " "**actual values have not changed**, only the reported unit names. There is " "no action needed unless you are converting the metrics to other units (or " "referencing the reported units in some way), in which case we would " "recommend double-checking that the values are being handled correctly." msgid "Remove deprecated option `batch_polled_samples`." msgstr "Remove deprecated option `batch_polled_samples`." msgid "" "Remove deprecated option meter_definitions_cfg_file, use " "meter_definitions_dirs to configure meter notification file." msgstr "" "Remove deprecated option meter_definitions_cfg_file, use " "meter_definitions_dirs to configure meter notification file." msgid "Remove direct publisher and use the explicit publisher instead." msgstr "Remove direct publisher and use the explicit publisher instead." msgid "Remove eventlet from Ceilometer in favour of threaded approach" msgstr "Remove eventlet from Ceilometer in favour of threaded approach" msgid "Remove integration with the inactive Monasca project" msgstr "Remove integration with the inactive Monasca project" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Run db-sync to add new indices." msgstr "Run db-sync to add new indices." msgid "" "Samples are required to measure some aspect of a resource. Samples not " "measuring anything will be dropped." msgstr "" "Samples are required to measure some aspect of a resource. Samples not " "measuring anything will be dropped." msgid "Security Issues" msgstr "Security Issues" msgid "" "Ship YAML files to ceilometer/pipeline/data/ make it convenient to update " "all the files on upgrade. Users can copy yaml files from /usr/share/" "ceilometer and customise their own files located in /etc/ceilometer/." msgstr "" "Ship YAML files to ceilometer/pipeline/data/ make it convenient to update " "all the files on upgrade. Users can copy yaml files from /usr/share/" "ceilometer and customise their own files located in /etc/ceilometer/." msgid "" "Since the Glance v1 APIs won't be maintained any more, this change add the " "support of glance v2 in images pollsters." msgstr "" "Since the Glance v1 APIs won't be maintained any more, this change add the " "support of glance v2 in images pollsters." msgid "Start using reno to manage release notes." msgstr "Start using Reno to manage release notes." msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "" "Support for CADF-only payload in HTTP dispatcher is dropped as audit " "middleware in pyCADF was dropped in Kilo cycle." msgstr "" "Support for CADF-only payload in HTTP dispatcher is dropped as audit " "middleware in pyCADF was dropped in Kilo cycle." msgid "" "Support for CORS is added. More information can be found [`here `_]" msgstr "" "Support for CORS is added. More information can be found [`here `_]" msgid "Support for Intel Node Manager was removed." msgstr "Support for Intel Node Manager was removed." msgid "" "Support for Neutron FWaaS has been officially deprecated. The feature has " "been useless since the Neutron FWaaS project was retired." msgstr "" "Support for Neutron FWaaS has been officially deprecated. The feature has " "been useless since the Neutron FWaaS project was retired." msgid "" "Support for Neutron FWaaS has been un-deprecated, because FWaaS project was " "restored and is now maintained." msgstr "" "Support for Neutron FWaaS has been un-deprecated, because the FWaaS project " "was restored and is now maintained." msgid "" "Support for Neutron LBaaS has been officially deprecated. The feature has " "been useless since the Neutron LBaaS project was retired." msgstr "" "Support for Neutron LBaaS has been officially deprecated. The feature has " "been useless since the Neutron LBaaS project was retired." msgid "" "Support for Open Contrail has been removed. Because no SDN is supported " "after the removal, the mechanism to pull metrics from SDN is also removed." msgstr "" "Support for Open Contrail has been removed. Because no SDN is supported " "after the removal, the mechanism to pull metrics from SDN is also removed." msgid "" "Support for OpenContrail, which is currently known as Tungsten Fabric, has " "been deprecated and will be removed in a future release." msgstr "" "Support for OpenContrail, which is currently known as Tungsten Fabric, has " "been deprecated and will be removed in a future release." msgid "" "Support for OpenDaylight has been deprecated and will be removed in a future " "release." msgstr "" "Support for OpenDaylight has been deprecated and will be removed in a future " "release." msgid "Support for OpenDaylight has been removed." msgstr "Support for OpenDaylight has been removed." msgid "" "Support for Python 3.9 has been removed. Now Python 3.10 is the minimum " "version supported." msgstr "" "Support for Python 3.9 has been removed. Now Python 3.10 is the minimum " "version supported." msgid "" "Support for VMWare vSphere has been deprecated, because the vmwareapi virt " "driver in nova has been marked experimental and may be removed in a future " "release." msgstr "" "Support for VMWare vSphere has been deprecated because the vmwareapi virt " "driver in Nova has been marked experimental and may be removed in a future " "release." msgid "Support for VMware vSphere has been removed." msgstr "Support for VMware vSphere has been removed." msgid "" "Support for XenServer/Xen Cloud Platform has been deprecated and will be " "removed in a future release." msgstr "" "Support for XenServer/Xen Cloud Platform has been deprecated and will be " "removed in a future release." msgid "Support for XenServer/Xen Cloud Platform has been removed." msgstr "Support for XenServer/Xen Cloud Platform has been removed." msgid "Support for neutron-lbaas resources has been removed." msgstr "Support for neutron-lbaas resources has been removed." msgid "" "Support for polling Neutron's LBaaS v2 API was added as v1 API in Neutron is " "deprecated. The same metrics are available between v1 and v2." msgstr "" "Support for polling Neutron's LBaaS v2 API was added as v1 API in Neutron is " "deprecated. The same metrics are available between v1 and v2." msgid "" "Support for pollster builder has been deprecated, because ceilometer no " "longer provides any built-in pollster builder now." msgstr "" "Support for pollster builder has been deprecated, because Ceilometer no " "longer provides any built-in pollster builder." msgid "" "Support for running Ceilometer in Windows operating systems has been " "deprecated because of retirement of the Winstackers project. Because of " "this, Hyper-V inspector is also deprecated." msgstr "" "Support for running Ceilometer in Windows operating systems has been " "deprecated because of retirement of the Winstackers project. Because of " "this, Hyper-V inspector is also deprecated." msgid "" "Support for running ceilometer in Windows operating systems has been " "removed. Because of the removal, Hyper-V inspector has also been removed." msgstr "" "Support for running Ceilometer in Windows operating systems has been " "removed. Because of the removal, Hyper-V inspector has also been removed." msgid "" "Support loading multiple meter definition files and allow users to add their " "own meter definitions into several files according to different types of " "metrics under the directory of /etc/ceilometer/meters.d." msgstr "" "Support loading multiple meter definition files and allow users to add their " "own meter definitions into several files according to different types of " "metrics under the directory of /etc/ceilometer/meters.d." msgid "" "Support resource caching in Gnocchi dispatcher to improve write performance " "to avoid additional queries." msgstr "" "Support resource caching in Gnocchi dispatcher to improve write performance " "to avoid additional queries." msgid "" "The Ceilometer compute agent can now retrieve some instance metadata from " "the metadata libvirt API instead of polling the Nova API. Since Mitaka, Nova " "fills this metadata with some information about the instance. To enable this " "feature you should set [compute]/instance_discovery_method = " "libvirt_metadata in the configuration file. The only downside of this method " "is that user_metadata (and some other instance attributes) are no longer " "part of the samples created by the agent. But when Gnocchi is used as " "backend, this is not an issue since Gnocchi doesn't store resource metadata " "aside of the measurements. And the missing informations are still retrieved " "through the Nova notifications and will fully update the resource " "information in Gnocchi." msgstr "" "The Ceilometer compute agent can now retrieve some instance metadata from " "the metadata libvirt API instead of polling the Nova API. Since Mitaka, Nova " "fills this metadata with some information about the instance. To enable this " "feature you should set [compute]/instance_discovery_method = " "libvirt_metadata in the configuration file. The only downside of this method " "is that user_metadata (and some other instance attributes) are no longer " "part of the samples created by the agent. But when Gnocchi is used as " "backend, this is not an issue since Gnocchi doesn't store resource metadata " "aside of the measurements. And the missing information is still retrieved " "through the Nova notifications and will fully update the resource " "information in Gnocchi." msgid "" "The Ceilometer event subsystem and pipeline is now deprecated and will be " "removed in a future release." msgstr "" "The Ceilometer event subsystem and pipeline is now deprecated and will be " "removed in a future release." msgid "" "The Events API (exposed at /v2/events) which was deprecated has been " "removed. The Panko project is now responsible for providing this API and can " "be installed separately." msgstr "" "The Events API (exposed at /v2/events) which was deprecated has been " "removed. The Panko project is now responsible for providing this API and can " "be installed separately." msgid "" "The Gnocchi dispatcher has been removed and replaced by a native Gnocchi " "publisher. The configuration options from the `[dispatcher_gnocchi]` has " "been removed and should be passed via the URL in `pipeline.yaml`. The " "service authentication override can be done by adding specific credentials " "to a `[gnocchi]` section instead." msgstr "" "The Gnocchi dispatcher has been removed and replaced by a native Gnocchi " "publisher. The configuration options from the `[dispatcher_gnocchi]` has " "been removed and should be passed via the URL in `pipeline.yaml`. The " "service authentication override can be done by adding specific credentials " "to a `[gnocchi]` section instead." msgid "" "The Kwapi pollsters are deprecated and will be removed in the next major " "version of Ceilometer." msgstr "" "The Kwapi pollsters are deprecated and will be removed in the next major " "version of Ceilometer." msgid "" "The [compute]/workload_partitioning = True is deprecated in favor of " "[compute]/instance_discovery_method = workload_partitioning" msgstr "" "The [compute]/workload_partitioning = True is deprecated in favour of " "[compute]/instance_discovery_method = workload_partitioning" msgid "" "The ``NodesDiscoveryTripleO`` discovery plugin has been deprecated and will " "be removed in a future release. This plugin is designed for TripleO " "deployment but no longer used since Telemetry services were removed from " "undercloud." msgstr "" "The ``NodesDiscoveryTripleO`` discovery plugin has been deprecated and will " "be removed in a future release. This plugin is designed for TripleO " "deployment but is no longer used since Telemetry services were removed from " "undercloud." msgid "The ``NodesDiscoveryTripleO`` discovery plugin has been removed." msgstr "The ``NodesDiscoveryTripleO`` discovery plugin has been removed." msgid "" "The ``[DEFAULT] http_timeout`` option has been deprecated because it is " "unused." msgstr "" "The ``[DEFAULT] http_timeout`` option has been deprecated because it is " "unused." msgid "" "The ``[DEFAULT] hypervisor_inspector`` option has been deprecated, because " "libvirt is the only supported hypervisor currently. The option will be " "removed in a future release." msgstr "" "The ``[DEFAULT] hypervisor_inspector`` option has been deprecated, because " "libvirt is the only supported hypervisor currently. The option will be " "removed in a future release." msgid "" "The ``[DEFAULT] virt_type`` option no longer supports ``uml``. UML support " "by nova was removed in nova 23.3.0 release." msgstr "" "The ``[DEFAULT] virt_type`` option no longer supports ``uml``. UML support " "by Nova was removed in Nova 23.3.0 release." msgid "The ``[DEFAULT] virt_type`` option now supports ``parallels``." msgstr "The ``[DEFAULT] virt_type`` option now supports ``parallels``." msgid "" "The ``[coordination] check_watchers`` parameter has been deprecated since it " "has been ineffective." msgstr "" "The ``[coordination] check_watchers`` parameter has been deprecated since it " "has been ineffective." msgid "The ``[coordination] check_watchers`` parameter has been removed." msgstr "The ``[coordination] check_watchers`` parameter has been removed." msgid "" "The ``[notification] batch_size`` parameter now takes effect to enable batch " "processing of notifications. The ``[notification] batch_timeout`` parameter " "has been restored at the same time to determine how much and how long " "notifications are kept." msgstr "" "The ``[notification] batch_size`` parameter now takes effect to enable batch " "processing of notifications. The ``[notification] batch_timeout`` parameter " "has been restored at the same time to determine how much and how long " "notifications are kept." msgid "" "The ``[polling] ignore_disabled_projects`` option has been added. This " "option allows polling agent to only parse enabled projects, to reduce " "procese time in case many projects are disabled." msgstr "" "The ``[polling] ignore_disabled_projects`` option has been added. This " "option allows the polling agent to only parse enabled projects, to reduce " "processing time in case many projects are disabled." msgid "" "The ``[polling] tenant_name_discovery`` option has been deprecated in favor " "of the new ``[polling] identity_name_discovery`` option." msgstr "" "The ``[polling] tenant_name_discovery`` option has been deprecated in favour " "of the new ``[polling] identity_name_discovery`` option." msgid "" "The ``container_format`` and ``disk_format`` attributes have been made " "optional for ``image`` resources in Gnocchi. This fixes an issue where image " "resources would fail to be created in Gnocchi because ``container_format`` " "and ``disk_format`` are set to ``null`` (which is possible on images that " "haven't had data uploaded to them yet)." msgstr "" "The ``container_format`` and ``disk_format`` attributes have been made " "optional for ``image`` resources in Gnocchi. This fixes an issue where image " "resources would fail to be created in Gnocchi because ``container_format`` " "and ``disk_format`` are set to ``null`` (which is possible on images that " "haven't had data uploaded to them yet)." msgid "" "The ``disk.ephemeral.size`` meter is now published as a compute pollster, in " "addition to the existing notification meter." msgstr "" "The ``disk.ephemeral.size`` meter is now published as a compute pollster, in " "addition to the existing notification meter." msgid "" "The ``disk.root.size`` meter is now published as a compute pollster, in " "addition to the existing notification meter." msgstr "" "The ``disk.root.size`` meter is now published as a compute pollster, in " "addition to the existing notification meter." msgid "" "The ``image_meta`` metadata structure for compute meters was formerly only " "available via the notification meters. When using Nova 2025.2 Flamingo or " "later, ``image_meta`` is now also supplied by the compute pollsters. This is " "now possible due to the addition of the relevant metadata to the libvirt " "guest XML." msgstr "" "The ``image_meta`` metadata structure for compute meters was formerly only " "available via the notification meters. When using Nova 2025.2 Flamingo or " "later, ``image_meta`` is now also supplied by the compute pollsters. This is " "now possible due to the addition of the relevant metadata to the libvirt " "guest XML." msgid "" "The ``instance`` resource type has been updated to make the ``host`` " "resource attribute optional. This allows the hypervisor a compute instance " "is running on to be withheld from Gnocchi's resource metadata, which may be " "required for security reasons e.g. for public clouds." msgstr "" "The ``instance`` resource type has been updated to make the ``host`` " "resource attribute optional. This allows the hypervisor on which a compute " "instance is running to be withheld from Gnocchi's resource metadata, which " "may be required for security reasons, e.g. for public clouds." msgid "" "The ``ip.floating`` and ``network.services.vpn`` pollsters now publish " "samples for all found floating IPs and VPNs, even if they are known to have " "an unknown state, when they would previously be dropped. The volume of " "samples for such floating IPs and VPNs will be set to ``-1``. This improves " "visibility of floating IPs and VPNs with unknown states, allowing them to be " "monitored via samples and the Gnocchi metrics, making it easier to discover " "such resources for troubleshooting. It also moves some of the \"business " "logic\" for downstream rating/billing services such as CloudKitty out of " "Ceilometer itself." msgstr "" "The ``ip.floating`` and ``network.services.vpn`` pollsters now publish " "samples for all found floating IPs and VPNs, even if they are known to have " "an unknown state, when they would previously be dropped. The volume of " "samples for such floating IPs and VPNs will be set to ``-1``. This improves " "visibility of floating IPs and VPNs with unknown states, allowing them to be " "monitored via samples and the Gnocchi metrics, making it easier to discover " "such resources for troubleshooting. It also moves some of the \"business " "logic\" for downstream rating/billing services, such as CloudKitty out of " "Ceilometer itself." msgid "" "The ``memory`` meter now has measures published by a compute pollster, in " "addition to the existing notification meter." msgstr "" "The ``memory`` meter now has measures published by a compute pollster, in " "addition to the existing notification meter." msgid "" "The ``network.services.firewall`` pollster now publishes samples for all " "found firewalls, even if they are known to have an unknown state, when they " "would previously be dropped. The volume of samples for such firewalls will " "be set to ``-1``. This improves visibility of firewalls with unknown states, " "allowing them to be monitored via samples and the Gnocchi/Prometheus " "metrics, making it easier to discover such resources for troubleshooting. It " "also moves some of the \"business logic\" for downstream rating/billing " "services such as CloudKitty out of Ceilometer itself." msgstr "" "The ``network.services.firewall`` pollster now publishes samples for all " "found firewalls, even if they are known to have an unknown state, when they " "would previously be dropped. The volume of samples for such firewalls will " "be set to ``-1``. This improves visibility of firewalls with unknown states, " "allowing them to be monitored via samples and the Gnocchi/Prometheus " "metrics, making it easier to discover such resources for troubleshooting. It " "also moves some of the \"business logic\" for downstream rating/billing " "services, such as CloudKitty out of Ceilometer itself." msgid "" "The ``network.services.vpn`` now publishes samples for VPNs with status " "``ERROR``, when they would previously be dropped. The sample volume for VPNs " "in ``ERROR`` state is ``7``." msgstr "" "The ``network.services.vpn`` now publishes samples for VPNs with status " "``ERROR``, when they would previously be dropped. The sample volume for VPNs " "in ``ERROR`` state is ``7``." msgid "" "The ``storage_policy`` resource metadata attribute has been added to the " "``swift.containers.objects`` and ``swift.containers.objects.size`` meters, " "populated from already performed Swift account ``GET`` requests. This " "functionality requires using a new version of Swift that adds the " "``storage_policy`` attribute when listing containers in an account. " "Ceilometer is backwards compatible with Swift versions that do not provide " "this functionality, but ``storage_policy`` will be set to ``None`` in " "samples and Gnocchi resources." msgstr "" "The ``storage_policy`` resource metadata attribute has been added to the " "``swift.containers.objects`` and ``swift.containers.objects.size`` meters, " "populated from already performed Swift account ``GET`` requests. This " "functionality requires using a new version of Swift that adds the " "``storage_policy`` attribute when listing containers in an account. " "Ceilometer is backwards compatible with Swift versions that do not provide " "this functionality, but ``storage_policy`` will be set to ``None`` in " "samples and Gnocchi resources." msgid "" "The ``vcpus`` meter now has measures published by a compute pollster, in " "addition to the existing notification meter." msgstr "" "The ``vcpus`` meter now has measures published by a compute pollster, in " "addition to the existing notification meter." msgid "The `image` meter is dropped in favour of `image.size` meter." msgstr "The `image` meter is dropped in favour of `image.size` meter." msgid "The `instance` meter no longer will be generated." msgstr "The `instance` meter no longer will be generated." msgid "" "The `instance` meter no longer will be generated. For equivalent " "functionality, perform the exact same query on any compute meter such as " "`cpu`, `disk.read.requests`, `memory.usage`, `network.incoming.bytes`, etc..." msgstr "" "The `instance` meter no longer will be generated. For equivalent " "functionality, perform the exact same query on any compute meter such as " "`cpu`, `disk.read.requests`, `memory.usage`, `network.incoming.bytes`, etc..." msgid "" "The `shuffle_time_before_polling_task` option has been removed. This option " "never worked in the way it was originally intended too." msgstr "" "The `shuffle_time_before_polling_task` option has been removed. This option " "never worked in the way it was originally intended to." msgid "" "The api-paste.ini file can be modified to include or exclude the CORs " "middleware. Additional configurations can be made to middleware as well." msgstr "" "The api-paste.ini file can be modified to include or exclude the CORs " "middleware. Additional configurations can be made to middleware as well." msgid "The api.pecan_debug option has been removed." msgstr "The api.pecan_debug option has been removed." msgid "" "The built-in ``meters.yaml`` has been updated to publish the ``image_meta`` " "metadata attribute for compute notification meter samples by default." msgstr "" "The built-in ``meters.yaml`` has been updated to publish the ``image_meta`` " "metadata attribute for compute notification meter samples by default." msgid "" "The cinder api microversion has been increased from Pike to Wallaby version " "(3.64) for volume/snapshot/backup related pollsters. These might not work " "until the cinder API has been upgraded up to this microversion." msgstr "" "The Cinder API microversion has been increased from Pike to Wallaby version " "(3.64) for volume/snapshot/backup related pollsters. These might not work " "until the Cinder API has been upgraded up to this microversion." msgid "" "The collector service is removed. From Ocata, it's possible to edit the " "pipeline.yaml and event_pipeline.yaml files and modify the publisher to " "provide the same functionality as collector dispatcher. You may change " "publisher to 'gnocchi', 'http', 'panko', or any combination of available " "publishers listed in documentation." msgstr "" "The collector service is removed. From Ocata, it's possible to edit the " "pipeline.yaml and event_pipeline.yaml files and modify the publisher to " "provide the same functionality as collector dispatcher. You may change " "publisher to 'gnocchi', 'http', 'panko', or any combination of available " "publishers listed in documentation." msgid "" "The default ``polling.yaml`` file has been updated and now it enables meters " "related to cinder by default." msgstr "" "The default ``polling.yaml`` file has been updated and now it enables meters " "related to cinder by default." msgid "" "The default event definiton has been updated and no longer includes events " "for sahara." msgstr "" "The default event definition has been updated and no longer includes events " "for Sahara." msgid "" "The default hashing for the resource attributes cache in the Gnocchi " "publisher has been changed from the builtin Python hash function to using " "the BLAKE2 algorithm, this will cause the cache for attributes for Gnocchi " "resources to be rebuild that can cause a temporary higher load on the " "Gnocchi API due to more resource update requests." msgstr "" "The default hashing for the resource attributes cache in the Gnocchi " "publisher has been changed from the built-in Python hash function to using " "the BLAKE2 algorithm. This will cause the cache for attributes for Gnocchi " "resources to be rebuild that can cause a temporary higher load on the " "Gnocchi API due to more resource update requests." msgid "The deprecated Ceilometer API has been removed." msgstr "The deprecated Ceilometer API has been removed." msgid "" "The deprecated ``[service_types] cinderv2`` option has been removed. Use the " "``[service_types] cinder`` option instead." msgstr "" "The deprecated ``[service_types] cinderv2`` option has been removed. Use the " "``[service_types] cinder`` option instead." msgid "" "The deprecated `compute.workload_partitioning` option has been removed in " "favor of `compute.instance_discovery_method`." msgstr "" "The deprecated `compute.workload_partitioning` option has been removed in " "favour of `compute.instance_discovery_method`." msgid "" "The deprecated `disk.*` meters have been removed. Use the `disk.device.*` " "meters instead." msgstr "" "The deprecated `disk.*` meters have been removed. Use the `disk.device.*` " "meters instead." msgid "The deprecated `dispatcher_gnocchi` option group has been removed." msgstr "The deprecated `dispatcher_gnocchi` option group has been removed." msgid "The deprecated `gnocchi_dispatcher` option group has been removed." msgstr "The deprecated `gnocchi_dispatcher` option group has been removed." msgid "The deprecated `meter_definitions_cfg_file` option has been removed." msgstr "The deprecated `meter_definitions_cfg_file` option has been removed." msgid "The deprecated `nova_http_log_debug` option has been removed." msgstr "The deprecated `nova_http_log_debug` option has been removed." msgid "The deprecated `pollster-list` option has been removed." msgstr "The deprecated `pollster-list` option has been removed." msgid "" "The deprecated ceilometer-dbsync has been removed. Use ceilometer-upgrade " "instead." msgstr "" "The deprecated ceilometer-dbsync has been removed. Use ceilometer-upgrade " "instead." msgid "The deprecated control exchange options have been removed." msgstr "The deprecated control exchange options have been removed." msgid "The deprecated file dispatcher has been removed." msgstr "The deprecated file dispatcher has been removed." msgid "The deprecated http dispatcher has been removed." msgstr "The deprecated http dispatcher has been removed." msgid "" "The deprecated kafka publisher has been removed, use NotifierPublisher " "instead." msgstr "" "The deprecated Kafka publisher has been removed, use NotifierPublisher " "instead." msgid "The deprecated meter for compute where removed:" msgstr "The deprecated meter for compute where removed:" msgid "" "The deprecated support of configure polling in the `pipeline.yaml` file has " "been removed. Ceilometer now only uses the `polling.yaml` file for polling " "configuration." msgstr "" "The deprecated support of configure polling in the `pipeline.yaml` file has " "been removed. Ceilometer now only uses the `polling.yaml` file for polling " "configuration." msgid "" "The deprecated workload partitioning for notification agent has been removed." msgstr "" "The deprecated workload partitioning for notification agent has been removed." msgid "" "The event database dispatcher is now deprecated. It has been moved to a new " "project, alongside the Ceilometer API for /v2/events, called Panko." msgstr "" "The event database dispatcher is now deprecated. It has been moved to a new " "project, alongside the Ceilometer API for /v2/events, called Panko." msgid "" "The following commands are no longer required to be listed in your rootwrap " "configuration: ipmitool." msgstr "" "The following commands are no longer required to be listed in your rootwrap " "configuration: ipmitool." msgid "" "The following meters were removed. Nova removed support for Intel CMT perf " "events in 22.0.0, and these meters can no longer be measured since then." msgstr "" "The following meters were removed. Nova removed support for Intel CMT perf " "events in 22.0.0, and these meters can no longer be measured since then." msgid "" "The hashing algorithm for the resource attributes cache in the Gnocchi " "publisher has been changed to BLAKE2 to provide a deterministic and low " "collision risk hash that is stable between processes, hosts and restarts." msgstr "" "The hashing algorithm for the resource attributes cache in the Gnocchi " "publisher has been changed to BLAKE2 to provide a deterministic and low " "collision risk hash that is stable between processes, hosts and restarts." msgid "" "The newly added ``[compute]/fetch_extra_metadata`` option is set to ``True`` " "by default, but to reduce the amount of load Ceilometer places on Nova this " "will be changed to ``False`` by default in a future release." msgstr "" "The newly added ``[compute]/fetch_extra_metadata`` option is set to ``True`` " "by default, but to reduce the amount of load Ceilometer places on Nova, this " "will be changed to ``False`` by default in a future release." msgid "" "The notification-agent can now be configured to either build meters or " "events. By default, the notification agent will continue to load both " "pipelines and build both data models. To selectively enable a pipeline, " "configure the `pipelines` option under the `[notification]` section." msgstr "" "The notification-agent can now be configured to either build meters or " "events. By default, the notification agent will continue to load both " "pipelines and build both data models. To selectively enable a pipeline, " "configure the `pipelines` option under the `[notification]` section." msgid "" "The notifier publisher options `metering_topic` and `event_topic` are " "deprecated and will be removed. Use the `topic` query parameter in the " "notifier publisher URL instead." msgstr "" "The notifier publisher options `metering_topic` and `event_topic` are " "deprecated and will be removed. Use the `topic` query parameter in the " "notifier publisher URL instead." msgid "" "The option 'glance_page_size' has been removed because it's not actually " "needed." msgstr "" "The option 'glance_page_size' has been removed because it's not actually " "needed." msgid "" "The option ``glance_page_size`` has been removed because it's not actually " "needed." msgstr "" "The option ``glance_page_size`` has been removed because it's not actually " "needed." msgid "" "The option batch_polled_samples in the [DEFAULT] section is deprecated. Use " "batch_size option in [polling] to configure and/or disable batching." msgstr "" "The option batch_polled_samples in the [DEFAULT] section is deprecated. Use " "batch_size option in [polling] to configure and/or disable batching." msgid "" "The options 'requeue_event_on_dispatcher_error' and " "'requeue_sample_on_dispatcher_error' have been enabled and removed." msgstr "" "The options 'requeue_event_on_dispatcher_error' and " "'requeue_sample_on_dispatcher_error' have been enabled and removed." msgid "" "The options ``requeue_event_on_dispatcher_error`` and " "``requeue_sample_on_dispatcher_error`` have been enabled and removed." msgstr "" "The options ``requeue_event_on_dispatcher_error`` and " "``requeue_sample_on_dispatcher_error`` have been enabled and removed." msgid "" "The pipeline dynamic refresh code has been removed. Ceilometer relies on the " "cotyledon library for a few releases which provides reload functionality by " "sending the SIGHUP signal to the process. This achieves the same feature " "while making sure the reload is explicit once the file is correctly and " "entirely written to the disk, avoiding the failing load of half-written " "files." msgstr "" "The pipeline dynamic refresh code has been removed. Ceilometer relies on the " "cotyledon library for a few releases which provides reload functionality by " "sending the SIGHUP signal to the process. This achieves the same feature " "while making sure the reload is explicit once the file is correctly and " "entirely written to the disk, avoiding the failing load of half-written " "files." msgid "" "The previous configuration options default for " "'requeue_sample_on_dispatcher_error' and 'requeue_event_on_dispatcher_error' " "allowed to lose data very easily: if the dispatcher failed to send data to " "the backend (e.g. Gnocchi is down), then the dispatcher raised and the data " "were lost forever. This was completely unacceptable, and nobody should be " "able to configure Ceilometer in that way.\"" msgstr "" "The previous configuration options default for " "'requeue_sample_on_dispatcher_error' and 'requeue_event_on_dispatcher_error' " "allowed to lose data very easily: if the dispatcher failed to send data to " "the backend (e.g. Gnocchi is down), then the dispatcher raised and the data " "were lost forever. This was completely unacceptable, and nobody should be " "able to configure Ceilometer in that way.\"" msgid "" "The previous configuration options default for " "``requeue_sample_on_dispatcher_error`` and " "``requeue_event_on_dispatcher_error`` allowed to lose data very easily: if " "the dispatcher failed to send data to the backend (e.g. Gnocchi is down), " "then the dispatcher raised and the data were lost forever. This was " "completely unacceptable, and nobody should be able to configure Ceilometer " "in that way.\"" msgstr "" "The previous configuration options default for " "``requeue_sample_on_dispatcher_error`` and " "``requeue_event_on_dispatcher_error`` allowed to lose data very easily: if " "the dispatcher failed to send data to the backend (e.g. Gnocchi is down), " "then the dispatcher raised and the data were lost forever. This was " "completely unacceptable, and nobody should be able to configure Ceilometer " "in that way.\"" msgid "" "The reported units for the following metrics were changed from ``MB`` and " "``GB`` to ``MiB`` and ``GiB`` respectively, as the metrics are actually in " "**mebibytes**/**gibibytes**:" msgstr "" "The reported units for the following metrics were changed from ``MB`` and " "``GB`` to ``MiB`` and ``GiB`` respectively, as the metrics are actually in " "**mebibytes**/**gibibytes**:" msgid "" "The resource metadata for the Cinder volume size poller now includes the " "availability zone field." msgstr "" "The resource metadata for the Cinder volume size poller now includes the " "availability zone field." msgid "The support for transformers has been removed from the pipeline." msgstr "The support for transformers has been removed from the pipeline." msgid "" "The tenant (project) discovery code in the polling agent now scans for " "tenants in all available domains." msgstr "" "The tenant (project) discovery code in the polling agent now scans for " "tenants in all available domains." msgid "" "The transport_url defined in [oslo_messaging_notifications] was never used, " "which contradicts the oslo_messaging documentation. This is now fixed." msgstr "" "The transport_url defined in [oslo_messaging_notifications] was never used, " "which contradicts the oslo_messaging documentation. This is now fixed." msgid "" "These metrics complement the existing notification-based ``manila.share." "size`` metric with polling support." msgstr "" "These metrics complement the existing notification-based ``manila.share." "size`` metric with polling support." msgid "" "To minimise load on Nova API, an additional configuration option was added " "to control discovery interval vs metric polling interval. If " "resource_update_interval option is configured in compute section, the " "compute agent will discover new instances based on defined interval. The " "agent will continue to poll the discovered instances at the interval defined " "by pipeline." msgstr "" "To minimise load on Nova API, an additional configuration option was added " "to control discovery interval vs metric polling interval. If " "resource_update_interval option is configured in compute section, the " "compute agent will discover new instances based on defined interval. The " "agent will continue to poll the discovered instances at the interval defined " "by pipeline." msgid "" "To publish the ``storage_policy`` attribute for Swift containers, " "``gnocchi_resources.yaml`` will need to be updated to the latest version. " "Swift in the target OpenStack cloud will also need upgrading to add support " "for providing the storage policy when listing containers." msgstr "" "To publish the ``storage_policy`` attribute for Swift containers, " "``gnocchi_resources.yaml`` will need to be updated to the latest version. " "Swift in the target OpenStack cloud will also need upgrading to add support " "for providing the storage policy when listing containers." msgid "" "To take advantage of this new feature you will need to update your " "gnocchi_resources.yaml file. See the example file for an example. You will " "need to ensure all required attributes of an instance are specified in the " "event_attributes." msgstr "" "To take advantage of this new feature you will need to update your " "gnocchi_resources.yaml file. See the example file for an example. You will " "need to ensure all required attributes of an instance are specified in the " "event_attributes." msgid "" "To utilize the new policy support. The policy.json file should be updated " "accordingly. The pre-existing policy.json file will continue to function as " "it does if policy changes are not required." msgstr "" "To utilize the new policy support. The policy.json file should be updated " "accordingly. The pre-existing policy.json file will continue to function as " "it does if policy changes are not required." msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Upgrade Notes" msgstr "Upgrade Notes" msgid "" "Usage of pipeline.yaml for polling configuration is now deprecated. The " "dedicated polling.yaml should be used instead." msgstr "" "Usage of pipeline.yaml for polling configuration is now deprecated. The " "dedicated polling.yaml should be used instead." msgid "" "Usage of transformers in Ceilometer pipelines is deprecated. Transformers in " "Ceilometer have never computed samples correctly when you have multiple " "workers. This functionality can be done by the storage backend easily " "without all issues that Ceilometer has. For example, the rating is already " "computed in Gnocchi today." msgstr "" "Usage of transformers in Ceilometer pipelines is deprecated. Transformers in " "Ceilometer have never computed samples correctly when you have multiple " "workers. This functionality can be done by the storage backend easily " "without all issues that Ceilometer has. For example, the rating is already " "computed in Gnocchi today." msgid "" "Use `radosgw.*` to enable/disable radosgw meters explicitly rather than `rgw." "*`" msgstr "" "Use `radosgw.*` to enable/disable radosgw meters explicitly rather than `rgw." "*`" msgid "Ussuri Series Release Notes" msgstr "Ussuri Series Release Notes" msgid "Victoria Series Release Notes" msgstr "Victoria Series Release Notes" msgid "Wallaby Series Release Notes" msgstr "Wallaby Series Release Notes" msgid "" "When ``[polling]/ignore_disabled_projects`` is enabled, project filtering is " "now performed server-side using API filters. This improves the performance " "of project discovery (used by the Swift object storage pollsters), " "particularly when Keystone has a lot of disabled projects (e.g. retained for " "record-keeping purposes, since deleting projects in Keystone removes them " "completely)." msgstr "" "When ``[polling]/ignore_disabled_projects`` is enabled, project filtering is " "now performed server-side using API filters. This improves the performance " "of project discovery (used by the Swift object storage pollsters), " "particularly when Keystone has a lot of disabled projects (e.g. retained for " "record-keeping purposes, since deleting projects in Keystone removes them " "completely)." msgid "" "When ``[polling]/ignore_disabled_projects`` is enabled, projects that are " "part of disabled domains are also ignored, regardless of whether or not the " "projects inside those domains are marked as disabled." msgstr "" "When ``[polling]/ignore_disabled_projects`` is enabled, projects that are " "part of disabled domains are also ignored, regardless of whether or not the " "projects inside those domains are marked as disabled." msgid "" "When using Nova 2025.2 and later, the extra specs for the flavor and " "instance is running is now available from the libvirt domain metadata. " "Ceilometer now adds the flavor's extra specs to compute sample metadata when " "found." msgstr "" "When using Nova 2025.2 and later, the extra specs for the flavour and " "instance running are now available from the libvirt domain metadata. " "Ceilometer now adds the flavour's extra specs to compute sample metadata " "when found." msgid "" "When using Nova 2025.2 and later, the flavor ID for an instance is now " "available from the libvirt domain metadata. Ceilometer now takes advantage " "of this and populates the flavor ID from metadata instead of querying Nova, " "when the value is available. If not available from metadata, Ceilometer will " "fallback to querying Nova API for the flavor ID." msgstr "" "When using Nova 2025.2 and later, the flavour ID for an instance is now " "available from the libvirt domain metadata. Ceilometer now takes advantage " "of this and populates the flavour ID from metadata instead of querying Nova " "when the value is available. If not available from metadata, Ceilometer will " "fallback to querying Nova API for the flavour ID." msgid "" "When using dynamic pollsters to query OpenStack APIs, if the endpoint URL " "returned by Keystone does not have a trailing slash and ``url_path`` is a " "relative path, the ``url_path`` configured in the dynamic pollster would " "replace sections of the endpoint URL instead of being appended to the end of " "the URL. This behaviour has now been changed so that ``url_path`` values " "that do not start with a ``/`` are always appended to the end of endpoint " "URLs. This change may require existing dynamic pollsters that rely on this " "behaviour to be changed, but this allows dynamic pollsters to be added for " "OpenStack services that append the active project ID to the API endpoint URL " "(e.g. Trove)." msgstr "" "When using dynamic pollsters to query OpenStack APIs, if the endpoint URL " "returned by Keystone does not have a trailing slash and ``url_path`` is a " "relative path, the ``url_path`` configured in the dynamic pollster would " "replace sections of the endpoint URL instead of being appended to the end of " "the URL. This behaviour has now been changed so that ``url_path`` values " "that do not start with a ``/`` are always appended to the end of endpoint " "URLs. This change may require existing dynamic pollsters that rely on this " "behaviour to be changed, but this allows dynamic pollsters to be added for " "OpenStack services that append the active project ID to the API endpoint URL " "(e.g. Trove)." msgid "" "With collector service being deprecated, we now have to address the " "duplication between dispatchers and publishers. The file dispatcher is now " "marked as deprecated. Use the file publisher to push samples into a file." msgstr "" "With collector service being deprecated, we now have to address the " "duplication between dispatchers and publishers. The file dispatcher is now " "marked as deprecated. Use the file publisher to push samples into a file." msgid "" "Workload partitioning of notification agent is now split into queues based " "on pipeline type (sample, event, etc...) rather than per individual " "pipeline. This will save some memory usage specifically for pipeline " "definitions with many source/sink combinations." msgstr "" "Workload partitioning of notification agent is now split into queues based " "on pipeline type (sample, event, etc...) rather than per individual " "pipeline. This will save some memory usage specifically for pipeline " "definitions with many source/sink combinations." msgid "Xena Series Release Notes" msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" msgid "Zed Series Release Notes" msgstr "Zed Series Release Notes" msgid "" "[`bug 1254800 `_] Add " "better support to catch race conditions when creating event_types" msgstr "" "[`bug 1254800 `_] Add " "better support to catch race conditions when creating event_types" msgid "" "[`bug 1388680 `_] " "Suppose ability to query for None value when using SQL backend." msgstr "" "[`bug 1388680 `_] " "Suppose ability to query for None value when using SQL backend." msgid "" "[`bug 1480333 `_] " "Support ability to configure collector to capture events or meters mutually " "exclusively, rather than capturing both always." msgstr "" "[`bug 1480333 `_] " "Support ability to configure collector to capture events or meters mutually " "exclusively, rather than capturing both always." msgid "" "[`bug 1491509 `_] Patch " "to unify timestamp in samples polled by pollsters. Set the time point " "polling starts as timestamp of samples, and drop timetamping in pollsters." msgstr "" "[`bug 1491509 `_] Patch " "to unify timestamp in samples polled by pollsters. Set the time point " "polling starts as timestamp of samples, and drop timestamping in pollsters." msgid "" "[`bug 1504495 `_] " "Configure ceilometer to handle policy.json rules when possible." msgstr "" "[`bug 1504495 `_] " "Configure Ceilometer to handle policy.json rules when possible." msgid "" "[`bug 1506738 `_] [`bug " "1509677 `_] Optimise SQL " "backend queries to minimise query load" msgstr "" "[`bug 1506738 `_] [`bug " "1509677 `_] Optimise SQL " "backend queries to minimise query load" msgid "" "[`bug 1506959 `_] Add " "support to query unique set of meter names rather than meters associated " "with each resource. The list is available by adding unique=True option to " "request." msgstr "" "[`bug 1506959 `_] Add " "support to query unique set of meter names rather than meters associated " "with each resource. The list is available by adding unique=True option to " "request." msgid "" "[`bug 1513731 `_] Add " "support for hardware cpu_util in snmp.yaml" msgstr "" "[`bug 1513731 `_] Add " "support for hardware cpu_util in snmp.yaml" msgid "" "[`bug 1518338 `_] Add " "support for storing SNMP metrics in Gnocchi.This functionality requires " "Gnocchi v2.1.0 to be installed." msgstr "" "[`bug 1518338 `_] Add " "support for storing SNMP metrics in Gnocchi.This functionality requires " "Gnocchi v2.1.0 to be installed." msgid "" "[`bug 1519767 `_] " "fnmatch functionality in python <= 2.7.9 is not threadsafe. this issue and " "its potential race conditions are now patched." msgstr "" "[`bug 1519767 `_] " "fnmatch functionality in python <= 2.7.9 is not thread-safe. this issue and " "its potential race conditions are now patched." msgid "" "[`bug 1523124 `_] Fix " "gnocchi dispatcher to support UDP collector" msgstr "" "[`bug 1523124 `_] Fix " "Gnocchi dispatcher to support UDP collector" msgid "" "[`bug 1526793 `_] " "Additional indices were added to better support querying of event data." msgstr "" "[`bug 1526793 `_] " "Additional indices were added to better support querying of event data." msgid "" "[`bug 1530793 `_] " "network.services.lb.incoming.bytes meter was previous set to incorrect type. " "It should be a gauge meter." msgstr "" "[`bug 1530793 `_] " "network.services.lb.incoming.bytes meter was previous set to incorrect type. " "It should be a gauge meter." msgid "" "[`bug 1531626 `_] Ensure " "aggregator transformer timeout is honoured if size is not provided." msgstr "" "[`bug 1531626 `_] Ensure " "aggregator transformer timeout is honoured if size is not provided." msgid "" "[`bug 1532661 `_] Fix " "statistics query failures due to large numbers stored in MongoDB. Data from " "MongoDB is returned as Int64 for big numbers when int and float types are " "expected. The data is cast to appropriate type to handle large data." msgstr "" "[`bug 1532661 `_] Fix " "statistics query failures due to large numbers stored in MongoDB. Data from " "MongoDB is returned as Int64 for big numbers when int and float types are " "expected. The data is cast to appropriate type to handle large data." msgid "" "[`bug 1533787 `_] Fix an " "issue where agents are not properly getting registered to group when " "multiple notification agents are deployed. This can result in bad " "transformation as the agents are not coordinated. It is still recommended to " "set heartbeat_timeout_threshold = 0 in [oslo_messaging_rabbit] section when " "deploying multiple agents." msgstr "" "[`bug 1533787 `_] Fix an " "issue where agents are not properly getting registered to group when " "multiple notification agents are deployed. This can result in bad " "transformation as the agents are not coordinated. It is still recommended to " "set heartbeat_timeout_threshold = 0 in [oslo_messaging_rabbit] section when " "deploying multiple agents." msgid "" "[`bug 1536338 `_] Patch " "was added to fix the broken floatingip pollster that polled data from nova " "api, but since the nova api filtered the data by tenant, ceilometer was not " "getting any data back. The fix changes the pollster to use the neutron api " "instead to get the floating ip info." msgstr "" "[`bug 1536338 `_] Patch " "was added to fix the broken floatingip pollster that polled data from Nova " "API, but since the Nova API filtered the data by tenant, Ceilometer was not " "getting any data back. The fix changes the pollster to use the Neutron API " "instead to get the floating IP info." msgid "" "[`bug 1536498 `_] Patch " "to fix duplicate meter definitions causing duplicate samples. If a duplicate " "is found, log a warning and skip the meter definition. Note that the first " "occurance of a meter will be used and any following duplicates will be " "skipped from processing." msgstr "" "[`bug 1536498 `_] Patch " "to fix duplicate meter definitions causing duplicate samples. If a duplicate " "is found, log a warning and skip the meter definition. Note that the first " "occurrence of a meter will be used and any following duplicates will be " "skipped from processing." msgid "" "[`bug 1536498 `_] Patch " "to fix duplicate meter definitions causing duplicate samples. If a duplicate " "is found, log a warning and skip the meter definition. Note that the first " "occurrence of a meter will be used and any following duplicates will be " "skipped from processing." msgstr "" "[`bug 1536498 `_] Patch " "to fix duplicate meter definitions causing duplicate samples. If a duplicate " "is found, log a warning and skip the meter definition. Note that the first " "occurrence of a meter will be used and any following duplicates will be " "skipped from processing." msgid "" "[`bug 1536699 `_] Patch " "to fix volume field lookup in meter definition file. In case the field is " "missing in the definition, it raises a keyerror and aborts. Instead we " "should skip the missing field meter and continue with the rest of the " "definitions." msgstr "" "[`bug 1536699 `_] Patch " "to fix volume field lookup in meter definition file. In case the field is " "missing in the definition, it raises a key error and aborts. Instead we " "should skip the missing field meter and continue with the rest of the " "definitions." msgid "" "[`bug 1539163 `_] Add " "ability to define whether to use first or last timestamps when aggregating " "samples. This will allow more flexibility when chaining transformers." msgstr "" "[`bug 1539163 `_] Add " "ability to define whether to use first or last timestamps when aggregating " "samples. This will allow more flexibility when chaining transformers." msgid "" "[`bug 1542189 `_] Handle " "malformed resource definitions in gnocchi_resources.yaml gracefully. " "Currently we raise an exception once we hit a bad resource and skip the " "rest. Instead the patch skips the bad resource and proceeds with rest of the " "definitions." msgstr "" "[`bug 1542189 `_] Handle " "malformed resource definitions in gnocchi_resources.yaml gracefully. " "Currently we raise an exception once we hit a bad resource and skip the " "rest. Instead the patch skips the bad resource and proceeds with rest of the " "definitions." msgid "" "[`bug 1550436 `_] Cache " "json parsers when building parsing logic to handle event and meter " "definitions. This will improve agent startup and setup time." msgstr "" "[`bug 1550436 `_] Cache " "json parsers when building parsing logic to handle event and meter " "definitions. This will improve agent startup and setup time." msgid "" "[`bug 1578128 `_] Add a " "tool that allow users to drop the legacy alarm and alarm_history tables." msgstr "" "[`bug 1578128 `_] Add a " "tool that allow users to drop the legacy alarm and alarm_history tables." msgid "" "[`bug 1597618 `_] Add " "the full support of snmp v3 user security model." msgstr "" "[`bug 1597618 `_] Add " "the full support of SNMP v3 user security model." msgid "" "[`bug 1848286 `_] " "Enable load balancer metrics by adding the loadbalancer resource type, " "allowing Gnocchi to capture measurement data for Octavia load balancers." msgstr "" "[`bug 1848286 `_] " "Enable load balancer metrics by adding the loadbalancer resource type, " "allowing Gnocchi to capture measurement data for Octavia load balancers." msgid "" "[`bug 1940660 `_] Fixes " "an issue with the Swift pollster where the ``[service_credentials] cafile`` " "option was not used. This could prevent communication with TLS-enabled Swift " "APIs." msgstr "" "[`bug 1940660 `_] Fixes " "an issue with the Swift pollster where the ``[service_credentials] cafile`` " "option was not used. This could prevent communication with TLS-enabled Swift " "APIs." msgid "" "[`bug 2007108 `_] The " "retired metrics dependent on SNMP have been removed from the default " "``polling.yaml``." msgstr "" "[`bug 2007108 `_] The " "retired metrics dependent on SNMP have been removed from the default " "``polling.yaml``." msgid "" "[`bug 2113903 `_] Fix " "volume provider pool capacity metrics for ceph backend." msgstr "" "[`bug 2113903 `_] Fix " "volume provider pool capacity metrics for Ceph backend." msgid "" "[`bug 255569 `_] Fix " "caching support in Gnocchi dispatcher. Added better locking support to " "enable smoother cache access." msgstr "" "[`bug 255569 `_] Fix " "caching support in Gnocchi dispatcher. Added better locking support to " "enable smoother cache access." msgid "" "``GenericHardwareDeclarativePollster`` has been deprecated and will be " "removed in a future release. This pollster was designed to be used in " "TripleO deployment to gather hardware metrics from overcloud nodes but " "Telemetry services are no longer deployed in undercloud in current TripleO." msgstr "" "``GenericHardwareDeclarativePollster`` has been deprecated and will be " "removed in a future release. This pollster was designed to be used in " "TripleO deployment to gather hardware metrics from overcloud nodes but " "Telemetry services are no longer deployed in undercloud in the current " "TripleO." msgid "" "``GenericHardwareDeclarativePollster`` has been removed. Because of this " "removal all metrics gathered by SNMP daemon have been removed as well." msgstr "" "``GenericHardwareDeclarativePollster`` has been removed. Because of this " "removal, all metrics gathered by the SNMP daemon have also been removed." msgid "``[DEFAULT] metering_secret``" msgstr "``[DEFAULT] metering_secret``" msgid "" "``[compute]/fetch_extra_metadata`` was previously set to ``True`` by " "default. To reduce load on Nova API, as of the 2026.2 Hibiscus release this " "option is now set to ``False`` by default." msgstr "" "``[compute]/fetch_extra_metadata`` was previously set to ``True`` by " "default. To reduce load on Nova API, as of the 2026.2 Hibiscus release this " "option is now set to ``False`` by default." msgid "``[publisher] metering_secret``" msgstr "``[publisher] metering_secret``" msgid "``[publisher_rpc] metering_secret``" msgstr "``[publisher_rpc] metering_secret``" msgid "``cpu_l3_cache_usage``" msgstr "``cpu_l3_cache_usage``" msgid "``disk.ephemeral.size``" msgstr "``disk.ephemeral.size``" msgid "``disk.root.size``" msgstr "``disk.root.size``" msgid "" "``gnocchi_resources.yaml`` has been updated with changes to the ``volume`` " "resource type. If you override this file in your deployment, it needs to be " "updated." msgstr "" "``gnocchi_resources.yaml`` has been updated with changes to the ``volume`` " "resource type. If you override this file in your deployment, it needs to be " "updated." msgid "" "``loadbalancer.operating``: Reports the operating status of load balancers " "(ONLINE=1, DRAINING=2, OFFLINE=3, DEGRADED=4, ERROR=5, NO_MONITOR=6, " "unknown=-1)." msgstr "" "``loadbalancer.operating``: Reports the operating status of load balancers " "(ONLINE=1, DRAINING=2, OFFLINE=3, DEGRADED=4, ERROR=5, NO_MONITOR=6, " "unknown=-1)." msgid "" "``loadbalancer.provisioning``: Reports the provisioning status of load " "balancers (ACTIVE=1, DELETED=2, ERROR=3, PENDING_CREATE=4, PENDING_UPDATE=5, " "PENDING_DELETE=6, unknown=-1)." msgstr "" "``loadbalancer.provisioning``: Reports the provisioning status of load " "balancers (ACTIVE=1, DELETED=2, ERROR=3, PENDING_CREATE=4, PENDING_UPDATE=5, " "PENDING_DELETE=6, unknown=-1)." msgid "``manila.share.size``" msgstr "``manila.share.size``" msgid "``manila.share.size`` - Size of the share in GiB (polling-based)" msgstr "``manila.share.size`` - Size of the share in GiB (polling-based)" msgid "``manila.share.status`` - Numeric status of the share" msgstr "``manila.share.status`` - Numeric status of the share" msgid "``memory_bandwidth_local``" msgstr "``memory_bandwidth_local``" msgid "``memory_bandwidth_total``" msgstr "``memory_bandwidth_total``" msgid "``memory``/``memory.*``" msgstr "``memory``/``memory.*``" msgid "" "``meters.yaml`` has been updated to add ``image_meta`` to compute meter " "samples by default." msgstr "" "``meters.yaml`` has been updated to add ``image_meta`` to compute meter " "samples by default." msgid "" "``meters.yaml`` has been updated with changes to the ``volume.size`` " "notification meter. If you override this file in your deployment, it needs " "to be updated." msgstr "" "``meters.yaml`` has been updated with changes to the ``volume.size`` " "notification meter. If you override this file in your deployment, it needs " "to be updated." msgid "``volume.backup.size``/``backup.size``" msgstr "``volume.backup.size``/``backup.size``" msgid "``volume.provider.capacity.*``/``volume.provider.pool.capacity.*``" msgstr "``volume.provider.capacity.*``/``volume.provider.pool.capacity.*``" msgid "``volume.size``" msgstr "``volume.size``" msgid "``volume.snapshot.size``/``snapshot.size``" msgstr "``volume.snapshot.size``/``snapshot.size``" msgid "" "`ceilometer-upgrade` must be run to build IPMI sensor resource in Gnocchi." msgstr "" "`ceilometer-upgrade` must be run to build IPMI sensor resource in Gnocchi." msgid "" "`launched_at`/`created_at`/`deleted_at` of Nova instances are now tracked." msgstr "" "`launched_at`/`created_at`/`deleted_at` of Nova instances are now tracked." msgid "`volume.provider.pool.capacity.allocated`" msgstr "`volume.provider.pool.capacity.allocated`" msgid "`volume.provider.pool.capacity.free`" msgstr "`volume.provider.pool.capacity.free`" msgid "`volume.provider.pool.capacity.provisioned`" msgstr "`volume.provider.pool.capacity.provisioned`" msgid "`volume.provider.pool.capacity.total`" msgstr "`volume.provider.pool.capacity.total`" msgid "`volume.provider.pool.capacity.virtual_free`" msgstr "`volume.provider.pool.capacity.virtual_free`" msgid "" "audit middleware in keystonemiddleware library should be used for similar " "support." msgstr "" "audit middleware in keystonemiddleware library should be used for similar " "support." msgid "" "batch_size and batch_timeout configuration options are added to both " "[notification] and [collector] sections of configuration. The batch_size " "controls the number of messages to grab before processing. Similarly, the " "batch_timeout defines the wait time before processing." msgstr "" "batch_size and batch_timeout configuration options are added to both " "[notification] and [collector] sections of configuration. The batch_size " "controls the number of messages to grab before processing. Similarly, the " "batch_timeout defines the wait time before processing." msgid "" "batch_size option added to [polling] section of configuration. Use " "batch_size=0 to disable batching of samples." msgstr "" "batch_size option added to [polling] section of configuration. Use " "batch_size=0 to disable batching of samples." msgid "" "cpu_util and \\*.rate meters are deprecated and will be removed in future " "release in favor of the Gnocchi rate calculation equivalent." msgstr "" "cpu_util and \\*.rate meters are deprecated and will be removed in future " "release in favour of the Gnocchi rate calculation equivalent." msgid "" "disk.* aggregated metrics for instance are deprecated, in favor of the per " "disk metrics (disk.device.*). Now, it's up to the backend to provide such " "aggregation feature. Gnocchi already provides this." msgstr "" "disk.* aggregated metrics for instance are deprecated, in favour of the per " "disk metrics (disk.device.*). Now, it's up to the backend to provide such an " "aggregation feature. Gnocchi already provides this." msgid "disk.device.read.bytes.rate" msgstr "disk.device.read.bytes.rate" msgid "disk.device.read.requests.rate" msgstr "disk.device.read.requests.rate" msgid "disk.device.write.bytes.rate" msgstr "disk.device.write.bytes.rate" msgid "disk.device.write.requests.rate" msgstr "disk.device.write.requests.rate" msgid "disk.read.bytes.rate" msgstr "disk.read.bytes.rate" msgid "disk.read.requests.rate" msgstr "disk.read.requests.rate" msgid "disk.write.bytes.rate" msgstr "disk.write.bytes.rate" msgid "disk.write.requests.rate" msgstr "disk.write.requests.rate" msgid "gnocchi_resources.yaml in Ceilometer should be updated." msgstr "gnocchi_resources.yaml in Ceilometer should be updated." msgid "gnocchiclient library is now a requirement if using ceilometer+gnocchi." msgstr "" "gnocchiclient library is now a requirement if using ceilometer+gnocchi." msgid "" "metrics hardware.cpu.util and hardware.system_stats.cpu.idle are now " "deprecated. Other hardware.cpu.* metrics should be used instead." msgstr "" "metrics hardware.cpu.util and hardware.system_stats.cpu.idle are now " "deprecated. Other hardware.cpu.* metrics should be used instead." msgid "" "new metrics are available for snmp polling hardware.cpu.user, hardware.cpu." "nice, hardware.cpu.system, hardware.cpu.idle, hardware.cpu.wait, hardware." "cpu.kernel, hardware.cpu.interrupt. They replace deprecated hardware.cpu." "util and hardware.system_stats.cpu.idle." msgstr "" "new metrics are available for snmp polling hardware.cpu.user, hardware.cpu." "nice, hardware.cpu.system, hardware.cpu.idle, hardware.cpu.wait, hardware." "cpu.kernel, hardware.cpu.interrupt. They replace deprecated hardware.cpu." "util and hardware.system_stats.cpu.idle." msgid "use memory usable metric from libvirt memoryStats if available." msgstr "use memory usable metric from libvirt memoryStats if available." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.2231276 ceilometer-27.0.0.0rc1/releasenotes/source/locale/fr/0000775000175000017500000000000015250267567021174 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3491275 ceilometer-27.0.0.0rc1/releasenotes/source/locale/fr/LC_MESSAGES/0000775000175000017500000000000015250267567022761 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po0000664000175000017500000000272215250267452026006 0ustar00zuulzuul# Gérald LONLAS , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: Ceilometer Release Notes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-03-28 06:27+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-10-22 05:24+0000\n" "Last-Translator: Gérald LONLAS \n" "Language-Team: French\n" "Language: fr\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" msgid "5.0.1" msgstr "5.0.1" msgid "5.0.2" msgstr "5.0.2" msgid "5.0.3" msgstr "5.0.3" msgid "6.0.0" msgstr "6.0.0" msgid "7.0.0" msgstr "7.0.0" msgid "7.0.0.0b2" msgstr "7.0.0.0b2" msgid "7.0.0.0b3" msgstr "7.0.0.0b3" msgid "7.0.0.0rc1" msgstr "7.0.0.0rc1" msgid "Bug Fixes" msgstr "Corrections de bugs" msgid "Ceilometer Release Notes" msgstr "Note de release de Ceilometer" msgid "Critical Issues" msgstr "Erreurs critiques" msgid "Current Series Release Notes" msgstr "Note de la release actuelle" msgid "Deprecation Notes" msgstr "Notes dépréciées " msgid "Known Issues" msgstr "Problèmes connus" msgid "Liberty Series Release Notes" msgstr "Note de release pour Liberty" msgid "New Features" msgstr "Nouvelles fonctionnalités" msgid "Other Notes" msgstr "Autres notes" msgid "Start using reno to manage release notes." msgstr "Commence à utiliser reno pour la gestion des notes de release" msgid "Upgrade Notes" msgstr "Notes de mises à jours" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/mitaka.rst0000664000175000017500000003332215250267452021322 0ustar00zuulzuul==================== Mitaka Release Notes ==================== 6.0.0 ===== New Features ------------ .. releasenotes/notes/batch-messaging-d126cc525879d58e.yaml @ c5895d2c6efc6676679e6973c06b85c0c3a10585 - Add support for batch processing of messages from queue. This will allow the collector and notification agent to grab multiple messages per thread to enable more efficient processing. .. releasenotes/notes/compute-discovery-interval-d19f7c9036a8c186.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - To minimise load on Nova API, an additional configuration option was added to control discovery interval vs metric polling interval. If resource_update_interval option is configured in compute section, the compute agent will discover new instances based on defined interval. The agent will continue to poll the discovered instances at the interval defined by pipeline. .. releasenotes/notes/configurable-data-collector-e247aadbffb85243.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - [`bug 1480333 `_] Support ability to configure collector to capture events or meters mutually exclusively, rather than capturing both always. .. releasenotes/notes/cors-support-70c33ba1f6825a7b.yaml @ c5895d2c6efc6676679e6973c06b85c0c3a10585 - Support for CORS is added. More information can be found [`here `_] .. releasenotes/notes/gnocchi-cache-1d8025dfc954f281.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - Support resource caching in Gnocchi dispatcher to improve write performance to avoid additional queries. .. releasenotes/notes/gnocchi-client-42cd992075ee53ab.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Gnocchi dispatcher now uses client rather than direct http requests .. releasenotes/notes/gnocchi-host-metrics-829bcb965d8f2533.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1518338 `_] Add support for storing SNMP metrics in Gnocchi.This functionality requires Gnocchi v2.1.0 to be installed. .. releasenotes/notes/keystone-v3-fab1e257c5672965.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Add support for Keystone v3 authentication .. releasenotes/notes/remove-alarms-4df3cdb4f1fb5faa.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - Ceilometer alarms code is now fully removed from code base. Equivalent functionality is handled by Aodh. .. releasenotes/notes/remove-cadf-http-f8449ced3d2a29d4.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Support for CADF-only payload in HTTP dispatcher is dropped as audit middleware in pyCADF was dropped in Kilo cycle. .. releasenotes/notes/remove-eventlet-6738321434b60c78.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - Remove eventlet from Ceilometer in favour of threaded approach .. releasenotes/notes/remove-rpc-collector-d0d0a354140fd107.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - RPC collector support is dropped. The queue-based notifier publisher and collector was added as the recommended alternative as of Icehouse cycle. .. releasenotes/notes/support-lbaasv2-polling-c830dd49bcf25f64.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - Support for polling Neutron's LBaaS v2 API was added as v1 API in Neutron is deprecated. The same metrics are available between v1 and v2. .. releasenotes/notes/support-snmp-cpu-util-5c1c7afb713c1acd.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - [`bug 1513731 `_] Add support for hardware cpu_util in snmp.yaml .. releasenotes/notes/support-unique-meter-query-221c6e0c1dc1b726.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1506959 `_] Add support to query unique set of meter names rather than meters associated with each resource. The list is available by adding unique=True option to request. Known Issues ------------ .. releasenotes/notes/support-lbaasv2-polling-c830dd49bcf25f64.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - Neutron API is not designed to be polled against. When polling against Neutron is enabled, Ceilometer's polling agents may generate a significant load against the Neutron API. It is recommended that a dedicated API be enabled for polling while Neutron's API is improved to handle polling. Upgrade Notes ------------- .. releasenotes/notes/always-requeue-7a2df9243987ab67.yaml @ 244439979fd28ecb0c76d132f0be784c988b54c8 - The options 'requeue_event_on_dispatcher_error' and 'requeue_sample_on_dispatcher_error' have been enabled and removed. .. releasenotes/notes/batch-messaging-d126cc525879d58e.yaml @ c5895d2c6efc6676679e6973c06b85c0c3a10585 - batch_size and batch_timeout configuration options are added to both [notification] and [collector] sections of configuration. The batch_size controls the number of messages to grab before processing. Similarly, the batch_timeout defines the wait time before processing. .. releasenotes/notes/cors-support-70c33ba1f6825a7b.yaml @ c5895d2c6efc6676679e6973c06b85c0c3a10585 - The api-paste.ini file can be modified to include or exclude the CORs middleware. Additional configurations can be made to middleware as well. .. releasenotes/notes/gnocchi-client-42cd992075ee53ab.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - gnocchiclient library is now a requirement if using ceilometer+gnocchi. .. releasenotes/notes/gnocchi-orchestration-3497c689268df0d1.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - gnocchi_resources.yaml in Ceilometer should be updated. .. releasenotes/notes/improve-events-rbac-support-f216bd7f34b02032.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - To utilize the new policy support. The policy.json file should be updated accordingly. The pre-existing policy.json file will continue to function as it does if policy changes are not required. .. releasenotes/notes/index-events-mongodb-63cb04200b03a093.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Run db-sync to add new indices. .. releasenotes/notes/remove-cadf-http-f8449ced3d2a29d4.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - audit middleware in keystonemiddleware library should be used for similar support. .. releasenotes/notes/remove-rpc-collector-d0d0a354140fd107.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Pipeline.yaml files for agents should be updated to notifier:// or udp:// publishers. The rpc:// publisher is no longer supported. .. releasenotes/notes/support-lbaasv2-polling-c830dd49bcf25f64.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - By default, Ceilometer will poll the v2 API. To poll legacy v1 API, add neutron_lbaas_version=v1 option to configuration file. Critical Issues --------------- .. releasenotes/notes/always-requeue-7a2df9243987ab67.yaml @ 244439979fd28ecb0c76d132f0be784c988b54c8 - The previous configuration options default for 'requeue_sample_on_dispatcher_error' and 'requeue_event_on_dispatcher_error' allowed to lose data very easily: if the dispatcher failed to send data to the backend (e.g. Gnocchi is down), then the dispatcher raised and the data were lost forever. This was completely unacceptable, and nobody should be able to configure Ceilometer in that way." .. releasenotes/notes/fix-agent-coordination-a7103a78fecaec24.yaml @ e84a10882a9b682ff41c84e8bf4ee2497e7e7a31 - [`bug 1533787 `_] Fix an issue where agents are not properly getting registered to group when multiple notification agents are deployed. This can result in bad transformation as the agents are not coordinated. It is still recommended to set heartbeat_timeout_threshold = 0 in [oslo_messaging_rabbit] section when deploying multiple agents. .. releasenotes/notes/thread-safe-matching-4a635fc4965c5d4c.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - [`bug 1519767 `_] fnmatch functionality in python <= 2.7.9 is not threadsafe. this issue and its potential race conditions are now patched. Bug Fixes --------- .. releasenotes/notes/aggregator-transformer-timeout-e0f42b6c96aa7ada.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - [`bug 1531626 `_] Ensure aggregator transformer timeout is honoured if size is not provided. .. releasenotes/notes/cache-json-parsers-888307f3b6b498a2.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1550436 `_] Cache json parsers when building parsing logic to handle event and meter definitions. This will improve agent startup and setup time. .. releasenotes/notes/event-type-race-c295baf7f1661eab.yaml @ 0e3ae8a667d9b9d6e19a7515854eb1703fc05013 - [`bug 1254800 `_] Add better support to catch race conditions when creating event_types .. releasenotes/notes/fix-aggregation-transformer-9472aea189fa8f65.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1539163 `_] Add ability to define whether to use first or last timestamps when aggregating samples. This will allow more flexibility when chaining transformers. .. releasenotes/notes/fix-floatingip-pollster-f5172060c626b19e.yaml @ 1f9f4e1072a5e5037b93734bafcc65e4211eb19f - [`bug 1536338 `_] Patch was added to fix the broken floatingip pollster that polled data from nova api, but since the nova api filtered the data by tenant, ceilometer was not getting any data back. The fix changes the pollster to use the neutron api instead to get the floating ip info. .. releasenotes/notes/fix-network-lb-bytes-sample-5dec2c6f3a8ae174.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - [`bug 1530793 `_] network.services.lb.incoming.bytes meter was previous set to incorrect type. It should be a gauge meter. .. releasenotes/notes/gnocchi-cache-b9ad4d85a1da8d3f.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - [`bug 255569 `_] Fix caching support in Gnocchi dispatcher. Added better locking support to enable smoother cache access. .. releasenotes/notes/gnocchi-orchestration-3497c689268df0d1.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - Fix samples from Heat to map to correct Gnocchi resource type .. releasenotes/notes/gnocchi-udp-collector-00415e6674b5cc0f.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - [`bug 1523124 `_] Fix gnocchi dispatcher to support UDP collector .. releasenotes/notes/handle-malformed-resource-definitions-ad4f69f898ced34d.yaml @ 02b1e1399bf885d03113a1cc125b1f97ed5540b9 - [`bug 1542189 `_] Handle malformed resource definitions in gnocchi_resources.yaml gracefully. Currently we raise an exception once we hit a bad resource and skip the rest. Instead the patch skips the bad resource and proceeds with rest of the definitions. .. releasenotes/notes/improve-events-rbac-support-f216bd7f34b02032.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1504495 `_] Configure ceilometer to handle policy.json rules when possible. .. releasenotes/notes/index-events-mongodb-63cb04200b03a093.yaml @ 1689e7053f4e7587a2b836035cdfa4fda56667fc - [`bug 1526793 `_] Additional indices were added to better support querying of event data. .. releasenotes/notes/lookup-meter-def-vol-correctly-0122ae429275f2a6.yaml @ 903a0a527cb240cfd9462b7f56d3463db7128993 - [`bug 1536699 `_] Patch to fix volume field lookup in meter definition file. In case the field is missing in the definition, it raises a keyerror and aborts. Instead we should skip the missing field meter and continue with the rest of the definitions. .. releasenotes/notes/mongodb-handle-large-numbers-7c235598ca700f2d.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1532661 `_] Fix statistics query failures due to large numbers stored in MongoDB. Data from MongoDB is returned as Int64 for big numbers when int and float types are expected. The data is cast to appropriate type to handle large data. .. releasenotes/notes/skip-duplicate-meter-def-0420164f6a95c50c.yaml @ 0c6f11cf88bf1a13a723879de46ec616678d2e0b - [`bug 1536498 `_] Patch to fix duplicate meter definitions causing duplicate samples. If a duplicate is found, log a warning and skip the meter definition. Note that the first occurrence of a meter will be used and any following duplicates will be skipped from processing. .. releasenotes/notes/sql-query-optimisation-ebb2233f7a9b5d06.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - [`bug 1506738 `_] [`bug 1509677 `_] Optimise SQL backend queries to minimise query load .. releasenotes/notes/support-None-query-45abaae45f08eda4.yaml @ e6fa0a84d1f7a326881f3587718f1df743b8585f - [`bug 1388680 `_] Suppose ability to query for None value when using SQL backend. Other Notes ----------- .. releasenotes/notes/configurable-data-collector-e247aadbffb85243.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - Configure individual dispatchers by specifying meter_dispatchers and event_dispatchers in configuration file. .. releasenotes/notes/gnocchi-cache-1d8025dfc954f281.yaml @ f24ea44401b8945c9cb8a34b2aedebba3c040691 - A dogpile.cache supported backend is required to enable cache. Additional configuration `options `_ are also required. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/newton.rst0000664000175000017500000001614215250267452021367 0ustar00zuulzuul==================== Newton Release Notes ==================== 7.0.5 ===== Bug Fixes --------- .. releasenotes/notes/refresh-legacy-cache-e4dbbd3e2eeca70b.yaml @ 66dd8ab65e2d9352de86e47056dea0b701e21a15 - A local cache is used when polling instance metrics to minimise calls Nova API. A new option is added `resource_cache_expiry` to configure a time to live for cache before it expires. This resolves issue where migrated instances are not removed from cache. 7.0.1 ===== New Features ------------ .. releasenotes/notes/http_proxy_to_wsgi_enabled-616fa123809e1600.yaml @ 032032642ad49e01d706f19f51d672fcff403442 - Ceilometer sets up the HTTPProxyToWSGI middleware in front of Ceilometer. The purpose of this middleware is to set up the request URL correctly in case there is a proxy (for instance, a loadbalancer such as HAProxy) in front of Ceilometer. So, for instance, when TLS connections are being terminated in the proxy, and one tries to get the versions from the / resource of Ceilometer, one will notice that the protocol is incorrect; It will show 'http' instead of 'https'. So this middleware handles such cases. Thus helping Keystone discovery work correctly. The HTTPProxyToWSGI is off by default and needs to be enabled via a configuration value. 7.0.0 ===== Prelude ------- .. releasenotes/notes/rename-ceilometer-dbsync-eb7a1fa503085528.yaml @ 18c181f0b3ce07a0cd552a9060dd09a95cc26078 Ceilometer backends are no more only databases but also REST API like Gnocchi. So ceilometer-dbsync binary name doesn't make a lot of sense and have been renamed ceilometer-upgrade. The new binary handles database schema upgrade like ceilometer-dbsync does, but it also handle any changes needed in configured ceilometer backends like Gnocchi. New Features ------------ .. releasenotes/notes/add-magnum-event-4c75ed0bb268d19c.yaml @ cf3f7c992e0d29e06a7bff6c1df2f0144418d80f - Added support for magnum bay CRUD events, event_type is 'magnum.bay.*'. .. releasenotes/notes/http-dispatcher-verify-ssl-551d639f37849c6f.yaml @ 2fca7ebd7c6a4d29c8a320fffd035ed9814e8293 - In the [dispatcher_http] section of ceilometer.conf, verify_ssl can be set to True to use system-installed certificates (default value) or False to ignore certificate verification (use in development only!). verify_ssl can also be set to the location of a certificate file e.g. /some/path/cert.crt (use for self-signed certs) or to a directory of certificates. The value is passed as the 'verify' option to the underlying requests method, which is documented at http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification .. releasenotes/notes/memory-bandwidth-meter-f86cf01178573671.yaml @ ed7b6dbc952e49ca69de9a94a01398b106aece4b - Add two new meters, including memory.bandwidth.total and memory.bandwidth.local, to get memory bandwidth statistics based on Intel CMT feature. .. releasenotes/notes/perf-events-meter-b06c2a915c33bfaf.yaml @ aaedbbe0eb02ad1f86395a5a490495b64ce26777 - Add four new meters, including perf.cpu.cycles for the number of cpu cycles one instruction needs, perf.instructions for the count of instructions, perf.cache_references for the count of cache hits and cache_misses for the count of caches misses. .. releasenotes/notes/support-meter-batch-recording-mongo-6c2bdf4fbb9764eb.yaml @ a2a04e5d234ba358c25d541f31f8ca1a61bfd5d8 - Add support of batch recording metering data to mongodb backend, since the pymongo support *insert_many* interface which can be used to batch record items, in "big-data" scenarios, this change can improve the performance of metering data recording. .. releasenotes/notes/use-glance-v2-in-image-pollsters-137a315577d5dc4c.yaml @ f8933f4abda4ecfc07ee41f84fd5fd8f6667e95a - Since the Glance v1 APIs won't be maintained any more, this change add the support of glance v2 in images pollsters. Upgrade Notes ------------- .. releasenotes/notes/always-requeue-7a2df9243987ab67.yaml @ 40684dafae76eab77b66bb1da7e143a3d7e2c9c8 - The options 'requeue_event_on_dispatcher_error' and 'requeue_sample_on_dispatcher_error' have been enabled and removed. .. releasenotes/notes/single-thread-pipelines-f9e6ac4b062747fe.yaml @ 5750fddf288c749cacfc825753928f66e755758d - Batching is enabled by default now when coordinated workers are enabled. Depending on load, it is recommended to scale out the number of `pipeline_processing_queues` to improve distribution. `batch_size` should also be configured accordingly. .. releasenotes/notes/use-glance-v2-in-image-pollsters-137a315577d5dc4c.yaml @ f8933f4abda4ecfc07ee41f84fd5fd8f6667e95a - The option 'glance_page_size' has been removed because it's not actually needed. Deprecation Notes ----------------- .. releasenotes/notes/deprecated_database_event_dispatcher_panko-607d558c86a90f17.yaml @ 3685dcf417543db0bb708b347e996d88385c8c5b - The event database dispatcher is now deprecated. It has been moved to a new project, alongside the Ceilometer API for /v2/events, called Panko. .. releasenotes/notes/kwapi_deprecated-c92b9e72c78365f0.yaml @ 2bb81d41f1c5086b68b1290362c72966c1e33702 - The Kwapi pollsters are deprecated and will be removed in the next major version of Ceilometer. .. releasenotes/notes/rename-ceilometer-dbsync-eb7a1fa503085528.yaml @ 18c181f0b3ce07a0cd552a9060dd09a95cc26078 - For backward compatibility reason we temporary keep ceilometer-dbsync, at least for one major version to ensure deployer have time update their tooling. Critical Issues --------------- .. releasenotes/notes/always-requeue-7a2df9243987ab67.yaml @ 40684dafae76eab77b66bb1da7e143a3d7e2c9c8 - The previous configuration options default for 'requeue_sample_on_dispatcher_error' and 'requeue_event_on_dispatcher_error' allowed to lose data very easily: if the dispatcher failed to send data to the backend (e.g. Gnocchi is down), then the dispatcher raised and the data were lost forever. This was completely unacceptable, and nobody should be able to configure Ceilometer in that way." Bug Fixes --------- .. releasenotes/notes/add-db-legacy-clean-tool-7b3e3714f414c448.yaml @ 800034dc0bbb9502893dedd9bcde7c170780c375 - [`bug 1578128 `_] Add a tool that allow users to drop the legacy alarm and alarm_history tables. .. releasenotes/notes/add-full-snmpv3-usm-support-ab540c902fa89b9d.yaml @ dc254e2f78a4bb42b0df6556df8347c7137ab5b2 - [`bug 1597618 `_] Add the full support of snmp v3 user security model. .. releasenotes/notes/single-thread-pipelines-f9e6ac4b062747fe.yaml @ 5750fddf288c749cacfc825753928f66e755758d - Fix to improve handling messages in environments heavily backed up. Previously, notification handlers greedily grabbed messages from queues which could cause ordering issues. A fix was applied to sequentially process messages in a single thread to prevent ordering issues. .. releasenotes/notes/unify-timestamp-of-polled-data-fbfcff43cd2d04bc.yaml @ 8dd821a03dcff45258251bebfd2beb86c07d94f7 - [`bug 1491509 `_] Patch to unify timestamp in samples polled by pollsters. Set the time point polling starts as timestamp of samples, and drop timetamping in pollsters. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/ocata.rst0000664000175000017500000000021015250267452021131 0ustar00zuulzuul=========================== Ocata Series Release Notes =========================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/pike.rst0000664000175000017500000000021715250267452021001 0ustar00zuulzuul=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/queens.rst0000664000175000017500000000022315250267452021346 0ustar00zuulzuul=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/rocky.rst0000664000175000017500000000022115250267452021173 0ustar00zuulzuul=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/stein.rst0000664000175000017500000000022115250267452021166 0ustar00zuulzuul=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/train.rst0000664000175000017500000000017615250267452021172 0ustar00zuulzuul========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3053567 ceilometer-27.0.0.0rc1/releasenotes/source/unreleased.rst0000664000175000017500000000015615250267452022202 0ustar00zuulzuul============================= Current Series Release Notes ============================= .. release-notes:: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/ussuri.rst0000664000175000017500000000020215250267452021375 0ustar00zuulzuul=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/victoria.rst0000664000175000017500000000022015250267452021663 0ustar00zuulzuul============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/wallaby.rst0000664000175000017500000000021415250267452021501 0ustar00zuulzuul============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/xena.rst0000664000175000017500000000020015250267452020774 0ustar00zuulzuul========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/yoga.rst0000664000175000017500000000020015250267452021000 0ustar00zuulzuul========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/releasenotes/source/zed.rst0000664000175000017500000000017415250267452020635 0ustar00zuulzuul======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3063567 ceilometer-27.0.0.0rc1/reno.yaml0000664000175000017500000000023015250267452015150 0ustar00zuulzuul--- # Ignore the kilo-eol tag because that branch does not work with reno # and contains no release notes. closed_branch_tag_re: "(.+)(?=0.13.0 # MIT License cachetools>=2.1.0 # MIT License cotyledon>=1.3.0 #Apache-2.0 futurist>=1.8.0 # Apache-2.0 jsonpath-rw-ext>=1.1.3 # Apache-2.0 lxml>=4.5.1 # BSD msgpack>=0.5.2 # Apache-2.0 oslo.concurrency>=3.29.0 # Apache-2.0 oslo.config>=8.6.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.reports>=1.18.0 # Apache-2.0 oslo.rootwrap>=2.0.0 # Apache-2.0 pbr>=2.0.0 # Apache-2.0 oslo.messaging>=10.3.0 # Apache-2.0 oslo.upgradecheck>=0.1.1 # Apache-2.0 oslo.utils>=4.7.0 # Apache-2.0 oslo.privsep>=1.32.0 # Apache-2.0 keystoneauth1>=3.18.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0 PyYAML>=5.1 # MIT requests>=2.25.1 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 tenacity>=6.3.1 # Apache-2.0 tooz>=1.47.0 # Apache-2.0 oslo.cache>=1.26.0 # Apache-2.0 gnocchiclient>=7.0.0 # Apache-2.0 python-zaqarclient>=1.3.0 # Apache-2.0 prometheus-client>=0.20.0 # Apache-2.0 aodhclient>=3.8.0 # Apache-2.0 awscurl>=0.40 # MIT botocore>=1.20.0 # Apache-2.0 openstacksdk>=4.6.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3511274 ceilometer-27.0.0.0rc1/setup.cfg0000664000175000017500000002630615250267567015165 0ustar00zuulzuul[metadata] name = ceilometer url = http://launchpad.net/ceilometer summary = OpenStack Telemetry description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/ceilometer/latest/ python_requires = >=3.11 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Topic :: System :: Monitoring Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.14 [files] packages = ceilometer data_files = etc/ceilometer = etc/ceilometer/* [entry_points] ceilometer.notification.pipeline = meter = ceilometer.pipeline.sample:SamplePipelineManager event = ceilometer.pipeline.event:EventPipelineManager ceilometer.sample.endpoint = http.request = ceilometer.middleware:HTTPRequest http.response = ceilometer.middleware:HTTPResponse hardware.ipmi.temperature = ceilometer.ipmi.notifications.ironic:TemperatureSensorNotification hardware.ipmi.voltage = ceilometer.ipmi.notifications.ironic:VoltageSensorNotification hardware.ipmi.current = ceilometer.ipmi.notifications.ironic:CurrentSensorNotification hardware.ipmi.fan = ceilometer.ipmi.notifications.ironic:FanSensorNotification _sample = ceilometer.telemetry.notifications:TelemetryIpc meter = ceilometer.meter.notifications:ProcessMeterNotifications ceilometer.discover.compute = local_instances = ceilometer.compute.discovery:InstanceDiscovery local_node = ceilometer.polling.discovery.localnode:LocalNodeDiscovery ceilometer.discover.central = barbican = ceilometer.polling.discovery.non_openstack_credentials_discovery:NonOpenStackCredentialsDiscovery endpoint = ceilometer.polling.discovery.endpoint:EndpointDiscovery tenant = ceilometer.polling.discovery.tenant:TenantDiscovery vpn_services = ceilometer.network.services.discovery:VPNServicesDiscovery ipsec_connections = ceilometer.network.services.discovery:IPSecConnectionsDiscovery fw_services = ceilometer.network.services.discovery:FirewallDiscovery fw_policy = ceilometer.network.services.discovery:FirewallPolicyDiscovery fip_services = ceilometer.network.services.discovery:FloatingIPDiscovery lb_services = ceilometer.load_balancer.discovery:LoadBalancerDiscovery dns_zones = ceilometer.dns.discovery:ZoneDiscovery images = ceilometer.image.discovery:ImagesDiscovery volumes = ceilometer.volume.discovery:VolumeDiscovery volume_pools = ceilometer.volume.discovery:VolumePoolsDiscovery volume_snapshots = ceilometer.volume.discovery:VolumeSnapshotsDiscovery volume_backups = ceilometer.volume.discovery:VolumeBackupsDiscovery volume_services = ceilometer.volume.discovery:VolumeServicesDiscovery alarm = ceilometer.alarm.discovery:AlarmDiscovery manila_shares = ceilometer.share.discovery:ShareDiscovery ceilometer.discover.ipmi = local_node = ceilometer.polling.discovery.localnode:LocalNodeDiscovery ceilometer.poll.compute = disk.device.read.requests = ceilometer.compute.pollsters.disk:PerDeviceReadRequestsPollster disk.device.write.requests = ceilometer.compute.pollsters.disk:PerDeviceWriteRequestsPollster disk.device.read.bytes = ceilometer.compute.pollsters.disk:PerDeviceReadBytesPollster disk.device.write.bytes = ceilometer.compute.pollsters.disk:PerDeviceWriteBytesPollster disk.device.read.latency = ceilometer.compute.pollsters.disk:PerDeviceDiskReadLatencyPollster disk.device.write.latency = ceilometer.compute.pollsters.disk:PerDeviceDiskWriteLatencyPollster power.state = ceilometer.compute.pollsters.instance_stats:PowerStatePollster cpu = ceilometer.compute.pollsters.instance_stats:CPUPollster vcpus = ceilometer.compute.pollsters.instance_stats:VCPUsPollster network.incoming.bytes = ceilometer.compute.pollsters.net:IncomingBytesPollster network.incoming.packets = ceilometer.compute.pollsters.net:IncomingPacketsPollster network.outgoing.bytes = ceilometer.compute.pollsters.net:OutgoingBytesPollster network.outgoing.packets = ceilometer.compute.pollsters.net:OutgoingPacketsPollster network.incoming.bytes.rate = ceilometer.compute.pollsters.net:IncomingBytesRatePollster network.outgoing.bytes.rate = ceilometer.compute.pollsters.net:OutgoingBytesRatePollster network.incoming.bytes.delta = ceilometer.compute.pollsters.net:IncomingBytesDeltaPollster network.outgoing.bytes.delta = ceilometer.compute.pollsters.net:OutgoingBytesDeltaPollster network.incoming.packets.drop = ceilometer.compute.pollsters.net:IncomingDropPollster network.outgoing.packets.drop = ceilometer.compute.pollsters.net:OutgoingDropPollster network.incoming.packets.error = ceilometer.compute.pollsters.net:IncomingErrorsPollster network.outgoing.packets.error = ceilometer.compute.pollsters.net:OutgoingErrorsPollster memory = ceilometer.compute.pollsters.instance_stats:MemoryPollster memory.available = ceilometer.compute.pollsters.instance_stats:MemoryAvailablePollster memory.usage = ceilometer.compute.pollsters.instance_stats:MemoryUsagePollster memory.resident = ceilometer.compute.pollsters.instance_stats:MemoryResidentPollster memory.swap.in = ceilometer.compute.pollsters.instance_stats:MemorySwapInPollster memory.swap.out = ceilometer.compute.pollsters.instance_stats:MemorySwapOutPollster disk.device.capacity = ceilometer.compute.pollsters.disk:PerDeviceCapacityPollster disk.device.allocation = ceilometer.compute.pollsters.disk:PerDeviceAllocationPollster disk.device.usage = ceilometer.compute.pollsters.disk:PerDevicePhysicalPollster disk.ephemeral.size = ceilometer.compute.pollsters.disk:EphemeralSizePollster disk.root.size = ceilometer.compute.pollsters.disk:RootSizePollster perf.cpu.cycles = ceilometer.compute.pollsters.instance_stats:PerfCPUCyclesPollster perf.instructions = ceilometer.compute.pollsters.instance_stats:PerfInstructionsPollster perf.cache.references = ceilometer.compute.pollsters.instance_stats:PerfCacheReferencesPollster perf.cache.misses = ceilometer.compute.pollsters.instance_stats:PerfCacheMissesPollster ceilometer.poll.ipmi = hardware.ipmi.temperature = ceilometer.ipmi.pollsters.sensor:TemperatureSensorPollster hardware.ipmi.voltage = ceilometer.ipmi.pollsters.sensor:VoltageSensorPollster hardware.ipmi.current = ceilometer.ipmi.pollsters.sensor:CurrentSensorPollster hardware.ipmi.fan = ceilometer.ipmi.pollsters.sensor:FanSensorPollster hardware.ipmi.power = ceilometer.ipmi.pollsters.sensor:PowerSensorPollster ceilometer.poll.central = alarm.evaluation_result = ceilometer.alarm.aodh:EvaluationResultPollster ip.floating = ceilometer.network.floatingip:FloatingIPPollster image.size = ceilometer.image.glance:ImageSizePollster radosgw.containers.objects = ceilometer.objectstore.rgw:ContainersObjectsPollster radosgw.containers.objects.size = ceilometer.objectstore.rgw:ContainersSizePollster radosgw.objects = ceilometer.objectstore.rgw:ObjectsPollster radosgw.objects.size = ceilometer.objectstore.rgw:ObjectsSizePollster radosgw.objects.containers = ceilometer.objectstore.rgw:ObjectsContainersPollster radosgw.usage = ceilometer.objectstore.rgw:UsagePollster storage.containers.objects = ceilometer.objectstore.swift:ContainersObjectsPollster storage.containers.objects.size = ceilometer.objectstore.swift:ContainersSizePollster storage.objects = ceilometer.objectstore.swift:ObjectsPollster storage.objects.size = ceilometer.objectstore.swift:ObjectsSizePollster storage.objects.containers = ceilometer.objectstore.swift:ObjectsContainersPollster network.services.vpn = ceilometer.network.services.vpnaas:VPNServicesPollster network.services.vpn.connections = ceilometer.network.services.vpnaas:IPSecConnectionsPollster network.services.firewall = ceilometer.network.services.fwaas:FirewallPollster network.services.firewall.policy = ceilometer.network.services.fwaas:FirewallPolicyPollster loadbalancer.operating = ceilometer.load_balancer.octavia:LoadBalancerOperatingStatusPollster loadbalancer.provisioning = ceilometer.load_balancer.octavia:LoadBalancerProvisioningStatusPollster dns.zone.status = ceilometer.dns.designate:ZoneStatusPollster dns.zone.recordsets = ceilometer.dns.designate:ZoneRecordsetCountPollster dns.zone.ttl = ceilometer.dns.designate:ZoneTTLPollster dns.zone.serial = ceilometer.dns.designate:ZoneSerialPollster volume.size = ceilometer.volume.cinder:VolumeSizePollster volume.snapshot.size = ceilometer.volume.cinder:VolumeSnapshotSize volume.backup.size = ceilometer.volume.cinder:VolumeBackupSize volume.provider.pool.capacity.total = ceilometer.volume.cinder:VolumeProviderPoolCapacityTotal volume.provider.pool.capacity.free = ceilometer.volume.cinder:VolumeProviderPoolCapacityFree volume.provider.pool.capacity.provisioned = ceilometer.volume.cinder:VolumeProviderPoolCapacityProvisioned volume.provider.pool.capacity.virtual_free = ceilometer.volume.cinder:VolumeProviderPoolCapacityVirtualFree volume.provider.pool.capacity.allocated = ceilometer.volume.cinder:VolumeProviderPoolCapacityAllocated volume.service.health = ceilometer.volume.cinder:VolumeServiceHealthPollster manila.share.status = ceilometer.share.manila:ShareStatusPollster manila.share.size = ceilometer.share.manila:ShareSizePollster ceilometer.compute.virt = libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector ceilometer.sample.publisher = test = ceilometer.publisher.test:TestPublisher notifier = ceilometer.publisher.messaging:SampleNotifierPublisher udp = ceilometer.publisher.udp:UDPPublisher tcp = ceilometer.publisher.tcp:TCPPublisher file = ceilometer.publisher.file:FilePublisher http = ceilometer.publisher.http:HttpPublisher prometheus = ceilometer.publisher.prometheus:PrometheusPublisher https = ceilometer.publisher.http:HttpPublisher gnocchi = ceilometer.publisher.gnocchi:GnocchiPublisher zaqar = ceilometer.publisher.zaqar:ZaqarPublisher opentelemetryhttp = ceilometer.publisher.opentelemetry_http:OpentelemetryHttpPublisher ceilometer.event.publisher = test = ceilometer.publisher.test:TestPublisher notifier = ceilometer.publisher.messaging:EventNotifierPublisher http = ceilometer.publisher.http:HttpPublisher https = ceilometer.publisher.http:HttpPublisher gnocchi = ceilometer.publisher.gnocchi:GnocchiPublisher zaqar = ceilometer.publisher.zaqar:ZaqarPublisher file = ceilometer.publisher.file:FilePublisher ceilometer.event.trait_plugin = split = ceilometer.event.trait_plugins:SplitterTraitPlugin bitfield = ceilometer.event.trait_plugins:BitfieldTraitPlugin timedelta = ceilometer.event.trait_plugins:TimedeltaPlugin map = ceilometer.event.trait_plugins:MapTraitPlugin console_scripts = ceilometer-polling = ceilometer.cmd.polling:main ceilometer-agent-notification = ceilometer.cmd.agent_notification:main ceilometer-send-sample = ceilometer.cmd.sample:send_sample ceilometer-upgrade = ceilometer.cmd.storage:upgrade ceilometer-rootwrap = oslo_rootwrap.cmd:main ceilometer-status = ceilometer.cmd.status:main oslo.config.opts = ceilometer = ceilometer.opts:list_opts ceilometer-auth = ceilometer.opts:list_keystoneauth_opts [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3073566 ceilometer-27.0.0.0rc1/setup.py0000664000175000017500000000127115250267452015041 0ustar00zuulzuul# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3073566 ceilometer-27.0.0.0rc1/test-requirements.txt0000664000175000017500000000024715250267452017572 0ustar00zuulzuulfixtures>=3.0.0 # Apache-2.0/BSD oslo.messaging[kafka]>=8.0.0 # Apache-2.0 oslotest>=3.8.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD stestr>=2.0.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964727.3501275 ceilometer-27.0.0.0rc1/tools/0000775000175000017500000000000015250267567014475 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3073566 ceilometer-27.0.0.0rc1/tools/__init__.py0000664000175000017500000000000015250267452016565 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3083568 ceilometer-27.0.0.0rc1/tools/send_test_data.py0000775000175000017500000001110215250267452020017 0ustar00zuulzuul#!/usr/bin/env python3 # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Command line tool for sending test data for Ceilometer via oslo.messaging. Usage: Send messages with samples generated by make_test_data source .tox/py27/bin/activate ./tools/send_test_data.py --count 1000 --resources_count 10 --topic metering """ import argparse import datetime import functools import json import random import uuid import make_test_data import oslo_messaging from oslo_utils import timeutils from ceilometer import messaging from ceilometer.publisher import utils from ceilometer import service def send_batch_notifier(notifier, topic, batch): notifier.sample({}, event_type=topic, payload=batch) def get_notifier(conf): return oslo_messaging.Notifier( messaging.get_transport(conf), driver='messagingv2', publisher_id='telemetry.publisher.test', topics=['metering'], ) def generate_data(conf, send_batch, make_data_args, samples_count, batch_size, resources_count, topic): make_data_args.interval = 1 make_data_args.start = (timeutils.utcnow() - datetime.timedelta(minutes=samples_count)) make_data_args.end = timeutils.utcnow() make_data_args.resource_id = None resources_list = [str(uuid.uuid4()) for _ in range(resources_count)] resource_samples = {resource: 0 for resource in resources_list} batch = [] count = 0 for sample in make_test_data.make_test_data(conf, **make_data_args.__dict__): count += 1 resource = resources_list[random.randint(0, len(resources_list) - 1)] resource_samples[resource] += 1 sample['resource_id'] = resource # need to change the timestamp from datetime.datetime type to iso # format (unicode type), because collector will change iso format # timestamp to datetime.datetime type before recording to db. sample['timestamp'] = sample['timestamp'].isoformat() # need to recalculate signature because of the resource_id change sig = utils.compute_signature(sample, conf.publisher.telemetry_secret) sample['message_signature'] = sig batch.append(sample) if len(batch) == batch_size: send_batch(topic, batch) batch = [] if count == samples_count: send_batch(topic, batch) return resource_samples send_batch(topic, batch) return resource_samples def get_parser(): parser = argparse.ArgumentParser() parser.add_argument( '--batch-size', dest='batch_size', type=int, default=100 ) parser.add_argument( '--config-file', default='/etc/ceilometer/ceilometer.conf' ) parser.add_argument( '--topic', default='perfmetering' ) parser.add_argument( '--samples-count', dest='samples_count', type=int, default=1000 ) parser.add_argument( '--resources-count', dest='resources_count', type=int, default=100 ) parser.add_argument( '--result-directory', dest='result_dir', default='/tmp' ) return parser def main(): args = get_parser().parse_known_args()[0] make_data_args = make_test_data.get_parser().parse_known_args()[0] conf = service.prepare_service(argv=['/', '--config-file', args.config_file]) notifier = get_notifier(conf) send_batch = functools.partial(send_batch_notifier, notifier) result_dir = args.result_dir del args.config_file del args.result_dir resource_writes = generate_data(conf, send_batch, make_data_args, **args.__dict__) result_file = "{}/sample-by-resource-{}".format(result_dir, random.getrandbits(32)) with open(result_file, 'w') as f: f.write(json.dumps(resource_writes)) return result_file if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1788964650.3083568 ceilometer-27.0.0.0rc1/tox.ini0000664000175000017500000000436115250267452014645 0ustar00zuulzuul[tox] minversion = 4.28.0 envlist = py3,pep8 [testenv] constraints = {env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt usedevelop = True passenv = OS_TEST_TIMEOUT OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_LOG_CAPTURE OS_DEBUG commands = stestr run {posargs} oslo-config-generator --config-file=etc/ceilometer/ceilometer-config-generator.conf allowlist_externals = bash [testenv:cover] deps = {[testenv]deps} coverage>=4.4.1 # Apache-2.0 setenv = PYTHON=coverage run --source ceilometer --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover --show-contexts coverage xml -o cover/coverage.xml [testenv:pep8] skip_install = true deps = pre-commit commands = pre-commit run -a [testenv:releasenotes] skip_install = true deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:genconfig] commands = oslo-config-generator --config-file=etc/ceilometer/ceilometer-config-generator.conf [testenv:docs] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build --keep-going -b html -j auto doc/source doc/build/html setenv = PYTHONHASHSEED=0 [testenv:pdf-docs] deps = {[testenv:docs]deps} allowlist_externals = make commands = sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:debug] allowlist_externals = find commands = find . -type f -name "*.pyc" -delete oslo_debug_helper {posargs} [testenv:venv] commands = {posargs} setenv = PYTHONHASHSEED=0 [doc8] ignore = D000 ignore-path = .venv,.git,.tox,.eggs,*ceilometer/locale*,*lib/python*,ceilometer.egg*,doc/build,doc/source/api,releasenotes/* [flake8] select = H exclude=.venv,.git,.tox,.eggs,dist,doc,*lib/python*,*egg,build,install-guide # [H106] Do not put vim configuration in source files. # [H203] Use assertIs(Not)None to check for None. # [H204] Use assert(Not)Equal to check for equality. # [H205] Use assert(Greater|Less)(Equal) for comparison. enable-extensions=H106,H203,H204,H205 show-source = True [hacking] import_exceptions = ceilometer.i18n