pax_global_header00006660000000000000000000000064151343604600014514gustar00rootroot0000000000000052 comment=0ff494d01be85a748c7f5ae583ee47d77b091ff2 ceilometer-25.0.0+git20260122.52.0ff494d01/000077500000000000000000000000001513436046000167735ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/.coveragerc000066400000000000000000000001421513436046000211110ustar00rootroot00000000000000[run] branch = True source = ceilometer omit = ceilometer/tests/* [report] ignore_errors = True ceilometer-25.0.0+git20260122.52.0ff494d01/.gitignore000066400000000000000000000004661513436046000207710ustar00rootroot00000000000000*.egg* *.mo *.pyc .coverage .stestr .tox AUTHORS build/* ChangeLog cover/* dist/* doc/build doc/source/_static/ doc/source/api etc/ceilometer/ceilometer.conf subunit.log # Files created by releasenotes build releasenotes/build #swap file *.swp #IntelJ Idea .idea/ #venv venv/ #Pyenv files .python-version ceilometer-25.0.0+git20260122.52.0ff494d01/.gitreview000066400000000000000000000001151513436046000207760ustar00rootroot00000000000000[gerrit] host=review.opendev.org port=29418 project=openstack/ceilometer.git ceilometer-25.0.0+git20260122.52.0ff494d01/.mailmap000066400000000000000000000037061513436046000204220ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/.pre-commit-config.yaml000066400000000000000000000023011513436046000232500ustar00rootroot00000000000000repos: - 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://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/asottile/pyupgrade rev: v3.20.0 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/openstack/bashate rev: 2.1.1 hooks: - id: bashate args: ['-v', '-iE006'] exclude: '.tox/.*' ceilometer-25.0.0+git20260122.52.0ff494d01/.stestr.conf000066400000000000000000000001051513436046000212400ustar00rootroot00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-ceilometer/tests/unit} top_dir=./ceilometer-25.0.0+git20260122.52.0ff494d01/.zuul.yaml000066400000000000000000000034001513436046000207310ustar00rootroot00000000000000- job: name: grenade-ceilometer parent: grenade required-projects: - opendev.org/openstack/grenade - opendev.org/openstack/ceilometer - name: gnocchixyz/gnocchi override-checkout: stable/4.6 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/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: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files gate: jobs: - grenade-ceilometer - telemetry-dsvm-integration: irrelevant-files: *ceilometer-irrelevant-files - telemetry-dsvm-integration-ipv6-only: irrelevant-files: *ceilometer-irrelevant-files ceilometer-25.0.0+git20260122.52.0ff494d01/CONTRIBUTING.rst000066400000000000000000000010651513436046000214360ustar00rootroot00000000000000If 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 ceilometer-25.0.0+git20260122.52.0ff494d01/HACKING.rst000066400000000000000000000020571513436046000205750ustar00rootroot00000000000000Ceilometer Style Commandments ============================= - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Ceilometer Specific Commandments -------------------------------- - [C301] LOG.warn() is not allowed. Use LOG.warning() - [C302] Deprecated library function os.popen() 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 testtools.TestCase. 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. ceilometer-25.0.0+git20260122.52.0ff494d01/LICENSE000066400000000000000000000236371513436046000200130ustar00rootroot00000000000000 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. ceilometer-25.0.0+git20260122.52.0ff494d01/MAINTAINERS000066400000000000000000000006401513436046000204700ustar00rootroot00000000000000= Generalist Code Reviewers = The current members of ceilometer-core are listed here: https://launchpad.net/~ceilometer-drivers/+members#active This group can +2 and approve patches in Ceilometer. However, they may choose to seek feedback from the appropriate specialist maintainer before approving a patch if it is in any way controversial or risky. = IRC handles of maintainers = gordc jd__ lhx pradk sileht ceilometer-25.0.0+git20260122.52.0ff494d01/README.rst000066400000000000000000000025361513436046000204700ustar00rootroot00000000000000========== 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://github.com/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/ ceilometer-25.0.0+git20260122.52.0ff494d01/bindep.txt000066400000000000000000000004161513436046000207760ustar00rootroot00000000000000libxml2-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] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/000077500000000000000000000000001513436046000211235ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/__init__.py000066400000000000000000000011701513436046000232330ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/agent.py000066400000000000000000000073171513436046000226030ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/alarm/000077500000000000000000000000001513436046000222175ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/alarm/__init__.py000066400000000000000000000000001513436046000243160ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/alarm/aodh.py000066400000000000000000000040551513436046000235100ustar00rootroot00000000000000# # 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']}, ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/alarm/discovery.py000066400000000000000000000025611513436046000246040ustar00rootroot00000000000000# # 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 oslo_config import cfg from ceilometer import keystone_client from ceilometer.polling import plugin_base 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.""" return [self.aodh_client.metrics.get(all_projects=True)] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cache_utils.py000066400000000000000000000056001513436046000237610ustar00rootroot00000000000000# # 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/000077500000000000000000000000001513436046000216665ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/__init__.py000066400000000000000000000000001513436046000237650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/agent_notification.py000066400000000000000000000020331513436046000261020ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/polling.py000066400000000000000000000070171513436046000237110ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/sample.py000066400000000000000000000060451513436046000235260ustar00rootroot00000000000000# # 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))]) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/status.py000066400000000000000000000030061513436046000235620ustar00rootroot00000000000000# 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()) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/cmd/storage.py000066400000000000000000000035731513436046000237140ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/000077500000000000000000000000001513436046000225775ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/__init__.py000066400000000000000000000000001513436046000246760ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/discovery.py000066400000000000000000000371701513436046000251700ustar00rootroot00000000000000# # 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('fetch_extra_metadata', default=True, 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. fetch_extra_metadata is currently set to " "True by default, but to reduce the load on Nova API " "this will be changed to False in a future release."), ] 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/000077500000000000000000000000001513436046000246265ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/__init__.py000066400000000000000000000175231513436046000267470ustar00rootroot00000000000000# 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.error( 'Could not get %(name)s events for %(id)s: %(e)s', { 'name': self.sample_name, 'id': instance.id, 'e': err}, exc_info=True) 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(), ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/disk.py000066400000000000000000000065111513436046000261350ustar00rootroot00000000000000# # 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'])) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/instance_stats.py000066400000000000000000000054661513436046000302350ustar00rootroot00000000000000# # 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' ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/net.py000066400000000000000000000074531513436046000257770ustar00rootroot00000000000000# # 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' ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/pollsters/util.py000066400000000000000000000070651513436046000261650ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/000077500000000000000000000000001513436046000235635ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/__init__.py000066400000000000000000000000001513436046000256620ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/inspector.py000066400000000000000000000206261513436046000261510ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/libvirt/000077500000000000000000000000001513436046000252365ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/libvirt/__init__.py000066400000000000000000000000001513436046000273350ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/libvirt/inspector.py000066400000000000000000000264201513436046000276220ustar00rootroot00000000000000# # 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_not_shut_off_or_raise(self, instance): instance_name = util.instance_name(instance) domain = self._lookup_by_uuid(instance) 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_not_shut_off_or_raise(instance) 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 instanceStats, " "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_not_shut_off_or_raise(instance) 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_not_shut_off_or_raise(instance) 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_not_shut_off_or_raise(instance) 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") ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/compute/virt/libvirt/utils.py000066400000000000000000000127311513436046000267540ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/data/000077500000000000000000000000001513436046000220345ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/data/meters.d/000077500000000000000000000000001513436046000235555ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/data/meters.d/meters.yaml000066400000000000000000000325061513436046000257460ustar00rootroot00000000000000--- 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/declarative.py000066400000000000000000000154271513436046000237710ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/designate_client.py000066400000000000000000000027601513436046000250030ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/dns/000077500000000000000000000000001513436046000217075ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/dns/__init__.py000066400000000000000000000000001513436046000240060ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/dns/designate.py000066400000000000000000000113461513436046000242310ustar00rootroot00000000000000# # 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) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/dns/discovery.py000066400000000000000000000020371513436046000242720ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/event/000077500000000000000000000000001513436046000222445ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/event/__init__.py000066400000000000000000000000001513436046000243430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/event/converter.py000066400000000000000000000270431513436046000246330ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/event/models.py000066400000000000000000000112441513436046000241030ustar00rootroot00000000000000# # 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] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/event/trait_plugins.py000066400000000000000000000237601513436046000255120ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/gnocchi_client.py000066400000000000000000000326771513436046000244640ustar00rootroot00000000000000# # 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} } }]}, ] 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/hacking/000077500000000000000000000000001513436046000225275ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/hacking/__init__.py000066400000000000000000000000001513436046000246260ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/hacking/checks.py000066400000000000000000000032641513436046000243460ustar00rootroot00000000000000# Copyright (c) 2016 OpenStack Foundation # 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. """ Guidelines for writing new hacking checks - Use only for Ceilometer specific tests. OpenStack general tests should be submitted to the common 'hacking' module. - Pick numbers in the range X3xx. Find the current test with the highest allocated number and then pick the next value. - Keep the test method code in the source file ordered based on the C3xx value. - List the new rule in the top level HACKING.rst file """ from hacking import core @core.flake8ext def no_log_warn(logical_line): """Disallow 'LOG.warn(' https://bugs.launchpad.net/tempest/+bug/1508442 C301 """ if logical_line.startswith('LOG.warn('): yield (0, 'C301 Use LOG.warning() rather than LOG.warn()') @core.flake8ext def no_os_popen(logical_line): """Disallow 'os.popen(' Deprecated library function os.popen() Replace it using subprocess https://bugs.launchpad.net/tempest/+bug/1529836 C302 """ if 'os.popen(' in logical_line: yield (0, 'C302 Deprecated library function os.popen(). ' 'Replace it using subprocess module. ') ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/i18n.py000066400000000000000000000020471513436046000222570ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/image/000077500000000000000000000000001513436046000222055ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/image/__init__.py000066400000000000000000000000001513436046000243040ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/image/discovery.py000066400000000000000000000025161513436046000245720ustar00rootroot00000000000000# # 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 glanceclient from oslo_config import cfg from ceilometer import keystone_client from ceilometer.polling import plugin_base SERVICE_OPTS = [ cfg.StrOpt('glance', default='image', help='Glance service type.'), ] class ImagesDiscovery(plugin_base.DiscoveryBase): def __init__(self, conf): super().__init__(conf) creds = conf.service_credentials self.glance_client = glanceclient.Client( version='2', session=keystone_client.get_session(conf), region_name=creds.region_name, interface=creds.interface, service_type=conf.service_types.glance) def discover(self, manager, param=None): """Discover resources to monitor.""" return self.glance_client.images.list() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/image/glance.py000066400000000000000000000034601513436046000240130ustar00rootroot00000000000000# # 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", "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), ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/000077500000000000000000000000001513436046000220615ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/__init__.py000066400000000000000000000000001513436046000241600ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/notifications/000077500000000000000000000000001513436046000247325ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/notifications/__init__.py000066400000000000000000000000001513436046000270310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/notifications/ironic.py000066400000000000000000000131621513436046000265720ustar00rootroot00000000000000# # 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' ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/platform/000077500000000000000000000000001513436046000237055ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/platform/__init__.py000066400000000000000000000000001513436046000260040ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/platform/exception.py000066400000000000000000000012441513436046000262560ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/platform/ipmi_sensor.py000066400000000000000000000102161513436046000266060ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/platform/ipmitool.py000066400000000000000000000106411513436046000261150ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/pollsters/000077500000000000000000000000001513436046000241105ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/pollsters/__init__.py000066400000000000000000000016741513436046000262310ustar00rootroot00000000000000# 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.') ] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/ipmi/pollsters/sensor.py000066400000000000000000000114541513436046000260000ustar00rootroot00000000000000# 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' ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/keystone_client.py000066400000000000000000000073311513436046000247000ustar00rootroot00000000000000# # 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 os from keystoneauth1 import loading as ka_loading from keystoneclient.v3 import client as ks_client_v3 from oslo_config import cfg DEFAULT_GROUP = "service_credentials" # List of group that can set auth_section to use a different # credentials section OVERRIDABLE_GROUPS = ['gnocchi', 'zaqar'] 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_client(conf, trust_id=None, requests_session=None, group=DEFAULT_GROUP): """Return a client for keystone v3 endpoint, optionally using a trust.""" session = get_session(conf, requests_session=requests_session, group=group) return ks_client_v3.Client(session=session, trust_id=trust_id, 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 get_auth_token(client): return client.session.auth.get_access(client.session).auth_token CLI_OPTS = [ cfg.StrOpt('region-name', deprecated_group="DEFAULT", deprecated_name="os-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', os.environ.get('OS_ENDPOINT_TYPE', 'public')), deprecated_name="os-endpoint-type", choices=('public', 'internal', 'admin', 'auth', '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, deprecated_opts={'cacert': [ cfg.DeprecatedOpt('os-cacert', group=group), cfg.DeprecatedOpt('os-cacert', group="DEFAULT")] }) 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/load_balancer/000077500000000000000000000000001513436046000236715ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/load_balancer/__init__.py000066400000000000000000000000001513436046000257700ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/load_balancer/discovery.py000066400000000000000000000020471513436046000262550ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/load_balancer/octavia.py000066400000000000000000000077721513436046000257060ustar00rootroot00000000000000# # 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) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/000077500000000000000000000000001513436046000223625ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/de/000077500000000000000000000000001513436046000227525ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/de/LC_MESSAGES/000077500000000000000000000000001513436046000245375ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/de/LC_MESSAGES/ceilometer.po000066400000000000000000000076611513436046000272410ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/en_GB/000077500000000000000000000000001513436046000233345ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/en_GB/LC_MESSAGES/000077500000000000000000000000001513436046000251215ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/en_GB/LC_MESSAGES/ceilometer.po000066400000000000000000000072511513436046000276160ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/es/000077500000000000000000000000001513436046000227715ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/es/LC_MESSAGES/000077500000000000000000000000001513436046000245565ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/es/LC_MESSAGES/ceilometer.po000066400000000000000000000063661513436046000272610ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/fr/000077500000000000000000000000001513436046000227715ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/fr/LC_MESSAGES/000077500000000000000000000000001513436046000245565ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/fr/LC_MESSAGES/ceilometer.po000066400000000000000000000101121513436046000272410ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/it/000077500000000000000000000000001513436046000227765ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/it/LC_MESSAGES/000077500000000000000000000000001513436046000245635ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/it/LC_MESSAGES/ceilometer.po000066400000000000000000000063141513436046000272570ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ja/000077500000000000000000000000001513436046000227545ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ja/LC_MESSAGES/000077500000000000000000000000001513436046000245415ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ja/LC_MESSAGES/ceilometer.po000066400000000000000000000067041513436046000272400ustar00rootroot00000000000000# 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 ă®ĺ®źčˇŚă«ĺ¤±ć•—ă—ăľă—ăź" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ko_KR/000077500000000000000000000000001513436046000233675ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ko_KR/LC_MESSAGES/000077500000000000000000000000001513436046000251545ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ko_KR/LC_MESSAGES/ceilometer.po000066400000000000000000000071761513436046000276570ustar00rootroot00000000000000# 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 실행 실패" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/pt_BR/000077500000000000000000000000001513436046000233705ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/pt_BR/LC_MESSAGES/000077500000000000000000000000001513436046000251555ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/pt_BR/LC_MESSAGES/ceilometer.po000066400000000000000000000062501513436046000276500ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ru/000077500000000000000000000000001513436046000230105ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ru/LC_MESSAGES/000077500000000000000000000000001513436046000245755ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/ru/LC_MESSAGES/ceilometer.po000066400000000000000000000107561513436046000272760ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_CN/000077500000000000000000000000001513436046000233635ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_CN/LC_MESSAGES/000077500000000000000000000000001513436046000251505ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_CN/LC_MESSAGES/ceilometer.po000066400000000000000000000064041513436046000276440ustar00rootroot00000000000000# 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时失败了" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_TW/000077500000000000000000000000001513436046000234155ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_TW/LC_MESSAGES/000077500000000000000000000000001513436046000252025ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/locale/zh_TW/LC_MESSAGES/ceilometer.po000066400000000000000000000057751513436046000277100ustar00rootroot00000000000000# 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 失敗" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/messaging.py000066400000000000000000000064251513436046000234610ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/meter/000077500000000000000000000000001513436046000222375ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/meter/__init__.py000066400000000000000000000000001513436046000243360ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/meter/notifications.py000066400000000000000000000222331513436046000254640ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/middleware.py000066400000000000000000000024561513436046000236210ustar00rootroot00000000000000# # 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'] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/000077500000000000000000000000001513436046000226145ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/__init__.py000066400000000000000000000000001513436046000247130ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/floatingip.py000066400000000000000000000040541513436046000253250ustar00rootroot00000000000000# 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) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/000077500000000000000000000000001513436046000244375ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/__init__.py000066400000000000000000000000001513436046000265360ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/base.py000066400000000000000000000022361513436046000257260ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/discovery.py000066400000000000000000000037151513436046000270260ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/fwaas.py000066400000000000000000000054661513436046000261250ustar00rootroot00000000000000# # 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 firewalls status samples.""" FIELDS = ['admin_state_up', 'description', 'name', 'status', '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['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['tenant_id'], resource_id=fw['id'], resource_metadata=self.extract_metadata(fw) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/network/services/vpnaas.py000066400000000000000000000061431513436046000263050ustar00rootroot00000000000000# # 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 = ['admin_state_up', 'description', 'name', 'status', 'subnet_id', 'router_id' ] @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['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', 'psk', 'initiator', 'ikepolicy_id', 'dpd', 'ipsecpolicy_id', 'vpnservice_id', 'mtu', 'admin_state_up', 'status', 'tenant_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['tenant_id'], resource_id=conn['id'], resource_metadata=self.extract_metadata(conn) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/neutron_client.py000066400000000000000000000050021513436046000245220ustar00rootroot00000000000000# 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 neutronclient.common import exceptions from neutronclient.v2_0 import client as clientv20 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): @functools.wraps(func) def with_logging(*args, **kwargs): try: return func(*args, **kwargs) except exceptions.NeutronClientException 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 python-neutronclient.""" def __init__(self, conf): creds = conf.service_credentials params = { 'session': keystone_client.get_session(conf), 'endpoint_type': creds.interface, 'region_name': creds.region_name, 'service_type': conf.service_types.neutron, } self.client = clientv20.Client(**params) @logged def vpn_get_all(self): resp = self.client.list_vpnservices() return resp.get('vpnservices') @logged def ipsec_site_connections_get_all(self): resp = self.client.list_ipsec_site_connections() return resp.get('ipsec_site_connections') @logged def firewall_get_all(self): resp = self.client.list_firewalls() return resp.get('firewalls') @logged def fw_policy_get_all(self): resp = self.client.list_firewall_policies() return resp.get('firewall_policies') @logged def fip_get_all(self): fips = self.client.list_floatingips()['floatingips'] return fips ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/notification.py000066400000000000000000000150361513436046000241700ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/nova_client.py000066400000000000000000000114561513436046000240050ustar00rootroot00000000000000# # 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 glanceclient import novaclient from novaclient import api_versions from novaclient import client as nova_client 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.glance_client = glanceclient.Client( version='2', session=ks_session, region_name=creds.region_name, interface=creds.interface, service_type=conf.service_types.glance) 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.glance_client.images.get(iid) except glanceclient.exc.HTTPNotFound: 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) image_metadata = getattr(image, 'metadata', None) for attr, default in attr_defaults: ameta = image_metadata.get(attr) if image_metadata 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. image_meta.update((k, str(v)) for k, v in image.items() if k not in ('id', 'name', 'metadata')) 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/objectstore/000077500000000000000000000000001513436046000234465ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/objectstore/__init__.py000066400000000000000000000000001513436046000255450ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/objectstore/rgw.py000066400000000000000000000211371513436046000246230ustar00rootroot00000000000000# # 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__) 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.'), ] 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 @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 and conf.rgw_client.rgw_service_name: try: creds = conf.service_credentials # Use the service_name to target the endpoint. # There are cases where both 'radosgw' and 'swift' are used. rgw_url = keystone_client.get_service_catalog( ksclient).url_for( service_name=conf.rgw_client.rgw_service_name, interface=creds.interface, region_name=creds.region_name) _Base._ENDPOINT = urlparse.urljoin(rgw_url, '/admin') except exceptions.EndpointNotFound: LOG.debug("Radosgw endpoint not found") elif _Base._ENDPOINT is None and conf.service_types.radosgw: try: creds = conf.service_credentials rgw_url = keystone_client.get_service_catalog( ksclient).url_for( service_type=conf.service_types.radosgw, interface=creds.interface, region_name=creds.region_name) _Base._ENDPOINT = urlparse.urljoin(rgw_url, '/admin') except exceptions.EndpointNotFound: LOG.debug("Radosgw endpoint not found") LOG.debug(f"Using endpoint {_Base._ENDPOINT} for radosgw connections") 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) 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, ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/objectstore/rgw_client.py000066400000000000000000000072111513436046000261560ustar00rootroot00000000000000# # 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 _ class RGWAdminAPIFailed(Exception): pass class RGWAdminClient: Bucket = namedtuple('Bucket', 'name, num_objects, size') def __init__(self, endpoint, access_key, secret_key, implicit_tenants): self.access_key = access_key self.secret = secret_key self.endpoint = endpoint self.hostname = urlparse.urlparse(endpoint).netloc self.implicit_tenants = implicit_tenants def _make_request(self, path, req_params): uri = f"{self.endpoint}/{path}" if req_params: uri = f"{uri}?" # Append req_params content to the uri for i, (key, value) in enumerate(req_params.items()): if i == len(req_params) - 1: uri = uri + key + "=" + value else: uri = uri + key + "=" + value + "&" # Data to be sent with request POST type, # otherwise provide an empty string data = "" service = "s3" method = "GET" # 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. region = "us-east-1" headers = {'Accept': 'application/json'} r = awscurl.make_request(method, service, region, uri, headers, data, self.access_key, self.secret, None, False, False) 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/objectstore/swift.py000066400000000000000000000175331513436046000251650ustar00rootroot00000000000000# # 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.get_service_catalog( ksclient).url_for( 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), ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/octavia_client.py000066400000000000000000000025471513436046000244710ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/opts.py000066400000000000000000000121411513436046000224610ustar00rootroot00000000000000# 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.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('http_timeout', default=600, deprecated_for_removal=True, deprecated_reason='This option has no effect', help='Timeout seconds for HTTP requests. Set it to None to ' 'disable timeout.'), 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): readd 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.designate_client.SERVICE_OPTS, ceilometer.image.discovery.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, ceilometer.volume.discovery.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 ))] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/000077500000000000000000000000001513436046000227305ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/__init__.py000066400000000000000000000000001513436046000250270ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/base.py000066400000000000000000000251531513436046000242220ustar00rootroot00000000000000# # 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.error("Unable to load publisher %s", p, exc_info=True) 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/data/000077500000000000000000000000001513436046000236415ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/data/event_definitions.yaml000066400000000000000000000442501513436046000302460ustar00rootroot00000000000000--- - 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_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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/data/event_pipeline.yaml000066400000000000000000000002601513436046000275310ustar00rootroot00000000000000--- sources: - name: event_source events: - "*" sinks: - event_sink sinks: - name: event_sink publishers: - notifier:// ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/data/pipeline.yaml000066400000000000000000000002571513436046000263360ustar00rootroot00000000000000--- sources: - name: meter_source meters: - "*" sinks: - meter_sink sinks: - name: meter_sink publishers: - gnocchi:// ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/event.py000066400000000000000000000105331513436046000244250ustar00rootroot00000000000000# 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.error('Fail to process a notification', exc_info=True) 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.error("Pipeline %(pipeline)s: %(status)s " "after error from publisher %(pub)s", {'pipeline': self, 'status': 'Continue' if self.multi_publish else 'Exit', 'pub': p}, exc_info=True) 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())] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/pipeline/sample.py000066400000000000000000000126031513436046000245650ustar00rootroot00000000000000# # 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.error('Fail to process notification message [%s]', message, exc_info=True) 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.error("Pipeline %(pipeline)s: Continue after " "error from publisher %(pub)s", {'pipeline': self, 'pub': p}, exc_info=True) @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] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/000077500000000000000000000000001513436046000225675ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/__init__.py000066400000000000000000000000001513436046000246660ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery/000077500000000000000000000000001513436046000245765ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery/__init__.py000066400000000000000000000000001513436046000266750ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery/endpoint.py000066400000000000000000000027701513436046000267760ustar00rootroot00000000000000# 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_service_catalog( manager.keystone).get_urls( 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery/localnode.py000066400000000000000000000015551513436046000271160ustar00rootroot00000000000000# 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 non_openstack_credentials_discovery.py000066400000000000000000000041521513436046000344000ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery# 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): barbican_secret = "No secrets found" if not param: return [barbican_secret] 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 [barbican_secret] 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] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/discovery/tenant.py000066400000000000000000000035231513436046000264440ustar00rootroot00000000000000# 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): domains = manager.keystone.domains.list() LOG.debug(f"Found {len(domains)} keystone domains") tenants = [] for domain in domains: domain_tenants = manager.keystone.projects.list(domain) if self.conf.polling.ignore_disabled_projects: enabled_tenants = [tenant for tenant in domain_tenants if tenant.enabled] LOG.debug(f"Found {len(enabled_tenants)} enabled " f"tenants in domain {domain.name}") tenants = enabled_tenants + domain_tenants else: LOG.debug(f"Found {len(domain_tenants)} " f"tenants in domain {domain.name}") tenants = tenants + domain_tenants return tenants or [] ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/dynamic_pollster.py000066400000000000000000001356331513436046000265240ustar00rootroot00000000000000# # 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()) 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) 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) 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) 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) 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/manager.py000066400000000000000000001300201513436046000245470ustar00rootroot00000000000000# # 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 resource_factory = lambda: Resources(agent_manager) # noqa: E731 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.error( 'Continue after error from %(name)s: %(error)s', {'name': pollster.name, 'error': err}, exc_info=True) 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" f" on {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(f"Updated heartbeat for {hb['pollster']} " f"({hb['timestamp']})") def _send_heartbeat(self): s, addr = self._sock.accept() LOG.debug("Heartbeat status report requested " f"at {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(f"Reported heartbeat status:\n{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(f"Polster heartbeat update: {name}") except queue.Full: LOG.warning(f"Heartbeat queue full. Update failed: {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(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.\ get_service_catalog(self.keystone).\ get_endpoints(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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/plugin_base.py000066400000000000000000000137031513436046000254350ustar00rootroot00000000000000# # 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' ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/polling/prom_exporter.py000066400000000000000000000134461513436046000260560ustar00rootroot00000000000000# # 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. import prometheus_client as prom 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('.', '_') 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 '') 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/privsep/000077500000000000000000000000001513436046000226135ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/privsep/__init__.py000066400000000000000000000021251513436046000247240ustar00rootroot00000000000000# # 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], ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/privsep/ipmitool.py000066400000000000000000000014271513436046000250250ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/000077500000000000000000000000001513436046000231205ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/__init__.py000066400000000000000000000027311513436046000252340ustar00rootroot00000000000000# # 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.""" ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/data/000077500000000000000000000000001513436046000240315ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/data/gnocchi_resources.yaml000066400000000000000000000226751513436046000304350ustar00rootroot00000000000000--- 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_type_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: 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: 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/file.py000066400000000000000000000102631513436046000244130ustar00rootroot00000000000000# # 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 explicitely 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) self.publisher_logger = logging.Logger('publisher.file') 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()) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/gnocchi.py000066400000000000000000000653221513436046000251140ustar00rootroot00000000000000# # 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 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 wierd 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.error("Failed to load resource due to error", exc_info=True) 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.domains.find( name=self.filter_domain) project = self._ks_client.projects.find( name=self.filter_project, domain_id=domain.id) except ka_exceptions.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, "project_id": sample.project_id}} gnocchi_data[resource_id].setdefault( "resource_extra", {}).update(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.error("Unexpected exception while pushing measures [%s] for " "gnocchi data [%s]: [%s].", measures, gnocchi_data, str(e), exc_info=True) 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.error("Unexpected exception updating resource type [%s] " "with ID [%s] for resource data [%s]: [%s].", resource_type, resource.get('id'), resource_extra, str(e), exc_info=True) @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(), digest_size=16).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.error("Failed to create resource %s", resource, exc_info=True) def _search_resource(self, resource_type, query): try: return self._gnocchi.resource.search( resource_type, json.loads(query)) except Exception: LOG.error("Fail to search resource type %(resource_type)s " "with '%(query)s'", {'resource_type': resource_type, 'query': query}, exc_info=True) 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.error("Fail to update the resource %s", resource, exc_info=True) 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.error("Fail to update the resource %s", resource, exc_info=True) LOG.debug('Resource %(resource_id)s ended at %(ended_at)s', {'resource_id': resource["id"], 'ended_at': ended_at}) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/http.py000066400000000000000000000172011513436046000244520ustar00rootroot00000000000000# # 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): LOG.trace('Message: %s', data) try: res = self.session.post(self.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.', self.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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/messaging.py000066400000000000000000000222071513436046000254520ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/opentelemetry_http.py000066400000000000000000000111641513436046000274300ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/prometheus.py000066400000000000000000000053351513436046000256730ustar00rootroot00000000000000# # 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 ceilometer.publisher import http from ceilometer import sample 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 """ HEADERS = {'Content-type': 'plain/text'} def publish_samples(self, samples): """Send a metering message for publishing :param samples: Samples from pipeline after transformation """ if not samples: return data = "" doc_done = set() for s in samples: # 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(".", "_") if metric_type and curated_sname not in doc_done: data += f"# TYPE {curated_sname} {metric_type}\n" doc_done.add(curated_sname) # NOTE(sileht): prometheus pushgateway doesn't allow to push # timestamp_ms # # timestamp_ms = ( # s.get_iso_timestamp().replace(tzinfo=None) - # datetime.utcfromtimestamp(0) # ).total_seconds() * 1000 # data += '%s{resource_id="%s"} %s %d\n' % ( # curated_sname, s.resource_id, s.volume, timestamp_ms) data += '%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) self._do_post(data) @staticmethod def publish_events(events): raise NotImplementedError ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/tcp.py000066400000000000000000000075601513436046000242700ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/test.py000066400000000000000000000025371513436046000244600ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/udp.py000066400000000000000000000055301513436046000242650ustar00rootroot00000000000000# # 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/utils.py000066400000000000000000000123431513436046000246350ustar00rootroot00000000000000# # 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.', deprecated_opts=[cfg.DeprecatedOpt("metering_secret", "DEFAULT"), cfg.DeprecatedOpt("metering_secret", "publisher_rpc"), cfg.DeprecatedOpt("metering_secret", "publisher")] ), ] 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/publisher/zaqar.py000066400000000000000000000052231513436046000246120ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/sample.py000066400000000000000000000141061513436046000227600ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/service.py000066400000000000000000000037151513436046000231430ustar00rootroot00000000000000# 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 = (conf.default_log_levels + ['futurist=INFO', 'neutronclient=INFO', 'keystoneclient=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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/telemetry/000077500000000000000000000000001513436046000231355ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/telemetry/__init__.py000066400000000000000000000000001513436046000252340ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/telemetry/notifications.py000066400000000000000000000035601513436046000263640ustar00rootroot00000000000000# # 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') ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/000077500000000000000000000000001513436046000222655ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/__init__.py000066400000000000000000000000001513436046000243640ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/base.py000066400000000000000000000053401513436046000235530ustar00rootroot00000000000000# 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 import fixtures import oslo_messaging.conffixture from oslotest import base import yaml import ceilometer from ceilometer import messaging class BaseTestCase(base.BaseTestCase): 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/000077500000000000000000000000001513436046000232445ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/__init__.py000066400000000000000000000000001513436046000253430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/alarm/000077500000000000000000000000001513436046000243405ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/alarm/__init__.py000066400000000000000000000000001513436046000264370ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/alarm/test_aodh.py000066400000000000000000000045201513436046000266650ustar00rootroot00000000000000# # 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']) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/cmd/000077500000000000000000000000001513436046000240075ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/cmd/__init__.py000066400000000000000000000000001513436046000261060ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/cmd/test_status.py000066400000000000000000000017551513436046000267530ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/000077500000000000000000000000001513436046000247205ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/__init__.py000066400000000000000000000000001513436046000270170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/000077500000000000000000000000001513436046000267475ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/__init__.py000066400000000000000000000000001513436046000310460ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/base.py000066400000000000000000000063301513436046000302350ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_cpu.py000066400000000000000000000134641513436046000311570ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_disk.py000066400000000000000000000120211513436046000313060ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_diskio.py000066400000000000000000000204521513436046000316450ustar00rootroot00000000000000# # 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') test_location_metadata.py000066400000000000000000000163371513436046000337630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters# # 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 oslotest import base from ceilometer.compute.pollsters import util from ceilometer.polling import manager from ceilometer import service 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_memory.py000066400000000000000000000221101513436046000316640ustar00rootroot00000000000000# 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])) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_net.py000066400000000000000000000335251513436046000311560ustar00rootroot00000000000000# # 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', ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/pollsters/test_perf.py000066400000000000000000000067301513436046000313220ustar00rootroot00000000000000# 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])) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/test_discovery.py000066400000000000000000001201471513436046000303450ustar00rootroot00000000000000# # 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() 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) 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.assertEqual({"server_group": "group1"}, metadata["user_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_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) 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.assertEqual({"server_group": "group1"}, metadata["user_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_no_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", False, 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) metadata = s.resource_metadata self.assertNotIn("user_metadata", 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") self.CONF.set_override("fetch_extra_metadata", False, 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") self.CONF.set_override("fetch_extra_metadata", False, 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") self.CONF.set_override("fetch_extra_metadata", False, 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") self.CONF.set_override("fetch_extra_metadata", False, 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") self.CONF.set_override("fetch_extra_metadata", False, 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") self.CONF.set_override("fetch_extra_metadata", False, 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) # test raise NotFound exception self.client.nova_client.servers.get = mock.MagicMock( side_effect=exceptions.NotFound(404)) dsc = discovery.InstanceDiscovery(self.CONF) ret_server = dsc.get_server(uuid) self.assertIsNone(ret_server) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/virt/000077500000000000000000000000001513436046000257045ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/virt/__init__.py000066400000000000000000000000001513436046000300030ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/virt/libvirt/000077500000000000000000000000001513436046000273575ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/virt/libvirt/__init__.py000066400000000000000000000000001513436046000314560ustar00rootroot00000000000000test_inspector.py000066400000000000000000000577321513436046000327350ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/compute/virt/libvirt# 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 oslotest import base 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 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_cpus_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_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_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_usage_with_domain_shutoff(self): domain = mock.Mock() domain.info.return_value = (5, 0, 51200, 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_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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/dns/000077500000000000000000000000001513436046000240305ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/dns/__init__.py000066400000000000000000000000001513436046000261270ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/dns/test_designate.py000066400000000000000000000206201513436046000274040ustar00rootroot00000000000000# # 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.dns.v2 import recordset from openstack.dns.v2 import zone from oslotest import base from ceilometer.dns import designate from ceilometer.dns import discovery from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import service class _BaseTestDNSPollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) # Mock the openstack.connection.Connection to avoid auth issues with mock.patch('openstack.connection.Connection'): self.manager = manager.AgentManager(0, self.CONF) plugin_base._get_keystone = mock.Mock() catalog = (plugin_base._get_keystone.session.auth.get_access. return_value.service_catalog) catalog.get_endpoints = mock.MagicMock( return_value={'dns': mock.ANY}) @staticmethod def fake_get_zones(): return [ 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.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.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', ), ] @staticmethod def fake_get_recordsets(): return [ recordset.Recordset( connection=None, id='rs-1-uuid', name='www.example.com.', type='A', records=['192.168.1.1'], ttl=3600, ), recordset.Recordset( connection=None, id='rs-2-uuid', name='mail.example.com.', type='MX', records=['10 mail.example.com.'], ttl=3600, ), ] class TestZoneStatusPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() self.pollster = designate.ZoneStatusPollster(self.CONF) fake_zones = self.fake_get_zones() self.useFixture(fixtures.MockPatch( 'ceilometer.designate_client.Client.zones_list', return_value=fake_zones)) def test_zone_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) self.assertEqual(len(self.fake_get_zones()), len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(self.fake_get_zones()[0], field), samples[0].resource_metadata[field]) def test_zone_status_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) # 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): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) self.assertEqual({'dns.zone.status'}, {s.name for s in samples}) def test_zone_discovery(self): with mock.patch('openstack.connection.Connection'): discovered_zones = discovery.ZoneDiscovery( self.CONF).discover(self.manager) self.assertEqual(len(self.fake_get_zones()), len(list(discovered_zones))) class TestZoneRecordsetCountPollster(_BaseTestDNSPollster): def setUp(self): super().setUp() with mock.patch('openstack.connection.Connection'): self.pollster = designate.ZoneRecordsetCountPollster(self.CONF) self.useFixture(fixtures.MockPatch( 'ceilometer.designate_client.Client.recordsets_list', return_value=self.fake_get_recordsets())) def test_zone_recordsets_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) # Each zone should have the mocked number of recordsets num_recordsets = len(self.fake_get_recordsets()) for sample in samples: self.assertEqual(num_recordsets, sample.volume) def test_get_zone_meter_names(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) 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): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) 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): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) 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): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) 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): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_zones())) 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) fake_zone = 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', ) samples = list(pollster.get_samples( self.manager, {}, resources=[fake_zone])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/event/000077500000000000000000000000001513436046000243655ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/event/__init__.py000066400000000000000000000000001513436046000264640ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/event/test_converter.py000066400000000000000000001002431513436046000300050ustar00rootroot00000000000000# # 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:')) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/event/test_endpoint.py000066400000000000000000000164101513436046000276200ustar00rootroot00000000000000# # 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.error 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.error self.assertIn('Continue after error from publisher', exception_mock.call_args_list[0][0][0] % exception_mock.call_args_list[0][0][1]) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/event/test_trait_plugins.py000066400000000000000000000134221513436046000306640ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.event import trait_plugins 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)])) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/image/000077500000000000000000000000001513436046000243265ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/image/__init__.py000066400000000000000000000000001513436046000264250ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/image/test_glance.py000066400000000000000000000076011513436046000271740ustar00rootroot00000000000000# # 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 IMAGE_LIST = [ type('Image', (object,), {'status': 'active', 'tags': [], 'kernel_id': 'fd24d91a-dfd5-4a3c-b990-d4563eb27396', 'container_format': 'ami', 'min_ram': 0, 'ramdisk_id': 'd629522b-ebaa-4c92-9514-9e31fe760d18', 'updated_at': '2016-06-20T13: 34: 41Z', 'visibility': 'public', 'owner': '6824974c08974d4db864bbaa6bc08303', 'file': '/v2/images/fda54a44-3f96-40bf-ab07-0a4ce9e1761d/file', 'min_disk': 0, 'virtual_size': None, 'id': 'fda54a44-3f96-40bf-ab07-0a4ce9e1761d', 'size': 25165824, 'name': 'cirros-0.3.4-x86_64-uec', 'checksum': 'eb9139e4942121f22bbc2afc0400b2a4', 'created_at': '2016-06-20T13: 34: 40Z', 'disk_format': 'ami', 'protected': False, 'schema': '/v2/schemas/image'}), type('Image', (object,), {'status': 'active', 'tags': [], 'container_format': 'ari', 'min_ram': 0, 'updated_at': '2016-06-20T13: 34: 38Z', 'visibility': 'public', 'owner': '6824974c08974d4db864bbaa6bc08303', 'file': '/v2/images/d629522b-ebaa-4c92-9514-9e31fe760d18/file', 'min_disk': 0, 'virtual_size': None, 'id': 'd629522b-ebaa-4c92-9514-9e31fe760d18', 'size': 3740163, 'name': 'cirros-0.3.4-x86_64-uec-ramdisk', 'checksum': 'be575a2b939972276ef675752936977f', 'created_at': '2016-06-20T13: 34: 37Z', 'disk_format': 'ari', 'protected': False, 'schema': '/v2/schemas/image'}), type('Image', (object,), {'status': 'active', 'tags': [], 'container_format': 'aki', 'min_ram': 0, 'updated_at': '2016-06-20T13: 34: 35Z', 'visibility': 'public', 'owner': '6824974c08974d4db864bbaa6bc08303', 'file': '/v2/images/fd24d91a-dfd5-4a3c-b990-d4563eb27396/file', 'min_disk': 0, 'virtual_size': None, 'id': 'fd24d91a-dfd5-4a3c-b990-d4563eb27396', 'size': 4979632, 'name': 'cirros-0.3.4-x86_64-uec-kernel', 'checksum': '8a40c862b5735975d82605c1dd395796', 'created_at': '2016-06-20T13: 34: 35Z', 'disk_format': 'aki', 'protected': False, 'schema': '/v2/schemas/image'}), ] 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=IMAGE_LIST)) self.assertEqual(3, len(image_samples)) self.assertEqual('image.size', image_samples[0].name) self.assertEqual(25165824, image_samples[0].volume) self.assertEqual('6824974c08974d4db864bbaa6bc08303', image_samples[0].project_id) self.assertEqual('fda54a44-3f96-40bf-ab07-0a4ce9e1761d', image_samples[0].resource_id) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/000077500000000000000000000000001513436046000242025ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/__init__.py000066400000000000000000000000001513436046000263010ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/notifications/000077500000000000000000000000001513436046000270535ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/notifications/__init__.py000066400000000000000000000000001513436046000311520ustar00rootroot00000000000000ipmi_test_data.py000066400000000000000000001005001513436046000323300ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/notifications# # 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)', }, } } } } ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/notifications/test_ironic.py000066400000000000000000000217101513436046000317500ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.ipmi.notifications import ironic as ipmi from ceilometer import sample 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] ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/platform/000077500000000000000000000000001513436046000260265ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/platform/__init__.py000066400000000000000000000000001513436046000301250ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/platform/fake_utils.py000066400000000000000000000026271513436046000305350ustar00rootroot00000000000000# 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): datas = { 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 datas[cmd_str] def execute_without_ipmi(*cmd, **kwargs): raise nmexcept.IPMIException ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/platform/ipmitool_test_data.py000066400000000000000000000275121513436046000322730ustar00rootroot00000000000000# 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, '') ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/platform/test_ipmi_sensor.py000066400000000000000000000116341513436046000317730ustar00rootroot00000000000000# 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 oslotest import base from ceilometer.ipmi.platform import ipmi_sensor from ceilometer.privsep import ipmitool 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/pollsters/000077500000000000000000000000001513436046000262315ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/pollsters/__init__.py000066400000000000000000000000001513436046000303300ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/pollsters/base.py000066400000000000000000000045471513436046000275270ustar00rootroot00000000000000# 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/ipmi/pollsters/test_sensor.py000066400000000000000000000105731513436046000311610ustar00rootroot00000000000000# 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, float(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, float(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, float(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, int(154), self.CONF.host) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/load_balancer/000077500000000000000000000000001513436046000260125ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/load_balancer/__init__.py000066400000000000000000000000001513436046000301110ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/load_balancer/test_octavia.py000066400000000000000000000203171513436046000310540ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.load_balancer import discovery from ceilometer.load_balancer import octavia from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import service class FakeLoadBalancer: """Fake load balancer object mimicking openstacksdk Resource.""" def __init__(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) class _BaseTestLBPollster(base.BaseTestCase): def setUp(self): super().setUp() self.addCleanup(mock.patch.stopall) self.CONF = service.prepare_service([], []) # Mock the openstack.connection.Connection to avoid auth issues with mock.patch('openstack.connection.Connection'): self.manager = manager.AgentManager(0, self.CONF) plugin_base._get_keystone = mock.Mock() catalog = (plugin_base._get_keystone.session.auth.get_access. return_value.service_catalog) catalog.get_endpoints = mock.MagicMock( return_value={'load-balancer': mock.ANY}) @staticmethod def fake_get_loadbalancers(): return [ FakeLoadBalancer( 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', ), FakeLoadBalancer( 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', ), FakeLoadBalancer( 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', ), FakeLoadBalancer( 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', ), ] class TestLoadBalancerOperatingStatusPollster(_BaseTestLBPollster): def setUp(self): super().setUp() self.pollster = octavia.LoadBalancerOperatingStatusPollster(self.CONF) fake_lbs = self.fake_get_loadbalancers() self.useFixture(fixtures.MockPatch( 'ceilometer.octavia_client.Client.loadbalancers_list', return_value=fake_lbs)) def test_lb_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_loadbalancers())) self.assertEqual(4, len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(self.fake_get_loadbalancers()[0], field), samples[0].resource_metadata[field]) def test_lb_operating_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_loadbalancers())) # 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.fake_get_loadbalancers())) self.assertEqual({'loadbalancer.operating'}, {s.name for s in samples}) def test_lb_discovery(self): with mock.patch('openstack.connection.Connection'): discovered_lbs = discovery.LoadBalancerDiscovery( self.CONF).discover(self.manager) self.assertEqual(4, len(list(discovered_lbs))) class TestLoadBalancerProvisioningStatusPollster(_BaseTestLBPollster): def setUp(self): super().setUp() self.pollster = octavia.LoadBalancerProvisioningStatusPollster( self.CONF) fake_lbs = self.fake_get_loadbalancers() self.useFixture(fixtures.MockPatch( 'ceilometer.octavia_client.Client.loadbalancers_list', return_value=fake_lbs)) def test_lb_get_samples(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_loadbalancers())) self.assertEqual(4, len(samples)) for field in self.pollster.FIELDS: self.assertEqual( getattr(self.fake_get_loadbalancers()[0], field), samples[0].resource_metadata[field]) def test_lb_provisioning_volume(self): samples = list(self.pollster.get_samples( self.manager, {}, resources=self.fake_get_loadbalancers())) # 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.fake_get_loadbalancers())) self.assertEqual({'loadbalancer.provisioning'}, {s.name for s in samples}) class TestLoadBalancerPollsterUnknownStatus(_BaseTestLBPollster): def test_unknown_operating_status(self): pollster = octavia.LoadBalancerOperatingStatusPollster(self.CONF) fake_lb = FakeLoadBalancer( 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', ) samples = list(pollster.get_samples( self.manager, {}, resources=[fake_lb])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) def test_unknown_provisioning_status(self): pollster = octavia.LoadBalancerProvisioningStatusPollster(self.CONF) fake_lb = FakeLoadBalancer( 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', ) samples = list(pollster.get_samples( self.manager, {}, resources=[fake_lb])) self.assertEqual(1, len(samples)) self.assertEqual(-1, samples[0].volume) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/meter/000077500000000000000000000000001513436046000243605ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/meter/__init__.py000066400000000000000000000000001513436046000264570ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/meter/test_meter_plugins.py000066400000000000000000000062251513436046000306530ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.event import trait_plugins 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/meter/test_notifications.py000066400000000000000000001217361513436046000306540ustar00rootroot00000000000000# # 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")) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/000077500000000000000000000000001513436046000247355ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/__init__.py000066400000000000000000000000001513436046000270340ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/services/000077500000000000000000000000001513436046000265605ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/services/__init__.py000066400000000000000000000000001513436046000306570ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/services/test_fwaas.py000066400000000000000000000175611513436046000313040ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.network.services import discovery from ceilometer.network.services import fwaas from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import service 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) plugin_base._get_keystone = mock.Mock() catalog = (plugin_base._get_keystone.session.auth.get_access. return_value.service_catalog) catalog.get_endpoints = mock.MagicMock( return_value={'network': mock.ANY}) 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', 'firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'INACTIVE', 'name': 'myfw2', 'description': '', 'admin_state_up': True, 'id': 'e0d707dc-6194-4471-8286-0635bf65a055', 'firewall_policy_id': 'e0d707dc-6194-4471-8286-0635bf65a055', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'PENDING_CREATE', 'name': 'myfw3', 'description': '', 'admin_state_up': True, 'id': 'e538d353-31e9-4581-a511-0a487ff71d0d', 'firewall_policy_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'ERROR', 'name': 'myfw4', 'description': '', 'admin_state_up': True, 'id': '06f698c4-dc63-43c4-a2d9-7b978e80f09a', 'firewall_policy_id': 'bef98f97-789f-418e-82ad-3e5d69618916', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': 'UNKNOWN', 'name': 'myfw5', 'description': '', 'admin_state_up': True, 'id': 'c65a1bec-ab59-44ce-b784-1c725f427998', 'firewall_policy_id': 'd45b975e-738f-42c3-a4b3-760d3a58ab51', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa'}, {'status': None, 'name': 'myfw6', 'description': '', 'admin_state_up': True, 'id': 'ab5d19ff-32a8-49e5-aa2b-d008157359d9', 'firewall_policy_id': '79b9c933-2a7c-4f93-bbf9-d165f0326581', 'tenant_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, 'tenant_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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/services/test_vpnaas.py000066400000000000000000000205241513436046000314640ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.network.services import discovery from ceilometer.network.services import vpnaas from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import service 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) plugin_base._get_keystone = mock.Mock() catalog = (plugin_base._get_keystone.session.auth.get_access. return_value.service_catalog) catalog.get_endpoints = mock.MagicMock( return_value={'network': mock.ANY}) 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': '', 'admin_state_up': True, 'id': 'fdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'}, {'status': 'INACTIVE', 'name': 'myvpn2', 'description': '', 'admin_state_up': True, 'id': 'cdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'}, {'status': 'PENDING_CREATE', 'name': 'myvpn3', 'description': '', 'id': 'bdde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'admin_state_up': True, 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'}, {'status': 'error', 'name': 'myvpn4', 'description': '', 'id': 'edde3d818-fdcb-fg4b-de7f-6750dc8a9d7a', 'admin_state_up': False, 'subnet_id': 'bbe3d818-bdcb-4e4b-b47f-5650dc8a9d7a', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': 'ade3d818-fdcb-fg4b-de7f-6750dc8a9d7a'}, {'status': 'UNKNOWN', 'name': 'myvpn5', 'description': '', 'id': '34e6383a-b1ab-4602-b26a-a1ae7b759212', 'admin_state_up': False, 'subnet_id': '8c20bbbf-1409-4bc4-b652-3aeda66746c1', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': '0e5c9333-2ef5-4c90-9cca-5cc898515da4'}, {'status': None, 'name': 'myvpn6', 'description': '', 'id': '6e94ff61-8dea-4154-98f1-4020e4b2cecd', 'admin_state_up': False, 'subnet_id': '5e2a20c3-547a-43e4-90c5-26d32ea42d10', 'tenant_id': 'a4eb9f4938bb418bbc4f8eb31802fefa', 'router_id': '5b14df87-60c1-4fc7-8ad5-7811b2199c7f'}, ] 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', 'dpd': { 'action': 'hold', 'interval': 30, 'timeout': 120}, 'ikepolicy_id': 'ade3d818-fdcb-fg4b-de7f-4550dc8a9d7a', 'ipsecpolicy_id': 'fce3d818-fdcb-fg4b-de7f-7850dc8a9d7a', 'vpnservice_id': 'dce3d818-fdcb-fg4b-de7f-5650dc8a9d7a', 'admin_state_up': True, 'status': 'ACTIVE', '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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/network/test_floating_ip.py000066400000000000000000000133531513436046000306460ustar00rootroot00000000000000# 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. from unittest import mock import fixtures from oslotest import base from ceilometer.network import floatingip from ceilometer.network.services import discovery from ceilometer.polling import manager from ceilometer.polling import plugin_base from ceilometer import service class _BaseTestFloatingIPPollster(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self.manager = manager.AgentManager(0, self.CONF) plugin_base._get_keystone = mock.Mock() 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/objectstore/000077500000000000000000000000001513436046000255675ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/objectstore/__init__.py000066400000000000000000000000001513436046000276660ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/objectstore/test_rgw.py000066400000000000000000000167461513436046000300150ustar00rootroot00000000000000# 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 from oslotest import base import testscenarios.testcase from ceilometer.objectstore import rgw from ceilometer.objectstore import rgw_client from ceilometer.polling import manager from ceilometer import service 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/objectstore/test_rgw_client.py000066400000000000000000000173531513436046000313460ustar00rootroot00000000000000# 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 oslotest import base from ceilometer.objectstore import rgw_client 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.get_resp = mock.MagicMock() self.get = mock.patch('requests.request', return_value=self.get_resp).start() def test_make_request_exception(self): self.get_resp.status_code = 403 self.assertRaises(rgw_client.RGWAdminAPIFailed, self.client._make_request, *('foo', {})) def test_make_request(self): self.get_resp.status_code = 200 self.get_resp.json.return_value = buckets_json actual = self.client._make_request('foo', []) self.assertEqual(buckets_json, actual) def test_get_buckets(self): self.get_resp.status_code = 200 self.get_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.assertEqual(1, len(self.get.call_args_list)) self.assertEqual('http://127.0.0.1:8080/admin/bucket?' 'uid=foo&stats=true', self.get.call_args_list[0][0][1]) def test_get_buckets_implicit_tenants(self): self.get_resp.status_code = 200 self.get_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.assertEqual(1, len(self.get.call_args_list)) self.assertEqual('http://127.0.0.1:8080/admin/bucket?' 'uid=foo$foo&stats=true', self.get.call_args_list[0][0][1]) def test_get_usage(self): self.get_resp.status_code = 200 self.get_resp.json.return_value = usage_json actual = self.client.get_usage('foo') expected = 7 self.assertEqual(expected, actual) self.assertEqual(1, len(self.get.call_args_list)) self.assertEqual('http://127.0.0.1:8080/admin/usage?uid=foo', self.get.call_args_list[0][0][1]) def test_get_usage_implicit_tenants(self): self.get_resp.status_code = 200 self.get_resp.json.return_value = usage_json self.client.implicit_tenants = True actual = self.client.get_usage('foo') expected = 7 self.assertEqual(expected, actual) self.assertEqual(1, len(self.get.call_args_list)) self.assertEqual('http://127.0.0.1:8080/admin/usage?uid=foo$foo', self.get.call_args_list[0][0][1]) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/objectstore/test_swift.py000066400000000000000000000300011513436046000303260ustar00rootroot00000000000000# 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 oslotest import base from swiftclient import client as swift_client import testscenarios.testcase from ceilometer.objectstore import swift from ceilometer.polling import manager from ceilometer import service 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/pipeline_base.py000066400000000000000000000427521513436046000264270ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/000077500000000000000000000000001513436046000247105ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/__init__.py000066400000000000000000000000001513436046000270070ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/test_discovery.py000066400000000000000000000133131513436046000303310ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.polling.discovery import endpoint from ceilometer.polling.discovery import localnode from ceilometer.polling.discovery import tenant as project from ceilometer import service 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', 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, 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 prepare_mock_data(self): domain_heat = mock.MagicMock() domain_heat.id = '2f42ab40b7ad4140815ef830d816a16c' domain_heat.name = 'heat' domain_heat.enabled = True domain_heat.links = { 'self': 'http://192.168.1.1/identity/v3/domains/' '2f42ab40b7ad4140815ef830d816a16c'} domain_default = mock.MagicMock() domain_default.id = 'default' domain_default.name = 'Default' domain_default.enabled = True domain_default.links = { 'self': 'http://192.168.1.1/identity/v3/domains/default'} project_admin = mock.MagicMock() project_admin.id = '2ce92449a23145ef9c539f3327960ce3' project_admin.name = 'admin' project_admin.parent_id = 'default' project_admin.domain_id = 'default' project_admin.is_domain = False project_admin.enabled = True project_admin.links = { 'self': 'http://192.168.4.46/identity/v3/projects/' '2ce92449a23145ef9c539f3327960ce3'}, project_service = mock.MagicMock() project_service.id = '9bf93b86bca04e3b815f86a5de083adc' project_service.name = 'service' project_service.parent_id = 'default' project_service.domain_id = 'default' project_service.is_domain = False project_service.enabled = True project_service.links = { 'self': 'http://192.168.4.46/identity/v3/projects/' '9bf93b86bca04e3b815f86a5de083adc'} project_demo = mock.MagicMock() project_demo.id = '57d96b9af18d43bb9d047f436279b0be' project_demo.name = 'demo' project_demo.parent_id = 'default' project_demo.domain_id = 'default' project_demo.is_domain = False project_demo.enabled = True project_demo.links = { 'self': 'http://192.168.4.46/identity/v3/projects/' '57d96b9af18d43bb9d047f436279b0be'} self.domains = [domain_heat, domain_default] self.default_domain_projects = [project_admin, project_service] self.heat_domain_projects = [project_demo] def side_effect(self, domain=None): if not domain or domain.name == 'Default': return self.default_domain_projects elif domain.name == 'heat': return self.heat_domain_projects else: return [] def setUp(self): super().setUp() CONF = service.prepare_service([], []) self.discovery = project.TenantDiscovery(CONF) self.prepare_mock_data() self.manager = mock.MagicMock() self.manager.keystone.projects.list.side_effect = self.side_effect def test_project_discovery(self): self.manager.keystone.domains.list.return_value = self.domains result = self.discovery.discover(self.manager) self.assertEqual(len(result), 3) self.assertEqual(self.manager.keystone.projects.list.call_count, 2) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/test_dynamic_pollster.py000066400000000000000000002200101513436046000316640ustar00rootroot00000000000000# # 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 oslotest import base LOG = logging.getLogger(__name__) REQUIRED_POLLSTER_FIELDS = ['name', 'sample_type', 'unit', 'value_attribute', 'endpoint_type', 'url_path'] 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 = TestDynamicPollster.FakeResponse() return_value.status_code = requests.codes.ok return_value.json_object = self.response[url] return return_value class TestDynamicPollster(base.BaseTestCase): class FakeResponse: status_code = None json_object = None _text = None @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 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) @mock.patch('keystoneclient.v2_0.client.Client') def test_skip_samples_with_linked_samples(self, keystone_mock): 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.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) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_get_samples_empty_response(self, client_mock): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value.json_object = {} client_mock.session.get.return_value = return_value samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(0, len(samples)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_get_samples_response_non_empty( self, client_mock): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value.json_object = {"firstElement": [{}, {}, {}]} client_mock.session.get.return_value = return_value samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_json_response_handler( self, client_mock): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._text = '{"test": [1,2,3]}' client_mock.session.get.return_value = return_value samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_xml_response_handler( self, client_mock): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml'] pollster = dynamic_pollster.DynamicPollster(definitions) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._text = '123' client_mock.session.get.return_value = return_value samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_xml_json_response_handler( self, client_mock): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml', 'json'] pollster = dynamic_pollster.DynamicPollster(definitions) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._text = '123' client_mock.session.get.return_value = return_value samples = pollster.definitions.sample_gatherer. \ execute_request_get_samples( keystone_client=client_mock, resource="https://endpoint.server.name/") self.assertEqual(3, len(samples)) return_value._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)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_extra_metadata_fields_cache_disabled( self, client_mock): 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 = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._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 = self.FakeResponse() return_value9999.status_code = requests.codes.ok return_value9999._text = ''' {"project": {"project_id": 9999, "name": "project1"} } ''' return_value8888 = self.FakeResponse() return_value8888.status_code = requests.codes.ok return_value8888._text = ''' {"project": {"project_id": 8888, "name": "project2"} } ''' return_value7777 = self.FakeResponse() return_value7777.status_code = requests.codes.ok return_value7777._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.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) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_extra_metadata_fields_cache_enabled( self, client_mock): 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 = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._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 = self.FakeResponse() return_value9999.status_code = requests.codes.ok return_value9999._text = ''' {"project": {"project_id": 9999, "name": "project1"} } ''' return_value8888 = self.FakeResponse() return_value8888.status_code = requests.codes.ok return_value8888._text = ''' {"project": {"project_id": 8888, "name": "project2"} } ''' return_value7777 = self.FakeResponse() return_value7777.status_code = requests.codes.ok return_value7777._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.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) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_extra_metadata_fields( self, client_mock): 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 = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._text = ''' {"projects": [ {"project_id": 9999, "name": "project1"}, {"project_id": 8888, "name": "project2"}, {"project_id": 7777, "name": "project3"}] } ''' return_value9999 = self.FakeResponse() return_value9999.status_code = requests.codes.ok return_value9999._text = ''' {"project": {"project_id": 9999, "name": "project1", "meta": "m1"} } ''' return_value8888 = self.FakeResponse() return_value8888.status_code = requests.codes.ok return_value8888._text = ''' {"project": {"project_id": 8888, "name": "project2", "meta": "m2"} } ''' return_value7777 = self.FakeResponse() return_value7777.status_code = requests.codes.ok return_value7777._text = ''' {"project": {"project_id": 7777, "name": "project3", "meta": "m3"} } ''' return_valueP1 = self.FakeResponse() return_valueP1.status_code = requests.codes.ok return_valueP1._text = ''' {"project": {"project_id": 7777, "name": "p1", "meta": null} } ''' return_valueP2 = self.FakeResponse() return_valueP2.status_code = requests.codes.ok return_valueP2._text = ''' {"project": {"project_id": 7777, "name": "p2", "meta": null} } ''' return_valueP3 = self.FakeResponse() return_valueP3.status_code = requests.codes.ok return_valueP3._text = ''' {"project": {"project_id": 7777, "name": "p3", "meta": null} } ''' return_valueM1 = self.FakeResponse() return_valueM1.status_code = requests.codes.ok return_valueM1._text = ''' {"project": {"project_id": "META1", "name": "p3", "meta": null} } ''' return_valueM2 = self.FakeResponse() return_valueM2.status_code = requests.codes.ok return_valueM2._text = ''' {"project": {"project_id": "META2", "name": "p3", "meta": null} } ''' return_valueM3 = self.FakeResponse() return_valueM3.status_code = requests.codes.ok return_valueM3._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.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'}) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_extra_metadata_fields_skip( self, client_mock): 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 = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._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 = self.FakeResponse() return_value9999.status_code = requests.codes.ok return_value9999._text = ''' {"project": {"project_id": 9999, "name": "project1"} } ''' return_value8888 = self.FakeResponse() return_value8888.status_code = requests.codes.ok return_value8888._text = ''' {"project": {"project_id": 8888, "name": "project2"} } ''' return_value7777 = self.FakeResponse() return_value7777.status_code = requests.codes.ok return_value7777._text = ''' {"project": {"project_id": 7777, "name": "project3"} } ''' return_value6666 = self.FakeResponse() return_value6666.status_code = requests.codes.ok return_value6666._text = ''' {"project": {"project_id": 6666, "name": "project4"} } ''' return_valueP1 = self.FakeResponse() return_valueP1.status_code = requests.codes.ok return_valueP1._text = ''' {"project": {"project_id": 7777, "name": "p1"} } ''' return_valueP2 = self.FakeResponse() return_valueP2.status_code = requests.codes.ok return_valueP2._text = ''' {"project": {"project_id": 7777, "name": "p2"} } ''' return_valueP3 = self.FakeResponse() return_valueP3.status_code = requests.codes.ok return_valueP3._text = ''' {"project": {"project_id": 7777, "name": "p3"} } ''' return_valueP4 = self.FakeResponse() return_valueP4.status_code = requests.codes.ok return_valueP4._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.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'}) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_extra_metadata_fields_different_requests( self, client_mock): 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 = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._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.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'}) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_xml_json_response_handler_invalid_response( self, client_mock): definitions = copy.deepcopy( self.pollster_definition_only_required_fields) definitions['response_handlers'] = ['xml', 'json'] pollster = dynamic_pollster.DynamicPollster(definitions) return_value = self.FakeResponse() return_value.status_code = requests.codes.ok return_value._text = 'Invalid response' client_mock.session.get.return_value = return_value 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)) @mock.patch('keystoneclient.v2_0.client.Client') def test_execute_request_get_samples_exception_on_request( self, client_mock): pollster = dynamic_pollster.DynamicPollster( self.pollster_definition_only_required_fields) return_value = self.FakeResponse() return_value.status_code = requests.codes.bad client_mock.session.get.return_value = return_value 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"]) @mock.patch('keystoneclient.v2_0.client.Client') def test_metadata_nested_objects(self, keystone_mock): 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.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']) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/test_heartbeat.py000066400000000000000000000103421513436046000302600ustar00rootroot00000000000000# # 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" f" on {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(f"Updated heartbeat for 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 " f"at {self.tmpdir}/ceilometer-central.socket"), mock.call("Reported heartbeat status:\n" f"test1 {timestamp}\n" f"test2 {timestamp}")] LOG.debug.assert_has_calls(calls) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling/test_manager.py000066400000000000000000001154561513436046000277470ustar00rootroot00000000000000# # 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 keystoneauth1 import exceptions as ka_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 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.projects.list() 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.useFixture(fixtures.MockPatch('keystoneclient.v2_0.client.Client', return_value=mock.Mock())) 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() ks_client = mock.Mock(auth_token='fake_token') ks_client.projects.get.return_value = mock.Mock( name='admin', id='4465ecd1438b4d23a866cf8447387a7b' ) ks_client.users.get.return_value = mock.Mock( name='admin', id='c0c935468e654d5a8baae1a08adf4dfb' ) self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_client', return_value=ks_client)) self.ks_client = ks_client 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: 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: 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.""" self.useFixture(fixtures.MockPatch( 'keystoneclient.v2_0.client.Client', side_effect=ka_exceptions.ClientException)) 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() 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) test_non_openstack_credentials_discovery.py000066400000000000000000000076101513436046000355530ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling# 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 from oslotest import base import requests from ceilometer.polling.discovery.endpoint import EndpointDiscovery from ceilometer.polling.discovery.non_openstack_credentials_discovery import \ NonOpenStackCredentialsDiscovery 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(['No secrets found'], result) result = self.discovery.discover(None, "") self.assertEqual(['No secrets found'], 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(['No secrets found'], result) EndpointDiscovery.discover = original_discover_method @mock.patch('keystoneclient.v2_0.client.Client') def test_discover_error_response(self, client_mock): 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.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 @mock.patch('keystoneclient.v2_0.client.Client') def test_discover_response_ok(self, client_mock): 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.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 test_non_openstack_dynamic_pollster.py000066400000000000000000000461451513436046000345450ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/polling# # 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 from oslotest import base 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 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/000077500000000000000000000000001513436046000252415ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/__init__.py000066400000000000000000000000001513436046000273400ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_file.py000066400000000000000000000130501513436046000275700ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.publisher import file from ceilometer import sample from ceilometer import service 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']) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_gnocchi.py000066400000000000000000001122231513436046000302650ustar00rootroot00000000000000# # 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 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('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('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', }, ), ] ks_client = mock.Mock(auth_token='fake_token') ks_client.projects.find.return_value = mock.Mock( name='gnocchi', id='a2d42c23-d518-46b6-96ab-3fba2e146859') self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_client', return_value=ks_client)) 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())) self.ks_client = ks_client 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.error.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.ks_client.projects.find.side_effect = ka_exceptions.NotFound 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)) ks_client = mock.Mock() ks_client.projects.find.return_value = mock.Mock( name='gnocchi', id='a2d42c23-d518-46b6-96ab-3fba2e146859') self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_client', return_value=ks_client)) self.useFixture(fixtures.MockPatch( 'ceilometer.keystone_client.get_session', return_value=ks_client)) self.ks_client = ks_client @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) ) if self.patchable_attributes: expected_calls.append(mock.call.resource.update( self.resource_type, resource_id, self.patchable_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 self.patchable_attributes)): if self.update_resource_fail and self.patchable_attributes: logger.error.assert_called_with( 'Unexpected exception updating resource type [%s] with ' 'ID [%s] for resource data [%s]: [%s].', resource_type, resource_id, mock.ANY, 'boom!', exc_info=True) else: logger.error.assert_called_with( 'Unexpected exception while pushing measures [%s] for ' 'gnocchi data [%s]: [%s].', expected_measures_in_log, mock.ANY, 'boom!', exc_info=True) 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_http.py000066400000000000000000000243431513436046000276370ustar00rootroot00000000000000# # 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 from oslotest import base 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 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']) test_messaging_publisher.py000066400000000000000000000346771513436046000326460ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher# # 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] ) test_opentelemetry_http.py000066400000000000000000000147471513436046000325430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher# # 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 from oslotest import base import requests from urllib import parse as urlparse from ceilometer.publisher import opentelemetry_http from ceilometer import sample from ceilometer import service 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) datas = [] for s in self.sample_data: datas.append(self._make_fake_json(s, self.format_time)) expected = [] for d in datas: 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) datas = [] for s in self.sample_data: datas.append(self._make_fake_json(s, self.format_time)) expected = [] for d in datas: 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_prometheus.py000066400000000000000000000117501513436046000310510ustar00rootroot00000000000000# # 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 from oslotest import base import requests from urllib import parse as urlparse from ceilometer.publisher import prometheus from ceilometer import sample from ceilometer import service 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) expected = [ mock.call('http://localhost:90/metrics/job/os', 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) expected = [ mock.call('https://localhost:90/metrics/job/os', auth=None, cert=None, data=data, headers={'Content-type': 'plain/text'}, timeout=5, verify=True) ] self.assertEqual(expected, m_req.mock_calls) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_tcp.py000066400000000000000000000151601513436046000274430ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.publisher.tcp import TCPPublisher from ceilometer.publisher import utils from ceilometer import sample from ceilometer import service 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_udp.py000066400000000000000000000115551513436046000274510ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.publisher import udp from ceilometer.publisher import utils from ceilometer import sample from ceilometer import service 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_utils.py000066400000000000000000000143671513436046000300250ustar00rootroot00000000000000# # 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 oslotest import base from ceilometer.publisher import utils 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/publisher/test_zaqar.py000066400000000000000000000106271513436046000277760ustar00rootroot00000000000000# # 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 oslotest import base 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 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()) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_bin.py000066400000000000000000000077541513436046000254420ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_cache_utils.py000066400000000000000000000100371513436046000271410ustar00rootroot00000000000000# # 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 oslo_cache.backends import dictionary from oslo_cache import core as cache from oslo_config import fixture as config_fixture from oslotest import base 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_declarative.py000066400000000000000000000031311513436046000271360ustar00rootroot00000000000000# # 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)"), ]) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_decoupled_pipeline.py000066400000000000000000000163071513436046000305150ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_event_pipeline.py000066400000000000000000000335461513436046000276760ustar00rootroot00000000000000# # 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() ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_keystone_client.py000066400000000000000000000140631513436046000300600ustar00rootroot00000000000000# 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 oslo_config import cfg from oslo_config import fixture as config_fixture from ceilometer import keystone_client from ceilometer import service as ceilo_service from ceilometer.tests import base 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([], []) @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('keystoneclient.v3.client.Client', autospec=True) @mock.patch('ceilometer.keystone_client.get_session', autospec=True) def test_get_client(self, mock_get_session, mock_ks_client): mock_session = FakeSession() mock_get_session.return_value = mock_session mock_client = mock.Mock() mock_ks_client.return_value = mock_client 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") result = keystone_client.get_client(conf.conf) mock_get_session.assert_called_once_with( self.CONF, requests_session=None, group=keystone_client.DEFAULT_GROUP) mock_ks_client.assert_called_once_with( session=mock_get_session.return_value, trust_id=None, interface="internal", region_name="expected_region") self.assertEqual(result, mock_client) 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_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.assertEqual(result, 'auth_token') ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_messaging.py000066400000000000000000000051621513436046000266360ustar00rootroot00000000000000# 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 oslotest import base from ceilometer import messaging from ceilometer import service 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_middleware.py000066400000000000000000000077651513436046000270110ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_notification.py000066400000000000000000000204341513436046000273460ustar00rootroot00000000000000# # 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)) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_novaclient.py000066400000000000000000000242001513436046000270150ustar00rootroot00000000000000# 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 glanceclient import novaclient from oslotest import base from ceilometer import nova_client from ceilometer import service class FauxImage(dict): def __getattr__(self, key): return self[key] class TestNovaClient(base.BaseTestCase): def setUp(self): super().setUp() self.CONF = service.prepare_service([], []) self._flavors_count = 0 self._images_count = 0 self.nv = nova_client.Client(self.CONF) self.useFixture(fixtures.MockPatchObject( self.nv.nova_client.flavors, 'get', side_effect=self.fake_flavors_get)) self.useFixture(fixtures.MockPatchObject( self.nv.glance_client.images, 'get', side_effect=self.fake_images_get)) def fake_flavors_get(self, *args, **kwargs): self._flavors_count += 1 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 def fake_images_get(self, *args, **kwargs): self._images_count += 1 image_id = args[0] image_details = { # NOTE(callumdickinson): Real image IDs are UUIDs, not integers, # so the actual code runs assuming the IDs are strings. 1: ('ubuntu-12.04-x86', dict(kernel_id=11, ramdisk_id=21), dict(container_format='bare', disk_format='raw', min_disk=1, min_ram=0, os_distro='ubuntu', os_type='linux')), 2: ('centos-5.4-x64', dict(kernel_id=12, ramdisk_id=22), dict()), 3: ('rhel-6-x64', None, dict()), 4: ('rhel-6-x64', dict(), dict()), 5: ('rhel-6-x64', dict(kernel_id=11), dict()), 6: ('rhel-6-x64', dict(ramdisk_id=21), dict()), } if image_id in image_details: return FauxImage( id=image_id, name=image_details[image_id][0], metadata=image_details[image_id][1], **image_details[image_id][2]) else: raise glanceclient.exc.HTTPNotFound('foobar') @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': 'raw', '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': 'raw', 'min_disk': '1', 'min_ram': '0', 'os_distro': 'ubuntu', 'os_type': 'linux'}, instance.image_meta) def test_with_flavor_and_image_none_metadata(self): 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) def test_with_flavor_and_image_missing_metadata(self): 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) def test_with_flavor_and_image_missing_ramdisk(self): 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) def test_with_flavor_and_image_missing_kernel(self): 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) def test_with_flavor_and_image_no_cache(self): results = self.nv._with_flavor_and_image(self.fake_servers_list()) self.assertEqual(2, len(results)) self.assertEqual(2, self._flavors_count) self.assertEqual(2, self._images_count) def test_with_flavor_and_image_cache(self): results = self.nv._with_flavor_and_image(self.fake_servers_list() * 2) self.assertEqual(4, len(results)) self.assertEqual(2, self._flavors_count) self.assertEqual(2, self._images_count) def test_with_flavor_and_image_unknown_image_cache(self): instances = self.fake_servers_list_unknown_image() results = self.nv._with_flavor_and_image(instances * 2) self.assertEqual(2, len(results)) self.assertEqual(1, self._flavors_count) self.assertEqual(1, self._images_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) 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_polling.py000066400000000000000000000070771513436046000263340ustar00rootroot00000000000000# # 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_prom_exporter.py000066400000000000000000000450651513436046000275740ustar00rootroot00000000000000# # 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 oslotest import base from unittest import mock from unittest.mock import call from ceilometer.polling import manager from ceilometer.polling import prom_exporter from ceilometer import service 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' } ] @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)) 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/test_sample.py000066400000000000000000000107761513436046000261510ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/volume/000077500000000000000000000000001513436046000245535ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/volume/__init__.py000066400000000000000000000000001513436046000266520ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/tests/unit/volume/test_cinder.py000066400000000000000000000362341513436046000274400ustar00rootroot00000000000000# # 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.volume import cinder VOLUME_LIST = [ type('Volume', (object,), {'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_LIST = [ type('VolumeSnapshot', (object,), {'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_LIST = [ type('VolumeBackup', (object,), {'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_LIST = [ type('VolumePool', (object,), {'name': 'localhost.localdomain@lvmdriver-1#lvmdriver-1', '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'}), type('VolumePool', (object,), {'name': 'cinder-3ceee-volume-ceph-0@ceph#ceph', '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'}) ] 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=VOLUME_LIST)) self.assertEqual(1, 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"]) 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=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) 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=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) 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=POOL_LIST)) self.assertEqual(2, 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) 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=POOL_LIST)) self.assertEqual(2, 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) 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=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) 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=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) 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=POOL_LIST)) self.assertEqual(2, 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) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/utils.py000066400000000000000000000032431513436046000226370ustar00rootroot00000000000000# 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 ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/version.py000066400000000000000000000012111513436046000231550ustar00rootroot00000000000000# # 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') ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/volume/000077500000000000000000000000001513436046000224325ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/volume/__init__.py000066400000000000000000000000001513436046000245310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/volume/cinder.py000066400000000000000000000175671513436046000242700ustar00rootroot00000000000000# # 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 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', 'os-vol-host-attr:host', 'migration_status', 'attachments', 'snapshot_id', 'source_volid'] 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 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=getattr(volume, 'os-vol-tenant-attr:tenant_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', 'os-extended-snapshot-attributes:progress', ] 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=getattr( snapshot, 'os-extended-snapshot-attributes: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=getattr( backup, 'os-backup-project-attr:project_id', None), resource_id=backup.id, resource_metadata=self.extract_metadata(backup), ) class _VolumeProviderPoolBase(_Base): def extract_metadata(self, obj): metadata = super().extract_metadata(obj) metadata['pool_name'] = getattr(obj, "pool_name", None) return metadata class VolumeProviderPoolCapacityTotal(_VolumeProviderPoolBase): @property def default_discovery(self): return 'volume_pools' def get_samples(self, manager, cache, resources): for pool in resources: yield sample.Sample( name='volume.provider.pool.capacity.total', type=sample.TYPE_GAUGE, unit='GiB', volume=pool.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: yield sample.Sample( name='volume.provider.pool.capacity.free', type=sample.TYPE_GAUGE, unit='GiB', volume=pool.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: if getattr(pool, 'provisioned_capacity_gb', None): yield sample.Sample( name='volume.provider.pool.capacity.provisioned', type=sample.TYPE_GAUGE, unit='GiB', volume=pool.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: if getattr(pool, 'provisioned_capacity_gb', None): reserved_size = math.floor( (pool.reserved_percentage / 100) * pool.total_capacity_gb ) max_over_subscription_ratio = 1.0 if pool.thin_provisioning_support: max_over_subscription_ratio = float( pool.max_over_subscription_ratio ) value = ( max_over_subscription_ratio * (pool.total_capacity_gb - reserved_size) - pool.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: yield sample.Sample( name='volume.provider.pool.capacity.allocated', type=sample.TYPE_GAUGE, unit='GiB', volume=pool.allocated_capacity_gb, user_id=None, project_id=None, resource_id=pool.name, resource_metadata=self.extract_metadata(pool) ) ceilometer-25.0.0+git20260122.52.0ff494d01/ceilometer/volume/discovery.py000066400000000000000000000045411513436046000250170ustar00rootroot00000000000000# # 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 cinderclient import client as cinder_client from oslo_config import cfg from ceilometer import keystone_client from ceilometer.polling import plugin_base SERVICE_OPTS = [ cfg.StrOpt('cinder', default='volumev3', help='Cinder service type.'), ] class _BaseDiscovery(plugin_base.DiscoveryBase): def __init__(self, conf): super().__init__(conf) creds = conf.service_credentials # NOTE(mnederlof): We set 3.64 (the maximum for Wallaby) because: # we need atleast 3.41 to get user_id on snapshots. # we need atleast 3.56 for user_id and project_id on backups. # we need atleast 3.63 for volume_type_id on volumes. self.client = cinder_client.Client( version='3.64', session=keystone_client.get_session(conf), region_name=creds.region_name, interface=creds.interface, service_type=conf.service_types.cinder ) class VolumeDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover volume resources to monitor.""" return self.client.volumes.list(search_opts={'all_tenants': True}) class VolumeSnapshotsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover snapshot resources to monitor.""" return self.client.volume_snapshots.list( search_opts={'all_tenants': True}) class VolumeBackupsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover volume resources to monitor.""" return self.client.backups.list(search_opts={'all_tenants': True}) class VolumePoolsDiscovery(_BaseDiscovery): def discover(self, manager, param=None): """Discover volume resources to monitor.""" return self.client.pools.list(detailed=True) ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/000077500000000000000000000000001513436046000205775ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/README.rst000066400000000000000000000017211513436046000222670ustar00rootroot00000000000000=============================== 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``. ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/files/000077500000000000000000000000001513436046000217015ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/files/rpms/000077500000000000000000000000001513436046000226625ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/files/rpms/ceilometer000066400000000000000000000000301513436046000247260ustar00rootroot00000000000000selinux-policy-targeted ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/local.conf.sample000066400000000000000000000026661513436046000240320ustar00rootroot00000000000000[[local|localrc]] # Common options # -------------- #RECLONE=True #FORCE=True #OFFLINE=True # HOST_IP shoudl 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 placment 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 enbaled 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 ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/plugin.sh000066400000000000000000000344511513436046000224400ustar00rootroot00000000000000# 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_COORDINATOR_URL%%:*}" == "redis" || "${CEILOMETER_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 # we need to git clone manually to ensure that the git repo is added # to the global git repo list and ensure its cloned as the current user # not as root. git_clone ${GNOCCHI_REPO} ${GNOCCHI_DIR} ${GNOCCHI_BRANCH} pip_install -e ${GNOCCHI_DIR}[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 cache backend. # NOTE(cdent): This currently only works for redis. Still working # out how to express the other backends. function _ceilometer_configure_cache_backend { iniset $CEILOMETER_CONF cache enabled True iniset $CEILOMETER_CONF cache backend $CEILOMETER_CACHE_BACKEND inidelete $CEILOMETER_CONF cache backend_argument iniadd $CEILOMETER_CONF cache backend_argument url:$CEILOMETER_CACHE_URL iniadd $CEILOMETER_CONF cache backend_argument distributed_lock:True if [[ "${CEILOMETER_CACHE_BACKEND##*.}" == "redis" ]]; then iniadd $CEILOMETER_CONF cache backend_argument db:0 iniadd $CEILOMETER_CONF cache backend_argument redis_expiration_time:600 fi } # 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" if [[ -n "$CEILOMETER_COORDINATION_URL" ]]; then iniset $CEILOMETER_CONF coordination backend_url $CEILOMETER_COORDINATION_URL iniset $CEILOMETER_CONF notification workers $API_WORKERS fi if [[ -n "$CEILOMETER_CACHE_BACKEND" ]]; then _ceilometer_configure_cache_backend fi # 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. iniset $CEILOMETER_CONF service_credentials auth_type password iniset $CEILOMETER_CONF service_credentials user_domain_id default iniset $CEILOMETER_CONF service_credentials project_domain_id default iniset $CEILOMETER_CONF service_credentials project_name $SERVICE_PROJECT_NAME iniset $CEILOMETER_CONF service_credentials username ceilometer iniset $CEILOMETER_CONF service_credentials password $SERVICE_PASSWORD iniset $CEILOMETER_CONF service_credentials region_name $REGION_NAME iniset $CEILOMETER_CONF service_credentials auth_url $KEYSTONE_SERVICE_URI _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)" $CEILOMETER_BIN_DIR/ceilometer-upgrade 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 } # 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 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 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 ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/settings000066400000000000000000000053731513436046000223720ustar00rootroot00000000000000# 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 GNOCCHI_DIR=${GNOCCHI_DIR:-${DEST}/gnocchi} GNOCCHI_BRANCH=${GNOCCHI_BRANCH:-"master"} GNOCCHI_REPO=${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 overrided 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} # 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:-} # Cache Options # NOTE(cdent): These are incomplete and specific for this testing. CEILOMETER_CACHE_BACKEND=${CEILOMETER_CACHE_BACKEND:-dogpile.cache.redis} CEILOMETER_CACHE_URL=${CEILOMETER_CACHE_URL:-redis://localhost:6379} 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: ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/upgrade/000077500000000000000000000000001513436046000222265ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/upgrade/settings000066400000000000000000000007341513436046000240150ustar00rootroot00000000000000register_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 ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/upgrade/shutdown.sh000077500000000000000000000011451513436046000244410ustar00rootroot00000000000000#!/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 ceilometer-25.0.0+git20260122.52.0ff494d01/devstack/upgrade/upgrade.sh000077500000000000000000000044711513436046000242220ustar00rootroot00000000000000#!/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 "*********************************************************************" ceilometer-25.0.0+git20260122.52.0ff494d01/doc/000077500000000000000000000000001513436046000175405ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/requirements.txt000066400000000000000000000002761513436046000230310ustar00rootroot00000000000000sphinx>=2.1.1 # BSD sphinxcontrib-httpdomain>=1.3.0 # BSD sphinxcontrib-blockdiag>=1.5.4 # BSD reno>=3.1.0 # Apache-2.0 os-api-ref>=1.4.0 # Apache-2.0 openstackdocstheme>=2.2.1 # Apache-2.0 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/000077500000000000000000000000001513436046000210405ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/000077500000000000000000000000001513436046000221305ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/index.rst000066400000000000000000000010041513436046000237640ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-best-practices.rst000066400000000000000000000023221513436046000276010ustar00rootroot00000000000000Telemetry 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-data-collection.rst000066400000000000000000000277001513436046000277420ustar00rootroot00000000000000.. _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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-data-pipelines.rst000066400000000000000000000225761513436046000276050ustar00rootroot00000000000000.. _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 `__ 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-dynamic-pollster.rst000066400000000000000000001147341513436046000301720ustar00rootroot00000000000000.. _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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-events.rst000066400000000000000000000145641513436046000262100ustar00rootroot00000000000000====== 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-measurements.rst000066400000000000000000001221251513436046000274050ustar00rootroot00000000000000.. _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 | +--------------------+-------+--------+----------+----------+-----------------+ 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 | e 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 | +---------------+-------+---------+------------+-----------+------------------+ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-system-architecture.rst000066400000000000000000000044251513436046000307030ustar00rootroot00000000000000.. _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 `__. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/admin/telemetry-troubleshooting-guide.rst000066400000000000000000000015511513436046000312160ustar00rootroot00000000000000Troubleshoot 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/cli/000077500000000000000000000000001513436046000216075ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/cli/ceilometer-status.rst000066400000000000000000000037371513436046000260240ustar00rootroot00000000000000================= 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/cli/index.rst000066400000000000000000000003401513436046000234450ustar00rootroot00000000000000============================ Ceilometer CLI Documentation ============================ In this section you will find information on Ceilometer’s command line interface. .. toctree:: :maxdepth: 1 ceilometer-status ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/conf.py000066400000000000000000000235311513436046000223430ustar00rootroot00000000000000# # 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 shat 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', ] ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/configuration/000077500000000000000000000000001513436046000237075ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/configuration/index.rst000066400000000000000000000016051513436046000255520ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/000077500000000000000000000000001513436046000234125ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/1-agents.png000066400000000000000000001415711513436046000255500ustar00rootroot00000000000000‰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µ6pÄł±î´ő}ŰËŻýĽ|~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ł^^Ż—ëŕţ €xGÄ#n ˙ĺg?űŮĎ(89wîś*++;őÚo~ó›ňx<ş"¤O?ýTׯ_°‰v\ń€xGÄ#nOŚ\FȢGQLÜź=¦®ćOş~ĺK*?ĆĎxâČőŔőŹńH<Ä#â· ’ËYLÜźkě¤1AŹą¨K6ąě_Śzŕ›’¤ËţH…ŔőŔőŹG€x@<â¶Cr覊`ü_=`ţ› ëë â‘xGÄ#n7¬ąŚ *++Í5—cľĄˇ-‹ńéźtýÓzIRBB€XsąŁŠ`Ä_ UKĂ }y­Ik&q=p=Ä#@<Źń€xÄíä2‚"ą|k ®€xGâ ʏ]‘\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\cSë†çÎ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śŻXa^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ÎXgۦ4Úâ’hSö±>™qż-ݬŃCϬѲŤ{}îď­›6źŐŚ×›m4Łžčh‰3»vŹőgˇîG±xNšyť/۸ϱkŚ4űýůýÜÍç”˝z»Yw9]ź»JŽŰÖ}N˘"Ă|>_öšíšńâzíl{ă|ŠËÎ*{őv=ôĚÇ:Ř:0Şřč9ź~]TdYg•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ŢxxěÖëÚ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‡ĆAf˙ü *>©Ó ˙®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ł# €xGâpŁújŽžżŇ‡ßăóß®M I—?o$â°H.č4cJÓ¨a‘ćŽô鏌1§6pé–ŢßR,µnŢ€xGâpĂŘXsćĂ÷ë“ǬGż+éÖgĽşç¤ŞŰ’Y3Ű’dG` aC?ťfL]ňß±wŢ´űőęžSúeqE§v±ŻľÚ ÷ţP«W÷ś”Ô:µÉN €xGâծíÉ,cö€á™i÷ë˝?Ôšł śF9^ţĽŃŚ9%|b>č™ůđ=J3”G`Ŕ!ą  S¬Sšžů®oçů™ď¶vžË/Őéčů+ć¨-ďýˇVa›ţť!áwé_^ťĹ´&€xGâ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’ĺ-lu¶c9 E5»- W,×ün†µ $łŞFr {S$ˇăTČíĆkyKČ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Ľ‘3X}}ýű»|ů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‚Mgëł\ŠţË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×deÂ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€‹źp130 –. Â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[[k03„ËŔ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ÝşôJÇ믿>–-[–ţ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´µµĹ–-[â±Ç+şĎbq±Ě…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ťĘeaÝÝݱ˙ţ4Xţđ˘_ź1.Ě-*—`uwwGggg^°|oďr{|Ú€ňřăŹÇÁÓŰW]Qw߸(jćá2Ě€l6GŽIo_uEEÜ»qa,ľĚŘ07 —ŕËëk"îľqa,ľ¬˘čúŹ?ţ¸AyâÔ©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ĂekjkkcÇŽńÝď~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§ëß|}Ţ_źżů›ż‹/ŽÎ÷żÜHfk„Ë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Ďeg†Ź‹çżóť¸á‹_ŚšşşŘúý˙?éě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Óiqőqűu ăĹcďD¸,ľ¬"ţy­9`:„Ë0EÓm‘ŘĽnAl^g.]ćá2śĹtÚ`Ŕ\'\€IL· ĚuÂe(á\Ű`Ŕ\&\€Ú`ŔŮ — ÇÉ“'ăĐÎB¸ 9ş»»ó‚ĺ«–FÜŰp™€~Ű 92™L,^Ľ8˝ýĆéď=&Fß38C¸ đ˙·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'c2p€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·ň̉×).jN¶üť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˛±†#ě–$ŮSj{ĺő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É3q¨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¬×˛;ú,L0A)))ý¶{™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ÍM0Á!aĚNîék4\Ž´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ŐĆwz65Ôëźü@’Tx°‘Ůs¨č9óöď˙{ ‚!ÇCż˙ăžĹбM zÁ2ĐuĚ\€ ÜXH#)’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"nGYAw Ü€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¶‚źűm7łíť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ąě“ŮŹÓÇłpÍ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â»ŔŕcA5Aw Ü€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ň\ĐkoU3Ň’µ~éě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;ĘHcô/Ç߲ưŕ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ĺ‚îŰqLzł ›·µ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´ľĚRFSsKŘě"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^sEz‡Äř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ś0buž‚ľ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Ë×னÍëö÷ž0b§×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ÉÇ:ĽŮŮŮJHHLt«Ýëő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®ÁÝHA“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¸ qpâá.Ä!Â]C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]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套^’Ăá`"%´eC~żźIwĽ^/“Ŕ(˘r7ÎMëűBßřú+&Ŕ„tuĘ}9u*Ŕ ÜŤs˙‹ß/[0ČD:ÓÓŐý­t&€1@[C„»‡w î@"Ü€8D¸ qpâá.Ä!Â]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Ăĺ,)”ß÷AdjŃ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ąr7nyÜ­´`Ő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ÇÝ łňň˘*Ś WAu·µ™Ż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둲ň°°ËëŐ‡ż~[ą7hNAléiJv:uŃçS—×{ýžÜf 8Ô±NµŰ•ó׺ď®.Z˙lÄÜvy˝ú¸ľŢśŰX­-=MWA|ň©ŞŕţcväĺÉ=8L¨C(ŘýđíQ Ř…ľ÷z3ź•·8"Ü Żň>±ýżDUIw{˝ć_8XYş5´-Ö÷ńicŁ9/ßyîű–aaĚË?Ż[q|¬yąńěĹţ.g36‹őŢĘ˝säŤuţđq…·ue~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·ÓirWAł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.744AhMMMTkqq±\.—>ůä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˝fkÜ[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łoCCjjjäń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ŕ›ýPjßء ]~-~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¸ qpâá.Ä!Â]C„»‡w ý˙ÖAí™6»8IEND®B`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/2-2-collection-poll.png000066400000000000000000001002171513436046000275160ustar00rootroot00000000000000‰PNG  IHDR/„ć4ĎębKGD˙˙˙ ˝§“ pHYs  šśtIMEß ! i¤‡C IDATxÚěÝ”Őĺ}/úţH:SęĐ$˘h ÓŢäjlŔäôfzOďQđ$zcn0±bW LWoXe%ĺGNF#žjî9§3“Ő›¨!uL´Úfʧ b´ŚEĆ 11r˙ľ›ďžŮ{Ďža~|×k-×r†ůůĚţ|źçyźďóŚ9|řđáČS4EÂ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».Ú›ż‹^–ďłň}ŕ‘őIYl[˘ţěĚíÉ·˛Ŕ¦ż.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ŃmÄłß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Č1P¤Ňßůʦ»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É×ęë&ÇłßRđX\ú‘ę†y3ăŮ +FuEÓă÷6ĺď8ílďČ˙l×ĚźŹßŰTôw›:ibÁçĄ;ýąÔĹă÷6•=ŐqŐ’…źŰÓ!ě)čż·©`ݰôÁH3¦×Ć÷ďm˛ŚŇ‹ ę´ŇÉȵ Ç÷SŻë¤^“Ď5óçÄłß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Ťi7sŔ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çË˙+}BCző¨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ÜpFڏçž{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‰Îî}{ęňĹ_tT( .{ŽŐ{«†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$˝µĂÁŐ% ˛<ł–{\Ľ·©żQxVGG‡'` /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 łS4EÂ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 ý…ĘK0GŽ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’•ńČbMwźţ¸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[λďŰďĽ¶Ş˛Öë5w“7*;ź›áמ]WÚ{;|•«Ćő+öď—$ĄÍ›§ŚĹ‡<ß—q.z—Ń&ľsçNóŘ­ŃŇŠ5k˝ěŹ5/_MŤţĽ}‡ţaůŻ{·lVCyąąű¸;y©ŰcŮu ĘËUňÚ/4ăéźih|Ľ˛Ö=tNĹţýfv=Ž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ŔMjmmmeĐ—š››•źź`ţýŹ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”Ş× 72zĹÓ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Ňćg7,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…ŕGEmAgÇąĆ:”]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*ÔëĎ!ťŃâ}o5 ‹ ? Ő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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/2-accessmodel.png000066400000000000000000001257661513436046000265620ustar00rootroot00000000000000‰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‘RaADDDDDDDJ…a)5VUŐŢ˝{qćĚVQ6|řpôęŐ‹AD -3¨Ę˘ŁŁY DDuÜőë×Y D¤0Ř2ŞŤ0;F/^°"ęcc44XD¤PfQµfgĂňy<+‚¨IŐŐeAD §Ě0ýź‰ę>kkk,]ş”ADDDDDJĄĚ13dŐ}±±±¬"""""R:oífŇĐÂVV¬)˘:$2.ŹăŮ€”Ó[Ă ++ôsqaMŐ!§ÂÂf‘ŇâÔ¬DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RaADDDDDDDJ…a)†DDDDDDD¤Tf‘RQcŐéééرcŕłĎ>ŽŽ+…ę¶Ě "ŞC˘ŁŁqóćMÜĽyß˙=ŇÓÓY)DDDDTç0Ě "ŞŁbcchQťÄn&DDu8Đ?>»ś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Ć»Ŕ0h0Đ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ěşwP[»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ČĚÎFÄłgîă ü´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镸_äąghPMSÓÔ2łŞTŽeçÂ÷Ă€™ëźÖćN­+\¶é= —]v`aw q+YJWÚóç˙˙ú˘0˝ä:uí=ÇŞ–÷´ÄqËŁQżţ(ČĎÇ“Ŕ ő„üÜ<4ę×ď­eŚ={FęĎ  Ţłm¦OP[‘Ç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ď^PUWÇł˙E^vůťć‹D:{*B!öúżDDÄ0ă]YúóĎŚŠÂĐ>}0 {wŔ޽ѿkW<ŠŽĆŇź–Ú^<&‚p0rD"$ľx4t(´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 3Hn%g6ńôôdĄŃ;Ç0ä"Ď­DDDDDďĂ ""z+DDDD¤HfQąd‘˘aADDebADDDDŠ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ßţö x1Č """"*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Ć 2aUŻ'±±’€@_W .½[7aČěŮONtiÓŽ ŕţ“'8rć Fôë'UÖ™+W›€čääTˇ˛ßfËţýČÎÉÁ2OO,ś8Q˛ÜŢÚ 'ND~AVúůáw¬™;W˛^ ^Ą¦bĘđáTÔÝDŰÂľsćŕôĹ‹¸t㼧L‘śŹm±už<áM˘^&ÇáäţźYJhÄäer 2HŮ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řě/‘’cADDDDuĂŚZ4qńb8ô뇫wî”Z'3®ÄŕťŔ˙·Ŕ8xú4v :yذj)[,77WňsŹöíëwí’ąmŕĄKp2>7ň˘)0DDDDTW0̨EONĆ„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·{óć +źęŚşd1Đ "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‘ŠĚ… 3H)%''ăĹ‹’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Ë–-ĽaH!ŚܸqŁĚn#ő„BihŔP]ćšš .‘†F©)ŮŮH, 9˛˛‘—' 7BCC  p€ŃÎť;ĂÉÉ©Fş¤¨ÔtQňAALÜ˝ŕîÝ»ĄÖť>ź|ň ôőőaii‰5kÖâââ0lŘ0ŔĆĆß~űm©2ž>}ŠiÓ¦ˇAPWW‡ˇˇ!ÜÝÝ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»ŘµóćÍĂďż˙Žű÷ď+fq÷î]¤§§ŁqăĆ044”ąÍŢ˝{Ž©Qňa˘^˝zظq# §§‡eË–víÚ%µíŐ«Wm۶•, /µL܇'66kÖ¬ŤŤ 6lAř˙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{ÔjQV“‚‚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ÔHqĺĘŔü@ ůŁ˘˘‚úőëăĚ™3X»v-._ľ, *Ę’źźŹ§OꀤKBya†řˇXVŔŃ˝{w©×?yňĐ´iS…ł@·nÝJ‡şşz•”ĹÇĐŁG©ĺâ$łärńâc;tč€Ň-Y@Łh@–ěěě źóŰęLĽ¬˘ű—9~üř2E»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ˇPGGGj­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ꤼcgąé®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@şşčŢ˝;ţůç {÷î’ß_ˇPnÝş!((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¤$ńśŢŢŢ>L0&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č‹ĺ®gADDDDDDR®˝|‰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!˙ŢľÍďjb\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”988ZlcŹâłĹ{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,ÁśBGÂ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ěر:taƱ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_ŞPadlú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 tqż˛Ňú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Ő>Đ(OqôŹŤdZgűöíěÚµ €·?^üČÎ{ćŘ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!„wIQAqqq( śśśŞE˘˘˘P(ŘÚÚĘ8 !ÄV] 2„B!4IQA—.]Ŕ××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ŽäôéÓŘŮŮáĺĺ%ß˝BZhH!„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Ć´xpż€˘"Ç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>ůDBJ˛łł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ů€Ńş—BG˘ał¶h'ž,3B!„¨„sçÎ``b‰Ł·ź â‘iج-ź{%'âÉ%a†B!D%\˝zk÷F2BGN ń¤“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¶,›ĆüßIg×ú%\: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„BGhF=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ťÇ†Ç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ˇ78z3?¬¤ĺ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ţůżňŃ‹-HNeö–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~âŻßHWĎĘ(şü´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ţ°:^wS“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ăuuq'<{¬ę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’$$BA,$´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ő‡Ź]&ČŔXG0§Ž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źý˛ćŘ+ëł_VhARO‡ĆöíŰ)ř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ť=˙Wn Ś&™™™*//ď×Řşş:ą\.ť:Ń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Ś’““ 1i&  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áf5K’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™Íj2Ą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ĹÇÇé´‘ËEQV«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«•"`PL&Š#ŚÎ 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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/3-Pipeline.png000066400000000000000000000614611513436046000260350ustar00rootroot00000000000000‰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’*>ÎéPsí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łŮ¸}vME11Đ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׍—5Äł{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çꕵ§¬[žÇ¨ÄřůÖÇłĚ;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Ô! Şü%¤şwtď 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ţůdžŽ+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Ú°arrrdłŮ¦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„TNvű“ Ę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„TIAőî~źU P-–ĹÚşőE € 3ENNNÄ*A@"ÔŠŠ—éä `Nşăúőë×)€Ů¬żż_»wď’·ë?Tö˝µ˛­ĘŁ(ćśęĂŤŞ;q^………Lń@H€ŮT;:.©ô©"?ĽŠ˘ďo Ľ× ćómT„TmAµŞŞ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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/5-multi-publish.png000066400000000000000000001233771513436046000270750ustar00rootroot00000000000000‰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“’ëF8®v*ďjGżÇ]WŰeďąöX˝­ ßvŤ§éőLK$>ź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ô @I477«¶¶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ą$ť1Dš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ú)÷׫đĘIUmjkkŔ ·€Äçó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ŕ™ČŰ7n3ş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‘¦cpĺ«“ˇďÂŕÇŰ·#°ýĆ[űž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Ô[G†ă˝Óň§OÍםwܦew,QvV¦ą}ÍďßbX’)SzK§[ląc˙ŠšŽJo‘îy¨wIßĚÉRćÂŢ`äÝš@uEߪ‰—ľX!fő·Â+.>|;44•v6‚–‚™7>©˙öé§Ă+Zę^Ž|îŃĽ±+pžë~š őX7wB–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ÖĹŽÎ!6XĎ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$\hV¦× ‘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‚áE08ušÍ`IpzÍ˝÷ŢušŕkUUUĹ´RŤŰí6śá$k" @?Áé%“'O6‘5kÖhňäÉjooŹ©YkpűhÓl‚ÇV="Éě%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&ÖĄwU&Ѧż„ ť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“tb,Ź€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Ű©ű~}ůâń¸śúcTg!_˝¸\®ś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ĄRD"đűý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úôéśď‰`Ąű'"˘\ Hf)Ăëő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¤˘Ń¨¬·Ű źĎ‡`0SľbŢo·Ű‘H$䢮úçÂëőÂĺrɲŞ)Q×âY°ŰíňYP/´›ďţŨ•|÷ODDZ+±D9íţáđţűďľ¶çIěů˙aŃËpâ˙˙tâ8ŕOţäOđŤo| C´ţúŻ˙ŁŁŁ€UŢ?ĹĘ Ź°RS˙ű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?÷^}őUA|đÁ¬"˘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 HhY|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ÜTS?Ó,Ţ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¬_#Cbd§îâ­_ßťş‹ľ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ŇÉ_˝zA”Ç®/Ř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ŃLhÜ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ʏ» QaQIÚőĺ/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^ł HJ<$ů/ŻĽĘ‹ôŔ»9uWîz$Fk…(bĘĎbí4´Ó‹Î:¬«1šĘĺc)[‰Ż×V@AŁ‘!ÉROb˙ ß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&#Ď9WkŰ7aµZĺׯ:”óü©G%“ă%˙ď G-sb'śĄdôŢVĂD´´ełYÜĽy“Q †ĆFĽ°?~xä†ńúkßÇ;÷Fâ´´¶v€ó972";ÄĎú¦ďŰóä“°Z­śśÄĐń(€Ý{fľöă ¦ζúz¨˝}ôGČd2y;×ó˝č¶m÷CWľŚŻĘą†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#ęëë148H¤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 GJ‚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řIJĂ‘ĄĂR¶R^ĚeJŠ™ĆMČNÝĹ{gŻČ‘;ÖŻ‘Ű ?UżÉÉŰsş¶·Ć.Ă‘ľŃkš‘ękµŐ­3 #Ę9rg®L—ł®ň!´=şUĺehÜTS?Ó”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!"š_ HhY|@d8˛<µCĚŽjëjÓm„ŐĽ56åʲJî`3x93§r6n®@]ĄE¸čU–OÔLŢźcĆ'oăü˝PÇ[c×,ÚŞVWiA㦠ěXżUĺ,J´\ö1!"š_ HhY}@üÚ׾Ć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 HhI«ŞŞŔ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@DDDDDTD" .Ŕé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ąĐŰŰ ›Í†`0s?ííí°Űíčíí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Ô‡ꎹ"ÔWju,}>_Î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Ź#HhQ’¦¦¦śŽ|:ťÖěVcNh:ť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žbi0úĐČ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׫:ř×ÖשřúôéÓ9X4ŐŚ˘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Ń`0X,†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©}Äł®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ĂşVFĎ‹Q]GŁQTVVjžź| ´ęŰ^MĽ–N§5ŁG -uÓ((żÉÖŻ :«"<ŃźóŔĹbEýÖ?ßoŮE'Wż[‰čPŠ)ˇPHvng32è,ůÖ')t4†zVuyĚĄĚBqŁâŔH§Óš5eÄűŤ!ŤÇăŘ·ob±ŘĽ,*Ö ŃŹLRwüE#ęŘl]Ń®úzÓďT¤żŽzqÓbŰG_"¸0şŽŃýµg!#^Äu[ZZH$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ËhN·ţŁĐ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ă™±¬$“:óěł uon·«$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} G8©×ě¬Ë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˝ćŹ|T8{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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/6-storagemodel.png000066400000000000000000001472011513436046000267550ustar00rootroot00000000000000‰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Ť?ž; €čUD^^ž’’’”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’¤Q4ućO©lŠ4T“ŻHdźä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 Ńô[úé 7QMxé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×ĚýˇtV«UIII’¤qŁčŃÄŹť(I*((Đž={(H73yyJ@čP ‘””$›Í&I3‘Ţ=ÁŕřŃ2MŽëîG(g®CDă°żő‹¤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ěĘ? jLfoŞÖCE ŚŐ‰ô#ĘĚĚěô1.Ľě§ ěA1[` i_]č)mkW ‘‘‘!I ĺAµ' Ź˘oµQRçç‘ěˇ8Šy†% uí˛ŃŘťß/0Šő`~'ŻO{&bD÷břś^›Drr˛ăĎ–p*Ö5]Ž5==ť‚ô„в6‰üüü“Ľôčé,!a’$zB h®cD“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Śó*!1c%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|ô¨ÖÝ~‡.¸ď> Ľü2yLzçę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ĽY5p°Ž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âłÖĹxTfĚĐ 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őęŐNÇ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µŞÂÓSBŚF }vEOŐŘł˘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ĂĘ“MF§†ůAAÚť•ĄęŻżÖ”)SZ=>€sB~PŞţŁ9îryšĚôi~eVŤ<đťň‚”"›Ů¬ęúz)/בňrxxh°ŹŹůřĐkN—•)ŁĽ\ňě×OaĘŢľĂ)8íďĺ– ĄźČRib"€ľˇ8 @’d0ű+|Äd ÎyÁÁÁš5k–v˝úŞŚUŐ§ÝÎRP KAJ|Ľ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Č´âÇ»%IGś×ě˝Ř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ĂÓ·Í9h>É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Ó˨_"JHHC&<ţ"Ţ~$¬_É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¶śŞ•|}}™4iYzR:Ż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ŠâÇłgńŻ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ä†ńÚúgfIĚ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ÝJHH§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#iwJHH»SBBDDDDDDDÚť"""""""Ň&RC‚Ywô(Ë—/o˛¬&µ‘6awwÇn+ŔĂjm˛¬"""ĹŻý~<•ŰřŞ€Chh(3fĚŕÓéÓ1Ű ét”iŐ«WłnÝ:†?üúoĎŐdƧg˘Ł|©Ĺb±Í–›‚!ť’ć‘v§„„´;%$DDDDDDD¤Ý)!!"""""""íN iwJHt`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 iwJHH»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 iwJHH»SBBDD¤Ë9s‚K_aÚ´i$&&* ""â’’Â;ďĽCrDvwwD:˙Ě,b{0bÄ&ËjŮO‘NČn·“śś ^ž”ąč»féüł˛Ňł'7ĹĹ5YVw­´;Ťi#FŕÜ›]'  ‘VPBBDD¤(uő$ą8GÁi=˛!"""""""íN iwJHH›(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š+×Ç‡Ś‹É/“ÉÄôéÓ[”Ś%$D¤ŐŹlŚ=š÷Ţ{ŹË™3g˙üó:evîÜÉ˝÷ŢKPPFŁ‘ŔŔ@îľűnľúę«:e‡ Fż~ý˙®o.‡ .đüóĎÓż, FŁ‘°°0|đA>\ďy65'Dcűßzë-śśś8xđ`­˛Ë—/× """ťNhh(111T śëÄÁÁ±śî†ÝÝ]’ËND yě1Xł†›¦Oăąçžă©§žÂb±\V}JHHłôčѧź~€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íÚµJHH‹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|||Ú¤ť^x3gÎĐ«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ÇŇW1cŃŃŃ×Ôűęlnq»­˛Qy†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ľů€űŢ:ŞGiHë‘®!&&¦Ţíś={¶ÖöŚŚ ćϟϦ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<1Lxâ›·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îÜÉÎť;‰`âĉJLH»Ůľ};+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‹ŽWB3g;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(ZSZŞÉ_¤ĂÜ‹""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ć,~řŕCZopy¦ˇ= šŔ@€źŮŚÄÎ÷ş§á,ÎBŘí˝| (;vˇ5f3>®¨Ŕ8™ Sˇđócˇ¨ĎŠt:śih€ľÍ%Aţő ).†Đngh@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ąŃ2eFŁŰc~f3ĆTTňň """"t•UÖÔąő4eF#ĘŚFČëë§P`ś\ÎQÔiOS¤ÓAoµbĹĉ0h4(řŕĂnź[ŹÜś“8ř ظq#‰ľ[,®`âJX(ęT!° ĐŰlČ×j‘ŻŐ"P(DśB0‰„áÄ(?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}}aIˇ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ľ_¨22Ł±Ý·ěf‘F©F™F© Vo» €+¤č*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…ł‰Ž¸ęqTęvrpťŕvˇĐí4]q^&Ň‘˛aö‡Í×jÄ o­Ź#lŕ d#/ł ˘ASPP€Żżü<HuĆ`0 °°đWÂÚÚ GΠ¤łž¦I!‡ĐfÇ•.úmB<ĐÁň«{›Paa‚P,Ať¶ g**şÝß”°pŚ ęî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Ă™8HQW^<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:>“oyaŃ ;|ř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ÄSJĄ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ďÚ{óčš-ŧĄĄőbQ&//[¶lÄĺçĂż‹)5©çccq÷űďÁÜŘ×§± ˝Tż?âăă±qăĆAyŤśśěر&“ 0fÂŚ›}ë ýŁ|ńđG°ZLüĺŽBÚębŔúő둜ś<˘Ţ[eŁ™łl‘程ŢńďD"ÁňĺË‘–6řź‰·ÍśĹ_yeOĂÔ+ˇÉÉ7ŇľĄÇyJMá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÷†Pikî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ČBTt×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……HI4 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…R3m×á """"""/TZZŠ—^z “aIY˘Ëʱ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öďߏňňrE"¬‰ĹĄŻż†ľşšĹĹ=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Ť‰‰ńřţ2D%%%(((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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/architecture.rst000066400000000000000000000153541513436046000266360ustar00rootroot00000000000000.. _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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/ceilo-arch.png000066400000000000000000002536301513436046000261370ustar00rootroot00000000000000‰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źÖ&¬­+–pJĄĐ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ËODDDuEaaaˇˇ)©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ů3AA€Ř+ĄÖ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>Gjď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…BTč.…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"Á A0gî<íí ~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¨:™zIDDDD•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Ô1J˝ ®‹˝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 ÉŚ jLM6aҲ’ł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~Ľň2hݨ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Á !Şep """""""ł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ÔŠqP8KQN6Ść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™ť@dPČŠ &¨ ý'?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ňYL8Qpë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ÓĆwJMIZ­†˝Đť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ÇŞ•?â~p0BBBp Ş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€Áűˇ_`wEv¬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~Aax’ ˘'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|†:žx0q´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¬śGph–;ôĹ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+‚Ě*1S=Žß3""jbŻ\Áą¨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Ő...µâxp ""˘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ý äIUCp """"˘:ÉŐŐÁÁÁ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©T8a<¶…†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*< M4D°łłĂ3Ďşí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—D7éóÉÉɸ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Ů_b5üĽť°=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 †ŕpXy(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đ î9QaÖ]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ÄÄÄRPC‹‰‰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©ąÔěóÁrnv‰‹*–Ú:č 5°ÔÖŃ…ÜNł5ŕ@ˇ˘`Řżeeeyűí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–†Ţ-{"¤#)¤"deDÄ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ÂBcٶ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{ďO>ý_ VřČd˛f ćńřvŹo EPź „„†µ;y1hň‹.ăř#.Óćz-X,¨Ő*:x’“ńö»ď^×vµ ĘKŐiFsJĘô¸\Q řwŐg肱1Ă16f8Çé 50™­˝g4:Rőj „!„Bc‰:Ě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× ňCd7ň/j;mĚxsłT8D¬n ňFBBÖ­[GOZ˝ąQ"ęS‚sL €Łv×Ű …'ŘWf -ČĎo´­úš/- ›r S„ ĹĹĐTT@«ŐÂ`04Ů»Š)Q\rň  R—C©ľšŘp9IĐč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µ€ŢˇŕĹ-; €uEĘ_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\ľíftž‚ü|Ű%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šz6v™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ű‡ó”וlKđ@56$'µşnţ™3HX˝ ÇŤĹüűća}r***¨8©‘Q‘XµüĚnP«aÓÎXôÂŰŘ´c?¨‡‰é/Ç×ë>Fjj*%!„Ň=Ö•(ćzS@zI“'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‰AC«Ńŕçź~ÂéÓ§ńů_´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áîFMP[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!ÄUuTHKmN[‡™Ś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!=TD6Ąü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Á „!„Bk»\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¨2m‰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Ëë˝dZ 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!© Ůe1cFŁö?räüq$t…B&ňčl ây2í€ZŽčfäB|ŐG"•âibíęU8”“jľćňŠ#YđUGA˘†OW1p»"C±ŐeE¨:~Đľü%ÄÄĆbČĐaHĄ ©ŠR©kWŻBeeeÍ$’GvÁ§c|:FÂ'¨-2r#Ľ<†Ľ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"##ˇ3aµ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Ĺ/śťśĽRR 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ďžÝöbO‡řöż±”Á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ˇĐ·ŤËd7­č°iăűś ˘Ä»Yl ""·–“ťŤ¬¬,Ć"§ ü†Ú0Y¬Đ–’’ҨýŤF#rssˇ”‰!ň‚6"WĂK*č:™Űě·E'°Řŕ%b)D'\9ľ 3 »R±ˇWŻ^ö˘Ă´iÓ´ěĺZm}â)aL'_%""·§×éPVZĄLĚ0G8е(ôú+—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‚)q7Áľ‚‚·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¬\ńKmŚ5}úÝŢbmŔW_|Ü`Ńť1sćÄfµ_đZ˝ŰŇűgĎśnVÎčwë_gôMCm8Łonf˙¶ĄL )ĚČĚČ`ÁÁAFúŇłČÉ>čŐ‡g缅QĂ1$ˇä2?€X,BŇ H4.ńűf Ňߌ]űrP]íŮ—±ŕ@níöö ś5q´Ü„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-ĂpQeĹ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+LJ%čÖن&âÁÉ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ŁAzzzmĚ1ŃŃŃ-ÖĆÍě_gôMCm8ŁoÜ­Ű7~~~řŕřKŤÜŽD,Ćüął1(ľďuŰä2?Dv Gd×pL¸k8V®ß‚˝ů!¬V ä¸pAĎ ZT"­ąˇ+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$“&ODĘoŮoÚƸ8ÄÄĆÖ*:\=q',[¶ ˙řÇ?ě? Ľţúë7n&Nśďż˙ŐŐŐ ŠÜžˇ˛ 9ÚrĚš5«QűkµZĚ›7qáAK={đ¶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ÚˇPI“&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?Ü Urą 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ľ–6J<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ÉÉ)čĆ@ę±}25ddlfŰ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¸ďľű5IRaż]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 A0nÜ8D"†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ÁÓét1cNž#""°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{qq125OHdXD7hÓ† ·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ë_Ŕq1jćqĆŔA0kö?šô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|üńǨ=Ô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ů\ÎRXXyóćą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´W0VÔ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*ĹČQzx`{\ĆŚ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=´ą%íźdK3©ç!ş@íç€Ŕ@Ŕňض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|úŮçĽip ""˘Ö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€ĹbH$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'ŇŻˇ3b©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~dr9DÄ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ęĺŰ á˝ŰěÇŽĹŔAx¶@жK–LÁ˝íâ|¸JŮĄšdCŻ^˝02¦ńńî{‡!,Đ!ť˝8ćę"€L*DZZš]ť—±ÔDlÇ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ŞT1kş(ĽŘÓČŽ˝ôâ ČČČ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]ŚĽ|;!<˘7ov”˘-Ä]÷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$ Ń-Şgj5ĆŤź™\Î Ń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]1E·„ÚĎŢ‚bĽűÎŰM–ýőČQD˘&ËÍž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ŞNtϽؽ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‹= ť»tX,†ĹbANN'&bĺ‡ *şe% …HŚŹď8Ťi«eĄĄ-zIV¦é©gp`Âţě$ÂS _/ń +DŐs)tŐř7lĐ!’ -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/…BM›·ŕ«Ő«±ß>\¸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÷:el°?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Ť7AD‡ """"â!vą¬zťŽÁčR’“P— peL"r@L8‘S4ić/\Ś„łl“sĐëtČÎĘP=™"µłĄ« ŚFb$’xpĹśv–śY€‰“&#**Ę.· """"GýßdbO'ľ¶»vîxJÜ”v¤3P)¦ĹÎh×{"K«µŰŐ8t”ꌌŐnç`4Q—ëX+–´u J-čâ…BaçĂ„‘ŞŞŞÂĆ ë±vÍj¤$'3 NÄl2aă†őČŐëV«2©¨C|v±Â5ŢX¶lo„ëôzľÝ°Ł)II8’p1˛#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";ÄŮgX"Á´Ř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ŽÄřxn9ĄJĄJ…ČčhM&deeA—“­6ąz=ĚW,4–lŘżď'ŤPuę±H ĄJ±Xě Ššefł z˝¦2ôzŚCťäďô0°ÁŽ?jŤ†ČÝÁ’ IDAT=Cnö:”ZpV‡¤ź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_cdGp """"˛×”L_?e‡Yň±ÄTäĚ,Y˛¤YĺµZ-–-[†pŤ7<ÄnNĄJ…i±3˘-dĹh@xD†DßÉŮ&Č.TVYa,µ ,,¬YĺKKK‘––ąTWWHdgp """"""˘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‹ŢŘđáűĐC1}˘‡ŢŽŰ·ăí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އs8Cä/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/BJ1oÁ<|*Ă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® ü͸(Ë9KU@ż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ˇÖ‘––ćÔń:ť[¶lJ‚ @>Ú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ȤAI! ôÉ+†®şĂ#6f¤ÁŰůŧO7ÎÎą ě˙ńÔŐŮĎbmPe¬…ˇ¦ĺ—-śĚp Yxc¡A[ •饄ĘƱĎŁŁŚ®H84űň. Iů…ÝůLJő×tlÝşR©'ňŻĆä9¬ ´Ufh«ĚŤ÷¦@˙ß&ÜÔăŕ/§mÇ)}«€XŐ„Iüńß_ŽŘ•Ó4ž]ŠŠB”˛Ż×$#,f3ĘĘĘeZ-,µh/]‚ĹbV«…L&Ăłç@«Ż€NWü‚b”éˡŐW ˙T1†©"ŕS-Śćz®0Ń µu PëŚxtŽs=ĂL&4 dŇ@Íő>=“©l=¤BE-Ď«2Ö6&-ję8ˇ#ä.QJĄíA¨ţh6üÄł—CoK8äďĐ8ź‡JĂ€t‘aÇă@n„25´%đS2Ö>›tP"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|ňş_ĚQQV©?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ŘśęëŇĚńÁÁÁ€Ë¤Őä1d#·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 """"""ę6qC"沽DDDDDDÔm,X€(Ą’čp ""ňz˝ŮŮ»]:G"– nđ`ŹÜŽ """ˇÓé°{×.—ΉR*±`ÁŹ<ÚÓĎ>‡ÄÄDÂË0á@DDDDDDÝfÇŽ0 D/Ŕ„u›í۶ˇŞŞ’čp ""ęĹ věŘÁ@‘Gűaß>čőzÂË0á@DDä# &LčŇ9UU•ŘľmGDDmßŢďˇÓé/Ă„‘ŹËĺ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Îdo´#“ D䬤¤$Ěž=ŰvźĆÂ… ]Zv·ąć˝L&–.] ŤFcű˝Ň|µśćÔj5>\˝ Ź/ZěÂý0o-_ÎF$""ŹöôłĎ!&z áeŘᫎţBÎdo´#“ D䪔”Ěž=PSSĄK—vz6mGÉţ^!""o·cÇ ‚ rĺauéŇĄL6řXŇÉ"ꩤ“ DDä«¶oŰ†ŞŞJ‚ rĺaµ°°_ }(éŔdőTҡ;“ łĹEEl8""ňh\“ >¬2ŮŕSI&¨'’ťI6( L0ŃĄşjË´X±bŤ<Ú§ëÖvz¨"1áŕŐ«÷Üs“ >”t`˛:Ł#I‡ÎölËĺxË- >yźH8D"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$!>>EEE6mÂĂá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Ó¦MJĄ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Ź<Úý<Ř«z0áŕâCíóĎ?Ź>řŔáţ;ďĽZ¬lđź˙üpűí·ŰX›x¸­IDATmoĆŕĘüŽ4Mtřý÷ßŰ=đ‹Ĺb»ä‚R©`ߣÁjµâő×_·ýÜ|ȇ3ősőš»Jvv6 55Ő¶­i®ŇŇŇÇ5—žž…B˘f“Ť) pX""""""*.*‚ĹěÚ˛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›ŚAaôčŃ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‰¬¬,Ŕý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—.ĹUcň”©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–,ABPPpçť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˙}L07Ţ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-fL&|˝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 """""""·spĐëőŘ“ťÍč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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/ceilo-gnocchi-arch.png000066400000000000000000003421231513436046000275430ustar00rootroot00000000000000‰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ٲĂۉ4u»ÝŁ*¨˝˝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"kh‚ĚcM ˛DDDDDDDDDDS„,ŃąźU@DDDł‘Őj…ŰídggĎéşhmm•Ďőş—ËĺBgg'@ŁŃ@Ż×łRhJ0%""˘Yٵµµµµ0›ÍĎĹĹš  ………HNNŽřş¨ŻŻGmm-¬VkŔsz˝(((@\\\Čr¬VëŚ #×­[(..ƶmŰ&Ľ|—Ë…wŢy&“ v»=ŕůěělŤFĚşýáěŮł¨®®ÔŐŐń`ADDD41%""˘Ďĺ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Ń,ÖÖÖ†žžĚź?ĚePöß˙ýßńÚ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ëVnY„,‘Š·ß~ÝÝÝ“ţÁ~Á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“Ď~ ÚtwYł 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ÎYn'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ťçLdi¨‘_őb}÷Ř>tßri˝j‚~•Q®ŰűçŹĘ1 G3áŘĺĘ]ň}×<÷3źí˛şh¬WMx÷Ř>o~|żjŕ%Žgjëýî±}ho¸0|«©iÚ&Zś•••čsŤA÷7yâ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{¶+Žwn§ü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°± €ÝăÁ Ú ŁTf<|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 š Ç`ű~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łščĺŻM6Ý€š´Ôť’Ď­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Ŕ„@D3ű‹:ś, 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+nNkń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)ŐÄHaH°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Ů4E,ŚőÎŻÎ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˘˝ťÔwwbr3xő diÜzdăż (T…/ßűl-ܸôn\úHą=0$s˙ţ {?ç""""˘ˇ˘V«ˇV«1žÚ|Ę•´2ąš°W–Ú¤¶€ş6E岧…ÄĽ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«Us0sqŽ 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†YdXKK"˘±,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Îθvprź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§sV+ǔ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ů+`bT5›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ťÄłűÎ 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żŐ69­“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ăőĚ}Ř„>ç<Ť("Qiá©`űű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ި­„´pYQ'° ňńť«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Ş'Îk0s_Ďť|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¬Sqh ü''*—»î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ňSqh „`„ű©Âť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ť6c¬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$Ć…1FC˝ĐĂ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Í ŁńwDç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]2dSkâ@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ŤîSfÇ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öß“aw6 ČŇÔ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쵝ś3gcÇâ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ŘxWĎ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„cÇaÎ%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ËĄĺň "KW*.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¬†<ĺ;nYS'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ú,ÂpvěŔŁ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ßĹÖ‚,Üé± ělDnŻ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;˛Ηó1JDDDڞDDÓ@­V#44tĆÖßĐŕz Ë`,Í5·˛ ł˝ZźV«Eee%Ö‘%""˘ŔĆ€,Ń4Ř´i“×—VŽçůçź—–Ś%""ź8U¶ŔŰ€,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čííĺ—BDDDu\Í& Ź;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ćYH€<ĺ;“˙1j6ŔÔÚÄË9FÇ€#ä ł]˛ß«â¤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ŃŠ/­řË<«Řçčîjo8vqHš4pdĆţŮ´žq”-P©T“~­s†ěTúËů‡üü|h4űDÍčĐĐPěرyyyürhĆ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Ů&#8FÇě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&˛AcŃÚµ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łAcѱ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Ť41cr0•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:ËAqÄł8שÎÎ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‚(Ś***đŃ•>|Üig10»± ˝^Ť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÷îݨ¨¨ŔîÝ»Y8ć»×ËçĎϨ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Ǣ0JdAxď.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‰2f+I¶Á{Ú3Äw,o8—Ä}.*ů°uÉ[8Fmm­÷´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…B55HŹ‚Ű¬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Ţ» ÷ŻăÁĽ4xŚ Ł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‚(ś***đŃ•>|Üig1fA*•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;)Ky’ä÷C% ĹE3Ďb^KĎ®ËíŰës|ŚÚÍ,j„đý˝§Ë“&¦V«ŃÔÔ„ĆĆĆ€đZĄRA§Ó„µz˝>îęô«ÂBśęě„ÉnÇëííxnÆyK0 d‰(ęŤ[ŕ+!m™7uöÝ—ź?ň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 |ĂXHdaÄČŢş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Ĺwlk ×;ž 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 ë[ĘYp±÷ô×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áĹ_ÄáÇYHznÂÍâ…ź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†®|čí”ő{qq„Š"~™(n€|î˙>/ľ8O6Iáš'ĆÍĺ“[ů}ł0xń` Ď˙”4÷>Ç™îńůŘ7¶ Q¶Ţ{™Ó~#äż)@Űěţhz˙č‡ <ł‹D.Ü <´^%'cĬ _:\hůܶޅ®‹Dîpľh3Řąňß/ĹwsKaa! nÔÔ ťš&»ÝÎ"Dâó–€höÉ)*ŠŕĘű&śönďÇŢ$Y\0„ Y†”ž…ÓvN{70ě€@śĹPhž$.]ŽE…?Űç€ŃżË슍ëcń¸±ń'cďkx‚¤Đß@wn@ËçŔ˙śëíwĐňąű|iš{”ÁCë9—zĆn÷X‚ _¸xßpwÄ® h3»¤inIĄRHĄRđaEÓUWWçťë+‹Y4 @źÉ„ôĹ‹!—űż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`ÓŞLb†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>Žď‚±Ű}^°™´ÁřvN܆Ę3BaĽŔ31łëß%qAšć®]Fşą™@Ćß± `ŁŮt‚Ő%‹…1sż›››ŃŇŇ‚GżBŐŃŃÁ"D ˛DDDDDWĆŚrőš"úv{F¸?Zď,ęŤ@żctfj· wÜç›n ĐďźŰ㡤ÂóVE" 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â‘ďŐĂÚ79b¤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łü»LcŃ Ţ{+Űă헥ŢöµŰŰĆ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¬¶cmODM$–Á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<ýÂŰ€'‰ńTrA>-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…,ůµŕyIMLR©d0sŃ …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,cPsŃ )++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ËŐłä›>ýĺZh’ ˇ×ë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·©°hsĎÁŞýß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Č‘_ ž—„ŘÄ(•R8GÍ QH‚ç%AžyÜ\d Ť×u¶‹‰ Ç[Ň]>mŃÉ˝t9Ţ|ßTô{Ť‡ăŮôD/ÓjµB2V,óů)VZZŠŇŇŇ Ý§¸¸óALČŃ«®®`«Ę,//ç‚QDc`B6Ŕ­_ż—Ż‹Łg{$‘1 DÄ9‚háÔŐ>|Őba08ŢŠÁ`€Ů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Ŕ˛GX5zú€†żőś{Ä ű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+Ť ¬Ç‰„-“´×Ów/ŰÖi…±]4j1®1’±áQ¶–˛TVÂÎ0ÝÉfě×5âóó×]nK’IđVţ“P?•‚q85Ă ]®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«ÓßÚŃě•´gGuAn…|ľŇ(+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ŔłżĂ¸Óۆ¶^ "4Aśöľ´Ž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<-ů®ŕyIMLaż!âÁ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ś#8G7Ů“°Ž_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ÚzPvF#â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ä×ÂŰ€'‰ńTrADś#fPD†°Ç6`í2)Rç1 DoDDłV'€đá‡HMMEII‰ÓíLČ‘_ ž—„ŘÄ(•R8GÍ QH‚ç%AžyÜ\„ăŤĆŔE˝¦ +dÜúőëqůú·8z¶ABD1Gô÷÷ŁŁŁ&“ ÝÝÝ|#}H||<ž~úib EEE8uµ_µX "Ž7"""šĄ prąaR_‹d0Čďçţţ~\Ľx­­­|ó|TGG0ĄR‰¦ˇßěe08Ţ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Ë=ľoD†°Ç6`í2)Rç1D^ÄńFDäŰ:€?Djj*JJJśçq†Ü…D xJřą±±‘}"É+ďí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‚8GxQb%‚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űav˙ă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íšďř­»fUN˝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}ÜdQ: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Ď­/“#d2ŤŤŤęîî¦@ź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ĘŻŚŚ %%%QHBŚ` 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ÖĐĐ0nlnn®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 Ď=÷ś~ó›ßPIŚ@€(ÓŘبîîn „ń9×ŐŐE!˛D™ňňrŐŐŐ…Ô¶żż_MMMęííĄü<ńIJX,"‚dB.]ş¤ýű÷Ë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ÓÎť;™N2ĺĺĺ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·öîÝ;lk4UYYéÓ·{?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üޱ˛˛R6l4ö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ś7bA¸tşß3'ýh™ÍfÝőŤ4]şrB€°čůĚ©»Óîą}ÂÜéŁË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’Ô×ÓďłOUúŕ‹ó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‡~ó›ßPI,Îĺ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ů?WFüµŔ’ššŞľ_‰óř?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 ą}RR7"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۶!ó+++}{ţůç]•••>Awhh0<ë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í,Äłm˙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狹&?-ËRCCYŔ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˙öRlIźĎ§––µ¶¶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#‘Č”kg3ß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`‚ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/devstack.rst000066400000000000000000000020211513436046000257430ustar00rootroot00000000000000============================== 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/ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/events.rst000066400000000000000000000271451513436046000254610ustar00rootroot00000000000000.. 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/ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/gmr.rst000066400000000000000000000060021513436046000247270ustar00rootroot00000000000000.. 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 `_ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/index.rst000066400000000000000000000022701513436046000252540ustar00rootroot00000000000000================= 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/measurements.rst000066400000000000000000000077211513436046000266630ustar00rootroot00000000000000.. 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/new_resource_types.rst000066400000000000000000000057441513436046000301020ustar00rootroot00000000000000.. 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`. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/overview.rst000066400000000000000000000027631513436046000260220ustar00rootroot00000000000000======== 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/plugins.rst000066400000000000000000000120741513436046000256310ustar00rootroot00000000000000.. 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/contributor/testing.rst000066400000000000000000000033131513436046000256210ustar00rootroot00000000000000.. 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 py27,pep8 As tox is a wrapper around testr, it also accepts the same flags as testr. See the `testr documentation`_ for details about these additional flags. .. _testr documentation: https://testrepository.readthedocs.org/en/latest/MANUAL.html Use a double hyphen to pass options to testr. For example, to run only tests under tests/unit/image:: $ tox -e py27 -- 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://wiki.openstack.org/wiki/Testr#Debugging_.28pdb.29_Tests .. _tox: https://tox.readthedocs.io/en/latest/ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/glossary.rst000066400000000000000000000106421513436046000234400ustar00rootroot00000000000000.. 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/index.rst000066400000000000000000000031211513436046000226760ustar00rootroot00000000000000.. 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` ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/000077500000000000000000000000001513436046000225065ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/cinder/000077500000000000000000000000001513436046000237525ustar00rootroot00000000000000install-cinder-config-common.inc000066400000000000000000000011541513436046000320300ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/cinder* 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/cinder/install-cinder-rdo.rst000066400000000000000000000020761513436046000302030ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/cinder/install-cinder-ubuntu.rst000066400000000000000000000017421513436046000307400ustar00rootroot00000000000000Enable 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 # service cinder-api restart # service cinder-scheduler restart #. Restart the Block Storage services on the storage nodes: .. code-block:: console # service cinder-volume restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/get_started.rst000066400000000000000000000034441513436046000255520ustar00rootroot00000000000000========================================== 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/glance/000077500000000000000000000000001513436046000237375ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/glance/install-glance-rdo.rst000066400000000000000000000017771513436046000301640ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/glance/install-glance-ubuntu.rst000066400000000000000000000016611513436046000307120ustar00rootroot00000000000000Enable 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 # service glance-api restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/heat/000077500000000000000000000000001513436046000234275ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/heat/install-heat-rdo.rst000066400000000000000000000015351513436046000273340ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/heat/install-heat-ubuntu.rst000066400000000000000000000014201513436046000300630ustar00rootroot00000000000000Enable 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 # service heat-api restart # service heat-api-cfn restart # service heat-engine restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/index.rst000066400000000000000000000005311513436046000243460ustar00rootroot00000000000000================== 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 `_. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-base-config-common.inc000066400000000000000000000031331513436046000303100ustar00rootroot000000000000002. 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-base-prereq-common.inc000066400000000000000000000137511513436046000303500ustar00rootroot000000000000001. 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 | +--------------+----------------------------------+ ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-base-rdo.rst000066400000000000000000000047371513436046000264130ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-base-ubuntu.rst000066400000000000000000000041731513436046000271430ustar00rootroot00000000000000.. _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 # service gnocchi-api restart # service gnocchi-metricd restart 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 # service ceilometer-agent-central restart # service ceilometer-agent-notification restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-compute-common.inc000066400000000000000000000034171513436046000276140ustar00rootroot000000000000002. 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-compute-rdo.rst000066400000000000000000000020461513436046000271440ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-compute-ubuntu.rst000066400000000000000000000014161513436046000277020ustar00rootroot00000000000000Enable 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 # service ceilometer-agent-compute restart # service ceilometer-agent-ipmi restart (optional) #. Restart the Compute service: .. code-block:: console # service nova-compute restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-compute.rst000066400000000000000000000006341513436046000263630ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-controller.rst000066400000000000000000000027001513436046000270660ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/install-gnocchi.inc000066400000000000000000000045361513436046000262670ustar00rootroot000000000000003. 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 documenation 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/neutron/000077500000000000000000000000001513436046000242005ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/neutron/install-neutron-rdo.rst000066400000000000000000000014071513436046000306540ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/neutron/install-neutron-ubuntu.rst000066400000000000000000000013031513436046000314050ustar00rootroot00000000000000Enable 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 # service neutron-server restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/next-steps.rst000066400000000000000000000003511513436046000253510ustar00rootroot00000000000000.. _next-steps: Next steps ~~~~~~~~~~ Your OpenStack environment now includes the ceilometer service. To add additional services, see the `OpenStack Installation Tutorials and Guides `_. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/swift/000077500000000000000000000000001513436046000236425ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/swift/install-swift-config-common.inc000066400000000000000000000024161513436046000316710ustar00rootroot00000000000000Configure 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``. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/swift/install-swift-prereq-common.inc000066400000000000000000000020061513436046000317150ustar00rootroot00000000000000Prerequisites ------------- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/swift/install-swift-rdo.rst000066400000000000000000000013051513436046000277550ustar00rootroot00000000000000Enable 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/swift/install-swift-ubuntu.rst000066400000000000000000000011721513436046000305150ustar00rootroot00000000000000Enable 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 # service swift-proxy restart ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/install/verify.rst000066400000000000000000000111231513436046000245420ustar00rootroot00000000000000.. _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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/releasenotes/000077500000000000000000000000001513436046000235315ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/releasenotes/folsom.rst000066400000000000000000000050361513436046000255660ustar00rootroot00000000000000.. 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 ceilometer-25.0.0+git20260122.52.0ff494d01/doc/source/releasenotes/index.rst000066400000000000000000000027411513436046000253760ustar00rootroot00000000000000.. 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/ ceilometer-25.0.0+git20260122.52.0ff494d01/etc/000077500000000000000000000000001513436046000175465ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/000077500000000000000000000000001513436046000216765ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/ceilometer-config-generator.conf000066400000000000000000000004561513436046000301310ustar00rootroot00000000000000[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 ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/examples/000077500000000000000000000000001513436046000235145ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/examples/osprofiler_event_definitions.yaml000066400000000000000000000013021513436046000323540ustar00rootroot00000000000000--- - 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 ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/polling.yaml000066400000000000000000000010061513436046000242230ustar00rootroot00000000000000--- 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 ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/polling_all.yaml000066400000000000000000000001271513436046000250560ustar00rootroot00000000000000--- sources: - name: all_pollsters interval: 300 meters: - "*" ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/rootwrap.conf000066400000000000000000000017271513436046000244310ustar00rootroot00000000000000# 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 # explicitely 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 ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/rootwrap.d/000077500000000000000000000000001513436046000237755ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/etc/ceilometer/rootwrap.d/ipmi.filters000066400000000000000000000005211513436046000263230ustar00rootroot00000000000000# 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/.* ceilometer-25.0.0+git20260122.52.0ff494d01/pyproject.toml000066400000000000000000000001331513436046000217040ustar00rootroot00000000000000[build-system] requires = ["pbr>=6.0.0", "setuptools>=64.0.0"] build-backend = "pbr.build" ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/000077500000000000000000000000001513436046000214645ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/000077500000000000000000000000001513436046000226145ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/.placeholder000066400000000000000000000000001513436046000250650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/add-aodh-metrics-afbe9b780fd137d6.yaml000066400000000000000000000001741513436046000311730ustar00rootroot00000000000000--- features: - | Ceilometer is now able to poll the /metrics endpoint in Aodh to get evaluation results metrics. add-availability_zone-gnocchi-instance-15170e4966a89d63.yaml000066400000000000000000000006461513436046000351040ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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.add-db-legacy-clean-tool-7b3e3714f414c448.yaml000066400000000000000000000002621513436046000321100ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1578128 `_] Add a tool that allow users to drop the legacy alarm and alarm_history tables. add-designate-dns-metrics-8f4a2c7e9b1d3e5a.yaml000066400000000000000000000007411513436046000327210ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-disk-latency-metrics-9e5c05108a78c3d9.yaml000066400000000000000000000002321513436046000323430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Add `disk.device.read.latency` and `disk.device.write.latency` meters to capture total time used by read or write operations. add-disk-size-pollsters-6b819d067f9cf736.yaml000066400000000000000000000004311513436046000322540ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-full-snmpv3-usm-support-ab540c902fa89b9d.yaml000066400000000000000000000002241513436046000331450ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1597618 `_] Add the full support of snmp v3 user security model. add-image-meta-to-samples-72d62016f5ab0043.yaml000066400000000000000000000020301513436046000322700ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-ipmi-sensor-data-gnocchi-70573728499abe86.yaml000066400000000000000000000003671513436046000327610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-json-output-to-file-publisher-786380cb7e21b56b.yaml000066400000000000000000000001251513436046000341440ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - > Add new json output option for the existing file publisher. add-loadbalancer-resource-type-a73c29594b72f012.yaml000066400000000000000000000003661513436046000334430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - | [`bug 1848286 `_] Enable load balancer metrics by adding the loadbalancer resource type, allowing Gnocchi to capture measurement data for Octavia load balancers. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/add-magnum-event-4c75ed0bb268d19c.yaml000066400000000000000000000001441513436046000311260ustar00rootroot00000000000000--- features: - > Added support for magnum bay CRUD events, event_type is 'magnum.bay.*'. add-map-trait-plugin-0d969f5cc7b18175.yaml000066400000000000000000000005731513436046000315170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-memory-available-metric-d59dc787c485efd3.yaml000066400000000000000000000004651513436046000332050ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-memory-pollster-7689d354c45cc740.yaml000066400000000000000000000002231513436046000314060ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | The ``memory`` meter now has measures published by a compute pollster, in addition to the existing notification meter. add-memory-swap-metric-f1633962ab2cf0f6.yaml000066400000000000000000000001501513436046000321170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - Add memory swap metric for VM, including 'memory.swap.in' and 'memory.swap.out'. add-octavia-loadbalancer-metrics-8f4e7d2c1a3b5e69.yaml000066400000000000000000000012101513436046000341440ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-parameter-for-disabled-projects-381da4543fff071d.yaml000066400000000000000000000003361513436046000345250ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-pool-size-metrics-cdecb979135bba85.yaml000066400000000000000000000005601513436046000321170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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` add-power-state-metric-cdfbb3098b50a704.yaml000066400000000000000000000001171513436046000321660ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Added the new power.state metric from virDomainState. add-swift-storage_policy-attribute-322fbb5716c5bb10.yaml000066400000000000000000000021601513436046000345220ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-tenant-name-discovery-668260bb4b2b0e8c.yaml000066400000000000000000000003421513436046000325710ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-tool-for-migrating-data-to-gnocchi-cea8d4db68ce03d0.yaml000066400000000000000000000003411513436046000352550ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-upgrade-check-framework-d78858c54cb85f91.yaml000066400000000000000000000007471513436046000330370ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-vcpus-pollster-23cfa683d07092b3.yaml000066400000000000000000000002221513436046000312750ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | The ``vcpus`` meter now has measures published by a compute pollster, in addition to the existing notification meter. add-volume-pollster-metadata-d7b435fed9aac0aa.yaml000066400000000000000000000006711513436046000336070ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. add-volume_type_id-attr-f29af86534907941.yaml000066400000000000000000000012531513436046000321610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. aggregator-transformer-timeout-e0f42b6c96aa7ada.yaml000066400000000000000000000002521513436046000342150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1531626 `_] Ensure aggregator transformer timeout is honoured if size is not provided. allow-disable-gnocchi-project-filter-c7e08c74d57b0224.yaml000066400000000000000000000011071513436046000346320ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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). ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/always-requeue-7a2df9243987ab67.yaml000066400000000000000000000011201513436046000306160ustar00rootroot00000000000000--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/batch-messaging-d126cc525879d58e.yaml000066400000000000000000000010061513436046000307030ustar00rootroot00000000000000--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/bug-1929178-a8243526ce2311f7.yaml000066400000000000000000000002031513436046000271620ustar00rootroot00000000000000--- deprecations: - | The ``[coordination] check_watchers`` parameter has been deprecated since it has been ineffective. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/bug-2007108-dba7163b245ad8fd.yaml000066400000000000000000000002761513436046000274520ustar00rootroot00000000000000--- fixes: - | [`bug 2007108 `_] The retired metrics dependent on SNMP have been removed from the default ``polling.yaml``. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/bug-2113768-a2db3a59c8e13558.yaml000066400000000000000000000003271513436046000273260ustar00rootroot00000000000000--- fixes: - | Fixed `bug #2113768 `__ where the Libvirt inspector did not catch exceptions thrown when calling interfaceStats function on a domain. cache-json-parsers-888307f3b6b498a2.yaml000066400000000000000000000003511513436046000312010ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1550436 `_] Cache json parsers when building parsing logic to handle event and meter definitions. This will improve agent startup and setup time. ceilometer-api-deprecate-862bfaa54e80fa01.yaml000066400000000000000000000002041513436046000325330ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - Ceilometer API is deprecated. Use the APIs from Aodh (alarms), Gnocchi (metrics), and/or Panko (events). ceilometer-api-removal-6bd44d3eab05e593.yaml000066400000000000000000000001071513436046000322540ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated Ceilometer API has been removed. ceilometer-event-api-removed-49c57835e307b997.yaml000066400000000000000000000003111513436046000331140ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. cinder-capacity-samples-de94dcfed5540b6c.yaml000066400000000000000000000002521513436046000325630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Add support to capture volume capacity usage details from cinder. This data is extracted from notifications sent by Cinder starting in Ocata. cinder-volume-size-poller-availability_zone-2d20a7527e2341b9.yaml000066400000000000000000000001761513436046000362110ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | The resource metadata for the Cinder volume size poller now includes the availability zone field. compute-discovery-interval-d19f7c9036a8c186.yaml000066400000000000000000000006351513436046000331000ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. configurable-data-collector-e247aadbffb85243.yaml000066400000000000000000000005451513436046000333330ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/cors-support-70c33ba1f6825a7b.yaml000066400000000000000000000005241513436046000303670ustar00rootroot00000000000000--- 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. deprecate-aggregated-disk-metrics-54a395c05e74d685.yaml000066400000000000000000000003551513436046000341330ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-ceilometer-collector-b793b91cd28b9e7f.yaml000066400000000000000000000007021513436046000340020ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-contrail-256177299deb6926.yaml000066400000000000000000000002451513436046000312060ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Support for OpenContrail, which is currently known as Tungsten Fabric, has been deprecated and will be removed in a future release. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/deprecate-events-6561f4059fa25c02.yaml000066400000000000000000000002071513436046000310070ustar00rootroot00000000000000--- deprecations: - | The Ceilometer event subsystem and pipeline is now deprecated and will be removed in a future release. deprecate-file-dispatcher-2aff376db7609136.yaml000066400000000000000000000003731513436046000325630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-generic-hardware-declarative-pollstar-dfa418bf6a5e0459.yaml000066400000000000000000000011041513436046000371750ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-http-control-exchanges-026a8de6819841f8.yaml000066400000000000000000000005651513436046000340530ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-http-dispatcher-dbbaacee8182b550.yaml000066400000000000000000000011351513436046000331010ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-http_timeout-ce98003e4949f9d9.yaml000066400000000000000000000001601513436046000322610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | The ``[DEFAULT] http_timeout`` option has been deprecated because it is unused. deprecate-kafka-publisher-17b4f221758e15da.yaml000066400000000000000000000007251513436046000325610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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 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. deprecations: - | Kafka publisher is deprecated to use generic notifier instead. deprecate-neutron-fwaas-e985afe956240c08.yaml000066400000000000000000000002441513436046000323150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Support for Neutron FWaaS has been officially deprecated. The feature has been useless since the Neutron FWaaS project was retired. deprecate-neutron-lbaas-5a36406cbe44bbe3.yaml000066400000000000000000000002441513436046000324110ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Support for Neutron LBaaS has been officially deprecated. The feature has been useless since the Neutron LBaaS project was retired. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/deprecate-odl-07e3f59165612566.yaml000066400000000000000000000001661513436046000301410ustar00rootroot00000000000000--- deprecations: - | Support for OpenDaylight has been deprecated and will be removed in a future release. deprecate-pollster-builder-d481966e497959a3.yaml000066400000000000000000000002331513436046000326700ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Support for pollster builder has been deprecated, because ceilometer no longer provides any built-in pollster builder now. deprecate-pollster-list-ccf22b0dea44f043.yaml000066400000000000000000000002471513436046000325310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Deprecating support for enabling pollsters via command line. Meter and pollster enablement should be configured via polling.yaml file. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/deprecate-vmware-ae49e07e40e74577.yaml000066400000000000000000000003031513436046000310770ustar00rootroot00000000000000--- 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. deprecate-windows-support-d784b975ce878864.yaml000066400000000000000000000003351513436046000326740ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. deprecate-xen-support-27600e2bf7be548c.yaml000066400000000000000000000002061513436046000320720ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Support for XenServer/Xen Cloud Platform has been deprecated and will be removed in a future release. deprecated_database_event_dispatcher_panko-607d558c86a90f17.yaml000066400000000000000000000002521513436046000362340ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/drop-collector-4c207b35d67b2977.yaml000066400000000000000000000005531513436046000305160ustar00rootroot00000000000000--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/drop-image-meter-9c9b6cebd546dae7.yaml000066400000000000000000000007441513436046000313130ustar00rootroot00000000000000--- 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. drop-instance-meter-1b657717b21a0f55.yaml000066400000000000000000000006671513436046000313610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/drop-kwapi-b687bc476186d01b.yaml000066400000000000000000000001201513436046000277060ustar00rootroot00000000000000--- deprecations: - | Previously deprecated kwapi meters are not removed. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/drop-py-2-7-87352d5763131c13.yaml000066400000000000000000000003151513436046000273070ustar00rootroot00000000000000--- 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. drop-python-3-6-and-3-7-f67097fa6894da52.yaml000066400000000000000000000002011513436046000314220ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now supported is Python 3.8. dynamic-pollster-system-6b45c8c973201b2b.yaml000066400000000000000000000002541513436046000323620ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Add dynamic pollster system. The dynamic pollster system enables operators to gather new metrics on the fly (without needing to code pollsters).dynamic-pollster-system-for-non-openstack-apis-4e06694f223f34f3.yaml000066400000000000000000000003361513436046000366100ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. dynamic-pollster-url-joins-6cdb01c4015976f7.yaml000066400000000000000000000013551513436046000327700ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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). enable-promethus-exporter-tls-76e78d4f4a52c6c4.yaml000066400000000000000000000001461513436046000335720ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Enhanced the Prometheus exporter to support TLS for exposing metrics securely.ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/event-type-race-c295baf7f1661eab.yaml000066400000000000000000000002451513436046000310650ustar00rootroot00000000000000--- fixes: - > [`bug 1254800 `_] Add better support to catch race conditions when creating event_types ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/fix-1940660-5226988f2e7ae1bd.yaml000066400000000000000000000004141513436046000273420ustar00rootroot00000000000000--- 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. fix-agent-coordination-a7103a78fecaec24.yaml000066400000000000000000000006641513436046000323450ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. fix-aggregation-transformer-9472aea189fa8f65.yaml000066400000000000000000000003741513436046000332740ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. fix-floatingip-pollster-f5172060c626b19e.yaml000066400000000000000000000006121513436046000322540ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. fix-network-lb-bytes-sample-5dec2c6f3a8ae174.yaml000066400000000000000000000003141513436046000332470ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1530793 `_] network.services.lb.incoming.bytes meter was previous set to incorrect type. It should be a gauge meter. fix-notification-batch-9bb42cbdf817e7f9.yaml000066400000000000000000000004301513436046000323410ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/fix-radosgw-name-6de6899ddcd7e06d.yaml000066400000000000000000000010341513436046000312470ustar00rootroot00000000000000--- 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 fix-size-metric-units-e6028b4b4fc3e6aa.yaml000066400000000000000000000036471513436046000321610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. fix-volume-provider-pool-capacity-metrics-7b8b0de29a513cea.yaml000066400000000000000000000002331513436046000361170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - | [`bug 2113903 `_] Fix volume provider pool capacity metrics for ceph backend. get-more-flavor-info-from-libvirt-c8db26fe410abe6e.yaml000066400000000000000000000035151513436046000344270ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/gnocchi-cache-1d8025dfc954f281.yaml000066400000000000000000000005421513436046000303150ustar00rootroot00000000000000--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/gnocchi-cache-b9ad4d85a1da8d3f.yaml000066400000000000000000000003121513436046000306050ustar00rootroot00000000000000--- fixes: - > [`bug 255569 `_] Fix caching support in Gnocchi dispatcher. Added better locking support to enable smoother cache access. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/gnocchi-client-42cd992075ee53ab.yaml000066400000000000000000000002671513436046000306120ustar00rootroot00000000000000--- features: - > Gnocchi dispatcher now uses client rather than direct http requests upgrade: - > gnocchiclient library is now a requirement if using ceilometer+gnocchi. gnocchi-host-metrics-829bcb965d8f2533.yaml000066400000000000000000000003221513436046000316210ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - > [`bug 1518338 `_] Add support for storing SNMP metrics in Gnocchi.This functionality requires Gnocchi v2.1.0 to be installed. gnocchi-no-metric-by-default-b643e09f5ffef2c4.yaml000066400000000000000000000003141513436046000333430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. gnocchi-orchestration-3497c689268df0d1.yaml000066400000000000000000000002361513436046000320130ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - > gnocchi_resources.yaml in Ceilometer should be updated. fixes: - > Fix samples from Heat to map to correct Gnocchi resource type gnocchi-publisher-hash-blake2-514c0ca1882b4423.yaml000066400000000000000000000011561513436046000331500ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. gnocchi-udp-collector-00415e6674b5cc0f.yaml000066400000000000000000000002171513436046000317330ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1523124 `_] Fix gnocchi dispatcher to support UDP collector handle-malformed-resource-definitions-ad4f69f898ced34d.yaml000066400000000000000000000005451513436046000353630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. http-dispatcher-batching-4e17fce46a196b07.yaml000066400000000000000000000003641513436046000325240ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. http-dispatcher-verify-ssl-551d639f37849c6f.yaml000066400000000000000000000011211513436046000327220ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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 http-publisher-authentication-6371c5a9aa8d4c03.yaml000066400000000000000000000014071513436046000336220ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. http_proxy_to_wsgi_enabled-616fa123809e1600.yaml000066400000000000000000000013271513436046000330350ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. improve-events-rbac-support-f216bd7f34b02032.yaml000066400000000000000000000006001513436046000331360ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. include-monasca-publisher-1f47dde52af50feb.yaml000066400000000000000000000004761513436046000331210ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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). index-events-mongodb-63cb04200b03a093.yaml000066400000000000000000000003321513436046000314760ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - > Run db-sync to add new indices. fixes: - > [`bug 1526793 `_] Additional indices were added to better support querying of event data. instance-discovery-new-default-7f9b451a515dddf4.yaml000066400000000000000000000005141513436046000337440ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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'. instance-record-launched-created-deleted-d7f44df3bbcf0790.yaml000066400000000000000000000001431513436046000356510ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | `launched_at`/`created_at`/`deleted_at` of Nova instances are now tracked. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/keystone-v3-fab1e257c5672965.yaml000066400000000000000000000001031513436046000300260ustar00rootroot00000000000000--- features: - > Add support for Keystone v3 authentication ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/kwapi_deprecated-c92b9e72c78365f0.yaml000066400000000000000000000001721513436046000311450ustar00rootroot00000000000000--- deprecations: - The Kwapi pollsters are deprecated and will be removed in the next major version of Ceilometer. less-nova-polling-ac56687da3f8b1a3.yaml000066400000000000000000000022601513436046000312700ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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 informations are 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 lookup-meter-def-vol-correctly-0122ae429275f2a6.yaml000066400000000000000000000005411513436046000334460ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. make-image-format-attributes-optional-cc7acd492f791553.yaml000066400000000000000000000005701513436046000351420ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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). make-instance-host-optional-972fa14405c1e2f6.yaml000066400000000000000000000004651513436046000330760ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. manager-based-ipc-queues-85e3bf59ffdfb0ac.yaml000066400000000000000000000015411513436046000327120ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. memory-bandwidth-meter-f86cf01178573671.yaml000066400000000000000000000002521513436046000320140ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - Add two new meters, including memory.bandwidth.total and memory.bandwidth.local, to get memory bandwidth statistics based on Intel CMT feature. mongodb-handle-large-numbers-7c235598ca700f2d.yaml000066400000000000000000000005061513436046000331770ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. network-statistics-from-opendaylight-787df77484d8d751.yaml000066400000000000000000000004221513436046000350360ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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 openstack-dynamic-pollsters-metadata-enrichment-703cf5914cf0c578.yaml000066400000000000000000000001511513436046000371270ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | OpenStack Dynamic pollsters metadata enrichment with other OpenStack API's data. parallel_requests_option-a3f901b6001e26e4.yaml000066400000000000000000000003661513436046000326650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. parallels-virt_type-ee29c4802fdf5c8e.yaml000066400000000000000000000001241513436046000320100ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - | The ``[DEFAULT] virt_type`` option now supports ``parallels``. pecan-debug-removed-dc737efbf911bde7.yaml000066400000000000000000000000761513436046000317020ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - The api.pecan_debug option has been removed. perf-events-meter-b06c2a915c33bfaf.yaml000066400000000000000000000004251513436046000313340ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. pipeline-fallback-polling-3d962a0fff49ccdd.yaml000066400000000000000000000003051513436046000330660ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. polling-batch-size-7fe11925df8d1221.yaml000066400000000000000000000006441513436046000312550ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. polling-definition-efffb92e3810e571.yaml000066400000000000000000000011201513436046000315110ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. polling-deprecation-4d5b83180893c053.yaml000066400000000000000000000002271513436046000313600ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | Usage of pipeline.yaml for polling configuration is now deprecated. The dedicated polling.yaml should be used instead. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/prometheus-bcb201cfe46d5778.yaml000066400000000000000000000001371513436046000301720ustar00rootroot00000000000000--- features: - | A new pulisher have been added to push data to Prometheus Pushgateway. publish-fw-with-invalid-state-e961372c95791bcd.yaml000066400000000000000000000011621513436046000333650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. publish-network-resources-with-invalid-state-6693c6fa1fefa097.yaml000066400000000000000000000015411513436046000366120ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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``. refresh-legacy-cache-e4dbbd3e2eeca70b.yaml000066400000000000000000000006541513436046000321520ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-alarms-4df3cdb4f1fb5faa.yaml000066400000000000000000000002051513436046000310360ustar00rootroot00000000000000--- features: - > Ceilometer alarms code is now fully removed from code base. Equivalent functionality is handled by Aodh. remove-batch_polled_samples-b40241c8aad3667d.yaml000066400000000000000000000001101513436046000332550ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Remove deprecated option `batch_polled_samples`. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-cadf-http-f8449ced3d2a29d4.yaml000066400000000000000000000003731513436046000311550ustar00rootroot00000000000000--- 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. remove-ceilometer-dbsync-53aa1b529f194f15.yaml000066400000000000000000000001461513436046000324570ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- other: - The deprecated ceilometer-dbsync has been removed. Use ceilometer-upgrade instead. remove-check_watchers-a7c955703b6d9f57.yaml000066400000000000000000000001311513436046000320400ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The ``[coordination] check_watchers`` parameter has been removed. remove-compute-disk-meters-264e686622886ff0.yaml000066400000000000000000000001651513436046000326350ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `disk.*` meters have been removed. Use the `disk.device.*` meters instead. remove-compute-rate-deprecated-meters-201893c6b686b04a.yaml000066400000000000000000000005331513436046000347670ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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 remove-compute-workload-partitioning-option-26538bc1e80500e3.yaml000066400000000000000000000002231513436046000362650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `compute.workload_partitioning` option has been removed in favor of `compute.instance_discovery_method`. remove-direct-publisher-5785ee7edd16c4d9.yaml000066400000000000000000000001271513436046000325000ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Remove direct publisher and use the explicit publisher instead. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-eventlet-6738321434b60c78.yaml000066400000000000000000000001271513436046000305410ustar00rootroot00000000000000--- features: - > Remove eventlet from Ceilometer in favour of threaded approach remove-exchange-control-options-75ecd49423639068.yaml000066400000000000000000000001221513436046000336530ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated control exchange options have been removed. remove-file-dispatcher-56ba1066c20d314a.yaml000066400000000000000000000001101513436046000320610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated file dispatcher has been removed. remove-generic-hardware-declarative-pollster-e05c614f273ab149.yaml000066400000000000000000000003671513436046000364040ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. remove-gnocchi-dispatcher-dd588252976c2abb.yaml000066400000000000000000000005541513436046000327010ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. remove-gnocchi-dispatcher-options-4f4ba2a155c1a766.yaml000066400000000000000000000001321513436046000343430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `gnocchi_dispatcher` option group has been removed. remove-http-dispatcher-1afdce1d1dc3158d.yaml000066400000000000000000000001101513436046000324320ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated http dispatcher has been removed. remove-intel-cmt-perf-meters-15d0fe72b2804f48.yaml000066400000000000000000000004231513436046000331730ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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`` remove-intel-node-manager-0889de66dede9ab0.yaml000066400000000000000000000001031513436046000327450ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Support for Intel Node Manager was removed. remove-kafka-broker-publisher-7026b370cfc831db.yaml000066400000000000000000000001471513436046000334560ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated kafka publisher has been removed, use NotifierPublisher instead. remove-meter-definitions-cfg-file-config-476596fc86c36a81.yaml000066400000000000000000000002211513436046000353500ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Remove deprecated option meter_definitions_cfg_file, use meter_definitions_dirs to configure meter notification file. remove-meter-definitions-cfg-file-d57c726d563d805f.yaml000066400000000000000000000001341513436046000341610ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `meter_definitions_cfg_file` option has been removed. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-monasca-d5ceda231839d43d.yaml000066400000000000000000000001141513436046000307040ustar00rootroot00000000000000--- upgrade: - | Remove integration with the inactive Monasca project remove-neutron-lbaas-d3d4a5327f6a167a.yaml000066400000000000000000000001151513436046000316750ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Support for neutron-lbaas resources has been removed. remove-notification-workload-partitioning-2cef114fb2478e39.yaml000066400000000000000000000001451513436046000361500ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated workload partitioning for notification agent has been removed. remove-nova-http-log-option-64e97a511e58da5d.yaml000066400000000000000000000001251513436046000331430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `nova_http_log_debug` option has been removed. remove-opencontrail-88656a9354179299.yaml000066400000000000000000000002561513436046000313050ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. remove-opendaylight-c3839bbe9aa2a227.yaml000066400000000000000000000001021513436046000316650ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Support for OpenDaylight has been removed. remove-pollster-list-bda30d747fb87c9e.yaml000066400000000000000000000001171513436046000321150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The deprecated `pollster-list` option has been removed. remove-publisher-topic-options-7a40787a3998921d.yaml000066400000000000000000000003141513436046000335310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-py38-80670bdcfd4dd135.yaml000066400000000000000000000001661513436046000300730ustar00rootroot00000000000000--- upgrade: - | Python 3.8 support was dropped. The minimum version of Python now supported is Python 3.9. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-py39-8c39f81f856bee9f.yaml000066400000000000000000000001661513436046000301310ustar00rootroot00000000000000--- upgrade: - | Support for Python 3.9 has been removed. Now Python 3.10 is the minimum version supported. remove-refresh-pipeline-618af089c5435db7.yaml000066400000000000000000000006511513436046000323240ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. remove-rpc-collector-d0d0a354140fd107.yaml000066400000000000000000000005101513436046000315710ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-sahara-9254593d4fb137b9.yaml000066400000000000000000000004211513436046000303170ustar00rootroot00000000000000--- upgrade: - | Default value of the ``[notification] notification_control_exchanges`` option has been updated and ``sahara`` is no longer included by default. - | The default event definiton has been updated and no longer includes events for sahara. remove-service-type-volume-v2-08c81098dc7c0922.yaml000066400000000000000000000002241513436046000332430ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | The deprecated ``[service_types] cinderv2`` option has been removed. Use the ``[service_types] cinder`` option instead. remove-shuffle_time_before_polling_task-option-05a4d225236c64b1.yaml000066400000000000000000000002441513436046000370230ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | The `shuffle_time_before_polling_task` option has been removed. This option never worked in the way it was originally intended too. remove-transformers-14e00a789dedd76b.yaml000066400000000000000000000001301513436046000317330ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | The support for transformers has been removed from the pipeline. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/remove-uml-e86feeabdd16c628.yaml000066400000000000000000000002221513436046000302310ustar00rootroot00000000000000--- upgrade: - | The ``[DEFAULT] virt_type`` option no longer supports ``uml``. UML support by nova was removed in nova 23.3.0 release. remove-vsphere-support-411c97b66bdcd264.yaml000066400000000000000000000004241513436046000323170ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. remove-windows-support-0d280cc7c7fffc61.yaml000066400000000000000000000002521513436046000324750ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Support for running ceilometer in Windows operating systems has been removed. Because of the removal, Hyper-V inspector has also been removed. remove-xen-support-7cb932b7bc621269.yaml000066400000000000000000000001221513436046000313520ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- upgrade: - | Support for XenServer/Xen Cloud Platform has been removed. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/removed-rgw-ae3d80c2eafc9319.yaml000066400000000000000000000001351513436046000303110ustar00rootroot00000000000000--- upgrade: - | Deprecated `rgw.*` meters have been removed. Use `radosgw.*` instead. rename-ceilometer-dbsync-eb7a1fa503085528.yaml000066400000000000000000000010661513436046000324310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. rename-tenant_name_discovery-1675a236bb51176b.yaml000066400000000000000000000002451513436046000333150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- deprecations: - | The ``[polling] tenant_name_discovery`` option has been deprecated in favor of the new ``[polling] identity_name_discovery`` option. save-rate-in-gnocchi-66244262bc4b7842.yaml000066400000000000000000000014521513436046000313230ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. scan-domains-for-tenants-8f8c9edcb74cc173.yaml000066400000000000000000000001771513436046000326310ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - The tenant (project) discovery code in the polling agent now scans for tenants in all available domains. selective-pipeline-notification-47e8a390b1c7dcc4.yaml000066400000000000000000000006321513436046000341710ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/ship-yaml-files-33aa5852bedba7f0.yaml000066400000000000000000000003651513436046000310530ustar00rootroot00000000000000--- 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/. single-thread-pipelines-f9e6ac4b062747fe.yaml000066400000000000000000000010701513436046000324410ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. skip-duplicate-meter-def-0420164f6a95c50c.yaml000066400000000000000000000005541513436046000322550ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes --- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/snmp-cpu-util-055cd7704056c1ce.yaml000066400000000000000000000007231513436046000303460ustar00rootroot00000000000000--- 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. snmp-diskio-samples-fc4b5ed5f19c096c.yaml000066400000000000000000000001621513436046000317030ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - | Add hardware.disk.read.* and hardware.disk.write.* metrics to capture diskio details. sql-query-optimisation-ebb2233f7a9b5d06.yaml000066400000000000000000000003351513436046000323710ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1506738 `_] [`bug 1509677 `_] Optimise SQL backend queries to minimise query load support-None-query-45abaae45f08eda4.yaml000066400000000000000000000002371513436046000316240ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - > [`bug 1388680 `_] Suppose ability to query for None value when using SQL backend. support-cinder-volume-snapshot-backup-metering-d0a93b86bd53e803.yaml000066400000000000000000000002211513436046000370230ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - Add support of metering the size of cinder volume/snapshot/backup. Like other meters, these are useful for billing system. support-lbaasv2-polling-c830dd49bcf25f64.yaml000066400000000000000000000011741513436046000324340ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. support-meter-batch-recording-mongo-6c2bdf4fbb9764eb.yaml000066400000000000000000000004271513436046000350630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. support-multiple-meter-definition-files-e3ce1fa73ef2e1de.yaml000066400000000000000000000003621513436046000360420ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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.support-snmp-cpu-util-5c1c7afb713c1acd.yaml000066400000000000000000000002211513436046000322630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - > [`bug 1513731 `_] Add support for hardware cpu_util in snmp.yaml support-unique-meter-query-221c6e0c1dc1b726.yaml000066400000000000000000000004121513436046000331100ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. switch-to-oslo-privsep-b58f20a279f31bc0.yaml000066400000000000000000000011371513436046000322120ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. thread-safe-matching-4a635fc4965c5d4c.yaml000066400000000000000000000003401513436046000316150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- critical: - > [`bug 1519767 `_] fnmatch functionality in python <= 2.7.9 is not threadsafe. this issue and its potential race conditions are now patched. threeads-process-pollsters-cbd22cca6f2effc4.yaml000066400000000000000000000002711513436046000335100ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. tooz-coordination-system-d1054b9d1a5ddf32.yaml000066400000000000000000000003471513436046000327140ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/transformer-ed4b1ea7d1752576.yaml000066400000000000000000000007511513436046000302640ustar00rootroot00000000000000--- 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 tranformers has. undeprecate-neutron-fwaas-9ad1b7fb10056e38.yaml000066400000000000000000000002051513436046000327150ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- other: - | Support for Neutron FWaaS has been un-deprecated, because FWaaS project was restored and is now maintained. unify-timestamp-of-polled-data-fbfcff43cd2d04bc.yaml000066400000000000000000000003761513436046000341370ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. use-glance-v2-in-image-pollsters-137a315577d5dc4c.yaml000066400000000000000000000003621513436046000336370ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- 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. use-notification-transport-url-489f3d31dc66c4d2.yaml000066400000000000000000000002451513436046000337570ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- fixes: - The transport_url defined in [oslo_messaging_notifications] was never used, which contradicts the oslo_messaging documentation. This is now fixed.use-usable-metric-if-available-970ee58e8fdeece6.yaml000066400000000000000000000001221513436046000337370ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes--- features: - use memory usable metric from libvirt memoryStats if available. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/volume-metrics-01ddde0180bc21cb.yaml000066400000000000000000000002061513436046000307760ustar00rootroot00000000000000--- upgrade: - | The default ``polling.yaml`` file has been updated and now it enables meters related to cinder by default. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/notes/zaqar-publisher-f7efa030b71731f4.yaml000066400000000000000000000001261513436046000310200ustar00rootroot00000000000000--- features: - Add a new publisher for pushing samples or events to a Zaqar queue. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/000077500000000000000000000000001513436046000227645ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2023.1.rst000066400000000000000000000002101513436046000242340ustar00rootroot00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2023.2.rst000066400000000000000000000002021513436046000242360ustar00rootroot00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2024.1.rst000066400000000000000000000002101513436046000242350ustar00rootroot00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2024.1 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2024.2.rst000066400000000000000000000002021513436046000242370ustar00rootroot00000000000000=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2025.1.rst000066400000000000000000000002021513436046000242370ustar00rootroot00000000000000=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/2025.2.rst000066400000000000000000000002021513436046000242400ustar00rootroot00000000000000=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/_static/000077500000000000000000000000001513436046000244125ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/_static/.placeholder000066400000000000000000000000001513436046000266630ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/conf.py000066400000000000000000000214571513436046000242740ustar00rootroot00000000000000# 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/'] ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/index.rst000066400000000000000000000005041513436046000246240ustar00rootroot00000000000000========================= Ceilometer Release Notes ========================= .. toctree:: :maxdepth: 1 unreleased 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 ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/liberty.rst000066400000000000000000000002201513436046000251620ustar00rootroot00000000000000============================= Liberty Series Release Notes ============================= .. release-notes:: :branch: origin/stable/liberty ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/000077500000000000000000000000001513436046000242235ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/en_GB/000077500000000000000000000000001513436046000251755ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/en_GB/LC_MESSAGES/000077500000000000000000000000001513436046000267625ustar00rootroot00000000000000releasenotes.po000066400000000000000000002757511513436046000317550ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/en_GB/LC_MESSAGES# 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-01-06 08:09+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2026-01-07 04:10+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 "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 "25.0.0-23" msgstr "25.0.0-23" 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 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 `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 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 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 "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 generage 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] 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 `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 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 deprecated Ceilometer API has been removed." msgstr "The deprecated Ceilometer API has been removed." 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 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 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 "" "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 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 "" "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 mutally " "exclusively, rather than capturing both always." msgstr "" "[`bug 1480333 `_] " "Support ability to configure collector to capture events or meters mutally " "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 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 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 "``cpu_l3_cache_usage``" msgstr "``cpu_l3_cache_usage``" 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 "``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 "" "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." ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/fr/000077500000000000000000000000001513436046000246325ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/fr/LC_MESSAGES/000077500000000000000000000000001513436046000264175ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po000066400000000000000000000027221513436046000314530ustar00rootroot00000000000000# 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" ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/mitaka.rst000066400000000000000000000333201513436046000247650ustar00rootroot00000000000000==================== 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 mutally 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 generage 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 occurance 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/newton.rst000066400000000000000000000161421513436046000250340ustar00rootroot00000000000000==================== 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. ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/ocata.rst000066400000000000000000000002101513436046000245760ustar00rootroot00000000000000=========================== Ocata Series Release Notes =========================== .. release-notes:: :branch: origin/stable/ocata ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/pike.rst000066400000000000000000000002171513436046000244460ustar00rootroot00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/queens.rst000066400000000000000000000002231513436046000250130ustar00rootroot00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/rocky.rst000066400000000000000000000002211513436046000246400ustar00rootroot00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/stein.rst000066400000000000000000000002211513436046000246330ustar00rootroot00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/train.rst000066400000000000000000000001761513436046000246370ustar00rootroot00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/unreleased.rst000066400000000000000000000001561513436046000256470ustar00rootroot00000000000000============================= Current Series Release Notes ============================= .. release-notes:: ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/ussuri.rst000066400000000000000000000002021513436046000250420ustar00rootroot00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/victoria.rst000066400000000000000000000002201513436046000253300ustar00rootroot00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/wallaby.rst000066400000000000000000000002141513436046000251460ustar00rootroot00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/xena.rst000066400000000000000000000002001513436046000244410ustar00rootroot00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/yoga.rst000066400000000000000000000002001513436046000244450ustar00rootroot00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ceilometer-25.0.0+git20260122.52.0ff494d01/releasenotes/source/zed.rst000066400000000000000000000001741513436046000243020ustar00rootroot00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ceilometer-25.0.0+git20260122.52.0ff494d01/reno.yaml000066400000000000000000000002301513436046000206150ustar00rootroot00000000000000--- # 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 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.18.0 # Apache-2.0 keystoneauth1>=3.18.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 python-swiftclient>=3.2.0 # Apache-2.0 python-cinderclient>=3.3.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.36 # MIT botocore>=1.20.0 # Apache-2.0 openstacksdk>=4.6.0 # Apache-2.0 ceilometer-25.0.0+git20260122.52.0ff494d01/setup.cfg000066400000000000000000000264121513436046000206210ustar00rootroot00000000000000[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.10 classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Topic :: System :: Monitoring [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 alarm = ceilometer.alarm.discovery:AlarmDiscovery 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 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 ceilometer-25.0.0+git20260122.52.0ff494d01/setup.py000066400000000000000000000012711513436046000205060ustar00rootroot00000000000000# 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) ceilometer-25.0.0+git20260122.52.0ff494d01/test-requirements.txt000066400000000000000000000003401513436046000232310ustar00rootroot00000000000000fixtures>=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 testtools>=2.2.0 # MIT stestr>=2.0.0 # Apache-2.0 testresources>=2.0.1 # Apache-2.0 ceilometer-25.0.0+git20260122.52.0ff494d01/tools/000077500000000000000000000000001513436046000201335ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/tools/__init__.py000066400000000000000000000000001513436046000222320ustar00rootroot00000000000000ceilometer-25.0.0+git20260122.52.0ff494d01/tools/send_test_data.py000077500000000000000000000111021513436046000234640ustar00rootroot00000000000000#!/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() ceilometer-25.0.0+git20260122.52.0ff494d01/tox.ini000066400000000000000000000054301513436046000203100ustar00rootroot00000000000000[tox] minversion = 3.18.0 envlist = py3,pep8 [testenv] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt usedevelop = True setenv = CEILOMETER_TEST_BACKEND={env:CEILOMETER_TEST_BACKEND:none} passenv = OS_TEST_TIMEOUT OS_STDOUT_CAPTURE OS_STDERR_CAPTURE OS_LOG_CAPTURE CEILOMETER_* 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 coverage xml -o cover/coverage.xml [testenv:pep8] skip_install = true deps = pre-commit commands = pre-commit run -a [testenv:releasenotes] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -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 = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -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] # E123 closing bracket does not match indentation of opening bracket's line # W503 line break before binary operator # W504 line break after binary operator ignore = E123,W503,W504 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. # [H904] Delay string interpolations at logging calls. enable-extensions=H106,H203,H204,H205,H904 show-source = True [hacking] import_exceptions = ceilometer.i18n [flake8:local-plugins] extension = C301 = checks:no_log_warn C302 = checks:no_os_popen paths = ./ceilometer/hacking