networking-ovn-4.0.0/0000775000175100017510000000000013245511554014533 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/0000775000175100017510000000000013245511554017224 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/notes/0000775000175100017510000000000013245511554020354 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/notes/maintenance-thread-ee65c1ad317204c7.yaml0000666000175100017510000000033613245511145027175 0ustar zuulzuul00000000000000--- features: - | Added a new mechanism that periodically detects and fix inconsistencies between resources in the Neutron and OVN database. upgrade: - | Adds a new dependency on the Oslo Futurist library. networking-ovn-4.0.0/releasenotes/notes/ovn-native-nat-9bbc92f16edcf2f5.yaml0000666000175100017510000000043613245511145026553 0ustar zuulzuul00000000000000--- features: - | OVN native L3 implementation. The native implementation supports distributed routing for east-west traffic and centralized routing for north-south (floatingip and snat) traffic. Also supported is the Neutron L3 Configurable external gateway mode.networking-ovn-4.0.0/releasenotes/notes/ovsdb_connection-cef6b02c403163a3.yaml0000666000175100017510000000023113245511145026763 0ustar zuulzuul00000000000000--- deprecations: - The ``ovn`` group ``ovsdb_connection`` configuration option was deprecated in the ``Newton`` release and has now been removed. networking-ovn-4.0.0/releasenotes/notes/networking-ovn-0df373f5a7b22d19.yaml0000666000175100017510000000436013245511145026443 0ustar zuulzuul00000000000000--- features: - | Initial release of the OpenStack Networking service (neutron) integration with Open Virtual Network (OVN), a component of the `Open vSwitch `_ project. OVN provides the following features either via native implementation or conventional agents: * Layer-2 (native OVN implementation) * Layer-3 (native OVN implementation or conventional layer-3 agent) The native OVN implementation supports distributed routing. However, it currently lacks support for floating IP addresses, NAT, and the metadata proxy. * DHCP (native OVN implementation or conventional DHCP agent) The native implementation supports distributed DHCP. However, it currently lacks support for IPv6, internal DNS, and metadata proxy. * Metadata (conventional metadata agent) * DPDK - Usable with OVS via either the Linux kernel datapath or the DPDK datapath. * Trunk driver - Driver to back the neutron's 'trunk' service plugin The initial release also supports the following Networking service API extensions: * ``agent`` * ``Address Scopes`` \* * ``Allowed Address Pairs`` * ``Auto Allocated Topology Services`` * ``Availability Zone`` * ``Default Subnetpools`` * ``DHCP Agent Scheduler`` \*\* * ``Distributed Virtual Router`` \* * ``DNS Integration`` \* * ``HA Router extension`` \* * ``L3 Agent Scheduler`` \* * ``Multi Provider Network`` * ``Network Availability Zone`` \*\* * ``Network IP Availability`` * ``Neutron external network`` * ``Neutron Extra DHCP opts`` * ``Neutron Extra Route`` * ``Neutron L3 Configurable external gateway mode`` \* * ``Neutron L3 Router`` * ``Network MTU`` * ``Port Binding`` * ``Port Security`` * ``Provider Network`` * ``Quality of Service`` * ``Quota management support`` * ``RBAC Policies`` * ``Resource revision numbers`` * ``Router Availability Zone`` \* * ``security-group`` * ``standard-attr-description`` * ``Subnet Allocation`` * ``Tag support`` * ``Time Stamp Fields`` (\*) Only applicable if using the conventional layer-3 agent. (\*\*) Only applicable if using the conventional DHCP agent. networking-ovn-4.0.0/releasenotes/notes/ovn_dhcpv6-729158d634aa280e.yaml0000666000175100017510000000036013245511145025370 0ustar zuulzuul00000000000000--- features: - | OVN native DHCPv6 implementation. The native implementation supports distributed DHCPv6. Support Neutron IPv6 subnet whose "ipv6_address_mode" attribute is None, "dhcpv6_stateless", or "dhcpv6_stateful". networking-ovn-4.0.0/releasenotes/notes/bug-1606458-b9f809b3914bb203.yaml0000666000175100017510000000046313245511145025004 0ustar zuulzuul00000000000000--- deprecations: - The ``ovn`` group ``vif_type`` configuration option is deprecated and will be removed in the next release. The port VIF type is now determined based on the OVN chassis information when the port is bound to a host. [Bug `1606458 `_] networking-ovn-4.0.0/releasenotes/notes/SRIOV-port-binding-support-bug-1515005.yaml0000666000175100017510000000100213245511145027626 0ustar zuulzuul00000000000000--- prelude: > support for binding a SR-IOV port in a networking-ovn deployment. features: - networking-ovn ML2 mechanism driver now supports binding of direct(SR-IOV) ports. Traffic Control(TC) hardware offload framework for SR-IOV VFs was introduced in Linux kernel 4.8. Open vSwitch(OVS) 2.8 supports offloading OVS datapath rules using the TC framework. By using OVS version 2.8 and Linux kernel >= 4.8, a SR-IOV VF can be controlled via Openflow control plane. networking-ovn-4.0.0/releasenotes/notes/distributed-fip-0f5915ef9fd00626.yaml0000666000175100017510000000035113245511145026476 0ustar zuulzuul00000000000000--- prelude: > Support distributed floating IP. features: - | Now distributed floating IP is supported and a new configuration option ``enable_distributed_floating_ip`` is added to ovn group to control the feature. networking-ovn-4.0.0/releasenotes/notes/.placeholder0000666000175100017510000000000013245511145022623 0ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/notes/ovsdb-ssl-support-213ff378777cf946.yaml0000666000175100017510000000034313245511145026766 0ustar zuulzuul00000000000000--- prelude: > networking-ovn now supports the use of SSL for its OVSDB connections to the OVN databases. features: - networking-ovn now supports the use of SSL for its OVSDB connections to the OVN databases. networking-ovn-4.0.0/releasenotes/notes/internal_dns_support-83737015a1019222.yaml0000666000175100017510000000016113245511145027331 0ustar zuulzuul00000000000000--- features: - | Use native OVN DNS support if "dns" extension is loaded and "dns_domain" is defined. networking-ovn-4.0.0/releasenotes/source/0000775000175100017510000000000013245511554020524 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/newton.rst0000666000175100017510000000023213245511164022564 0ustar zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton networking-ovn-4.0.0/releasenotes/source/_static/0000775000175100017510000000000013245511554022152 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/_static/.placeholder0000666000175100017510000000000013245511145024421 0ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/pike.rst0000666000175100017510000000021713245511145022204 0ustar zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike networking-ovn-4.0.0/releasenotes/source/conf.py0000666000175100017510000002164713245511145022033 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # OVN 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', ] # openstackdocstheme options repository_name = 'openstack/networking-ovn' bug_project = 'networking-ovn' bug_tag = '' # 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 = u'Networking OVN Release Notes' copyright = u'2015, Networking OVN Developers' # Release notes are version independent. # 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 = 'sphinx' # 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' # 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 not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%Y-%m-%d %H:%M' # 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 = 'NetworkingOVNReleaseNotesdoc' # -- 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', 'NetworkingOVNReleaseNotes.tex', u'Networking OVN Release Notes Documentation', u'Networking OVN 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', 'networkingovnreleasenotes', u'Networking OVN Release Notes Documentation', [u'Networking OVN 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', 'NetworkingOVNReleaseNotes', u'Networking OVN Release Notes Documentation', u'Networking OVN Developers', 'NetworkingOVNReleaseNotes', '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/'] networking-ovn-4.0.0/releasenotes/source/locale/0000775000175100017510000000000013245511554021763 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/locale/en_GB/0000775000175100017510000000000013245511554022735 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175100017510000000000013245511554024522 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000666000175100017510000002230213245511145027550 0ustar zuulzuul00000000000000# Andi Chandler , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: Networking OVN Release Notes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-12-14 16:58+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-12-12 09:16+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en-GB\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "(\\*) Only applicable if using the conventional layer-3 agent." msgstr "(\\*) Only applicable if using the conventional layer-3 agent." msgid "(\\*\\*) Only applicable if using the conventional DHCP agent." msgstr "(\\*\\*) Only applicable if using the conventional DHCP agent." msgid "1.0.0" msgstr "1.0.0" msgid "2.0.0" msgstr "2.0.0" msgid "3.0.0" msgstr "3.0.0" msgid "4.0.0.0b2" msgstr "4.0.0.0b2" msgid "Current Series Release Notes" msgstr "Current Series Release Notes" msgid "" "DHCP (native OVN implementation or conventional DHCP agent) The native " "implementation supports distributed DHCP. However, it currently lacks " "support for IPv6, internal DNS, and metadata proxy." msgstr "" "DHCP (native OVN implementation or conventional DHCP agent) The native " "implementation supports distributed DHCP. However, it currently lacks " "support for IPv6, internal DNS, and metadata proxy." msgid "" "DPDK - Usable with OVS via either the Linux kernel datapath or the DPDK " "datapath." msgstr "" "DPDK - Usable with OVS via either the Linux kernel datapath or the DPDK " "datapath." msgid "Deprecation Notes" msgstr "Deprecation Notes" msgid "" "Initial release of the OpenStack Networking service (neutron) integration " "with Open Virtual Network (OVN), a component of the the `Open vSwitch " "`_ project. OVN provides the following features " "either via native implementation or conventional agents:" msgstr "" "Initial release of the OpenStack Networking service (neutron) integration " "with Open Virtual Network (OVN), a component of the the `Open vSwitch " "`_ project. OVN provides the following features " "either via native implementation or conventional agents:" msgid "Layer-2 (native OVN implementation)" msgstr "Layer-2 (native OVN implementation)" msgid "" "Layer-3 (native OVN implementation or conventional layer-3 agent) The native " "OVN implementation supports distributed routing. However, it currently lacks " "support for floating IP addresses, NAT, and the metadata proxy." msgstr "" "Layer-3 (native OVN implementation or conventional layer-3 agent) The native " "OVN implementation supports distributed routing. However, it currently lacks " "support for floating IP addresses, NAT, and the metadata proxy." msgid "Metadata (conventional metadata agent)" msgstr "Metadata (conventional metadata agent)" msgid "Networking OVN Release Notes" msgstr "Networking OVN Release Notes" msgid "New Features" msgstr "New Features" msgid "Newton Series Release Notes" msgstr "Newton Series Release Notes" msgid "" "Now distributed floating IP is supported and a new configuration option " "``enable_distributed_floating_ip`` is added to ovn group to control the " "feature." msgstr "" "Now distributed Floating IP is supported and a new configuration option " "``enable_distributed_floating_ip`` is added to ovn group to control the " "feature." msgid "" "OVN native DHCPv6 implementation. The native implementation supports " "distributed DHCPv6. Support Neutron IPv6 subnet whose \"ipv6_address_mode\" " "attribute is None, \"dhcpv6_stateless\", or \"dhcpv6_stateful\"." msgstr "" "OVN native DHCPv6 implementation. The native implementation supports " "distributed DHCPv6. Support Neutron IPv6 subnet whose \"ipv6_address_mode\" " "attribute is None, \"dhcpv6_stateless\", or \"dhcpv6_stateful\"." msgid "" "OVN native L3 implementation. The native implementation supports distributed " "routing for east-west traffic and centralized routing for north-south " "(floatingip and snat) traffic. Also supported is the Neutron L3 Configurable " "external gateway mode." msgstr "" "OVN native L3 implementation. The native implementation supports distributed " "routing for east-west traffic and centralised routing for north-south " "(floatingip and snat) traffic. Also supported is the Neutron L3 Configurable " "external gateway mode." msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "Prelude" msgstr "Prelude" msgid "Support distributed floating IP." msgstr "Support distributed Floating IP." msgid "" "The ``ovn`` group ``ovsdb_connection`` configuration option was deprecated " "in the ``Newton`` release and has now been removed." msgstr "" "The ``ovn`` group ``ovsdb_connection`` configuration option was deprecated " "in the ``Newton`` release and has now been removed." msgid "" "The ``ovn`` group ``vif_type`` configuration option is deprecated and will " "be removed in the next release. The port VIF type is now determined based on " "the OVN chassis information when the port is bound to a host. [Bug `1606458 " "`_]" msgstr "" "The ``ovn`` group ``vif_type`` configuration option is deprecated and will " "be removed in the next release. The port VIF type is now determined based on " "the OVN chassis information when the port is bound to a host. [Bug `1606458 " "`_]" msgid "" "The initial release also supports the following Networking service API " "extensions:" msgstr "" "The initial release also supports the following Networking service API " "extensions:" msgid "Trunk driver - Driver to back the neutron's 'trunk' service plugin" msgstr "Trunk driver - Driver to back the neutron's 'trunk' service plugin" msgid "``Address Scopes`` \\*" msgstr "``Address Scopes`` \\*" msgid "``Allowed Address Pairs``" msgstr "``Allowed Address Pairs``" msgid "``Auto Allocated Topology Services``" msgstr "``Auto Allocated Topology Services``" msgid "``Availability Zone``" msgstr "``Availability Zone``" msgid "``DHCP Agent Scheduler`` \\*\\*" msgstr "``DHCP Agent Scheduler`` \\*\\*" msgid "``DNS Integration`` \\*" msgstr "``DNS Integration`` \\*" msgid "``Default Subnetpools``" msgstr "``Default Subnetpools``" msgid "``Distributed Virtual Router`` \\*" msgstr "``Distributed Virtual Router`` \\*" msgid "``HA Router extension`` \\*" msgstr "``HA Router extension`` \\*" msgid "``L3 Agent Scheduler`` \\*" msgstr "``L3 Agent Scheduler`` \\*" msgid "``Multi Provider Network``" msgstr "``Multi Provider Network``" msgid "``Network Availability Zone`` \\*\\*" msgstr "``Network Availability Zone`` \\*\\*" msgid "``Network IP Availability``" msgstr "``Network IP Availability``" msgid "``Network MTU``" msgstr "``Network MTU``" msgid "``Neutron Extra DHCP opts``" msgstr "``Neutron Extra DHCP opts``" msgid "``Neutron Extra Route``" msgstr "``Neutron Extra Route``" msgid "``Neutron L3 Configurable external gateway mode`` \\*" msgstr "``Neutron L3 Configurable external gateway mode`` \\*" msgid "``Neutron L3 Router``" msgstr "``Neutron L3 Router``" msgid "``Neutron external network``" msgstr "``Neutron external network``" msgid "``Port Binding``" msgstr "``Port Binding``" msgid "``Port Security``" msgstr "``Port Security``" msgid "``Provider Network``" msgstr "``Provider Network``" msgid "``Quality of Service``" msgstr "``Quality of Service``" msgid "``Quota management support``" msgstr "``Quota management support``" msgid "``RBAC Policies``" msgstr "``RBAC Policies``" msgid "``Resource revision numbers``" msgstr "``Resource revision numbers``" msgid "``Router Availability Zone`` \\*" msgstr "``Router Availability Zone`` \\*" msgid "``Subnet Allocation``" msgstr "``Subnet Allocation``" msgid "``Tag support``" msgstr "``Tag support``" msgid "``Time Stamp Fields``" msgstr "``Time Stamp Fields``" msgid "``agent``" msgstr "``agent``" msgid "``security-group``" msgstr "``security-group``" msgid "``standard-attr-description``" msgstr "``standard-attr-description``" msgid "" "networking-ovn ML2 mechanism driver now supports binding of direct(SR-IOV) " "ports. Traffic Control(TC) hardware offload framework for SR-IOV VFs was " "introduced in Linux kernel 4.8. Open vSwitch(OVS) 2.8 supports offloading " "OVS datapath rules using the TC framework. By using OVS version 2.8 and " "Linux kernel >= 4.8, a SR-IOV VF can be controlled via Openflow control " "plane." msgstr "" "networking-ovn ML2 mechanism driver now supports binding of direct(SR-IOV) " "ports. Traffic Control(TC) hardware offload framework for SR-IOV VFs was " "introduced in Linux kernel 4.8. Open vSwitch(OVS) 2.8 supports offloading " "OVS datapath rules using the TC framework. By using OVS version 2.8 and " "Linux kernel >= 4.8, a SR-IOV VF can be controlled via Openflow control " "plane." msgid "" "networking-ovn now supports the use of SSL for its OVSDB connections to the " "OVN databases." msgstr "" "networking-ovn now supports the use of SSL for its OVSDB connections to the " "OVN databases." msgid "support for binding a SR-IOV port in a networking-ovn deployment." msgstr "support for binding a SR-IOV port in a networking-ovn deployment." networking-ovn-4.0.0/releasenotes/source/unreleased.rst0000666000175100017510000000015613245511145023405 0ustar zuulzuul00000000000000============================= Current Series Release Notes ============================= .. release-notes:: networking-ovn-4.0.0/releasenotes/source/index.rst0000666000175100017510000000024413245511145022363 0ustar zuulzuul00000000000000============================== Networking OVN Release Notes ============================== .. toctree:: :maxdepth: 1 unreleased pike ocata newton networking-ovn-4.0.0/releasenotes/source/ocata.rst0000666000175100017510000000026413245511164022346 0ustar zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata :earliest-version: 2.0.0 networking-ovn-4.0.0/releasenotes/source/_templates/0000775000175100017510000000000013245511554022661 5ustar zuulzuul00000000000000networking-ovn-4.0.0/releasenotes/source/_templates/.placeholder0000666000175100017510000000000013245511145025130 0ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/0000775000175100017510000000000013245511554015300 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/0000775000175100017510000000000013245511554016600 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/configuration/0000775000175100017510000000000013245511554021447 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/configuration/networking_ovn_metadata_agent.rst0000666000175100017510000000030613245511145030265 0ustar zuulzuul00000000000000================================= networking_ovn_metadata_agent.ini ================================= .. show-options:: :config-file: etc/oslo-config-generator/networking_ovn_metadata_agent.ini networking-ovn-4.0.0/doc/source/configuration/ml2_conf.rst0000666000175100017510000000016213245511145023675 0ustar zuulzuul00000000000000============ ml2_conf.ini ============ .. show-options:: :config-file: etc/oslo-config-generator/ml2_conf.ini networking-ovn-4.0.0/doc/source/configuration/index.rst0000666000175100017510000000113613245511145023307 0ustar zuulzuul00000000000000===================== Configuration Options ===================== This section provides a list of all possible options for each configuration file. Configuration Reference ----------------------- networking-ovn uses the following configuration files for its various services. .. toctree:: :glob: :maxdepth: 1 * Sample Configuration Files -------------------------- The following are sample configuration files for all networking-ovn. These are generated from code and reflect the current state of code in the networking-ovn repository. .. toctree:: :glob: :maxdepth: 1 samples/* networking-ovn-4.0.0/doc/source/configuration/samples/0000775000175100017510000000000013245511554023113 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/configuration/samples/networking_ovn_metadata_agent.rst0000666000175100017510000000054713245511145031740 0ustar zuulzuul00000000000000======================================== Sample networking_ovn_metadata_agent.ini ======================================== This sample configuration can also be viewed in `the raw format <../../_static/config_samples/networking_ovn_metadata_agent.conf.sample>`_. .. literalinclude:: ../../_static/config_samples/networking_ovn_metadata_agent.conf.sample networking-ovn-4.0.0/doc/source/configuration/samples/ml2_conf.rst0000666000175100017510000000037613245511145025350 0ustar zuulzuul00000000000000=================== Sample ml2_conf.ini =================== This sample configuration can also be viewed in `the raw format <../../_static/config_samples/ml2_conf.conf.sample>`_. .. literalinclude:: ../../_static/config_samples/ml2_conf.conf.sample networking-ovn-4.0.0/doc/source/_static/0000775000175100017510000000000013245511554020226 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/_static/.placeholder0000666000175100017510000000000013245511145022475 0ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/conf.py0000666000175100017510000000644613245511145020107 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', 'openstackdocstheme', 'oslo_config.sphinxext', 'oslo_config.sphinxconfiggen', ] # openstackdocstheme options repository_name = 'openstack/networking-ovn' bug_project = 'networking-ovn' bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'networking-ovn' # 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 # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' html_static_path = ['_static'] html_theme = 'openstackdocs' html_last_updated_fmt = '%Y-%m-%d %H:%M' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, u'%s Documentation' % project, u'OpenStack Foundation', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} # -- Options for oslo_config.sphinxconfiggen --------------------------------- _config_generator_config_files = [ 'ml2_conf.ini', 'networking_ovn_metadata_agent.ini', ] def _get_config_generator_config_definition(config_file): config_file_path = '../../etc/oslo-config-generator/%s' % conf # oslo_config.sphinxconfiggen appends '.conf.sample' to the filename, # strip file extentension (.conf or .ini). output_file_path = '_static/config_samples/%s' % conf.rsplit('.', 1)[0] return (config_file_path, output_file_path) config_generator_config_file = [ _get_config_generator_config_definition(conf) for conf in _config_generator_config_files ] networking-ovn-4.0.0/doc/source/contributor/0000775000175100017510000000000013245511554021152 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/contributor/design/0000775000175100017510000000000013245511554022423 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/contributor/design/native_dhcp.rst0000666000175100017510000000415513245511145025444 0ustar zuulzuul00000000000000Using the native DHCP feature provided by OVN ============================================= DHCPv4 ------ OVN implements a native DHCPv4 support which caters to the common use case of providing an IP address to a booting instance by providing stateless replies to DHCPv4 requests based on statically configured address mappings. To do this it allows a short list of DHCPv4 options to be configured and applied at each compute host running ovn-controller. OVN northbound db provides a table 'DHCP_Options' to store the DHCP options. Logical switch port has a reference to this table. When a subnet is created and enable_dhcp is True, a new entry is created in this table. The 'options' column stores the DHCPv4 options. These DHCPv4 options are included in the DHCPv4 reply by the ovn-controller when the VIF attached to the logical switch port sends a DHCPv4 request. In order to map the DHCP_Options row with the subnet, the OVN ML2 driver stores the subnet id in the 'external_ids' column. When a new port is created, the 'dhcpv4_options' column of the logical switch port refers to the DHCP_Options row created for the subnet of the port. If the port has multiple IPv4 subnets, then the first subnet in the 'fixed_ips' is used. If the port has extra DHCPv4 options defined, then a new entry is created in the DHCP_Options table for the port. The default DHCP options are obtained from the subnet DHCP_Options table and the extra DHCPv4 options of the port are overridden. In order to map the port DHCP_Options row with the port, the OVN ML2 driver stores both the subnet id and port id in the 'external_ids' column. If admin wants to disable native OVN DHCPv4 for any particular port, then the admin needs to define the 'dhcp_disabled' with the value 'true' in the extra DHCP options. Ex. neutron port-update \ --extra-dhcp-opt ip_version=4, opt_name=dhcp_disabled, opt_value=false DHCPv6 ------ OVN implements a native DHCPv6 support similar to DHCPv4. When a v6 subnet is created, the OVN ML2 driver will insert a new entry into DHCP_Options table only when the subnet 'ipv6_address_mode' is not 'slaac', and enable_dhcp is True. networking-ovn-4.0.0/doc/source/contributor/design/database_consistency.rst0000666000175100017510000004425113245511164027347 0ustar zuulzuul00000000000000================================ Neutron/OVN Database consistency ================================ This document presents the problem and proposes a solution for the data consistency issue between the Neutron and OVN databases. Although the focus of this document is OVN this problem is common enough to be present in other ML2 drivers (e.g OpenDayLight, BigSwitch, etc...). Some of them already contain a mechanism in place for dealing with it. Problem description =================== In a common Neutron deployment model there could have multiple Neutron API workers processing requests. For each request, the worker will update the Neutron database and then invoke the ML2 driver to translate the information to that specific SDN data model. There are at least two situations that could lead to some inconsistency between the Neutron and the SDN databases, for example: .. _problem_1: Problem 1: Neutron API workers race condition --------------------------------------------- .. code-block:: python In Neutron: with neutron_db_transaction: update_neutron_db() ml2_driver.update_port_precommit() ml2_driver.update_port_postcommit() In the ML2 driver: def update_port_postcommit: port = neutron_db.get_port() update_port_in_ovn(port) Imagine the case where a port is being updated twice and each request is being handled by a different API worker. The method responsible for updating the resource in the OVN (``update_port_postcommit``) is not atomic and invoked outside of the Neutron database transaction. This could lead to a problem where the order in which the updates are committed to the Neutron database are different than the order that they are committed to the OVN database, resulting in an inconsistency. This problem has been reported at `bug #1605089 `_. .. _problem_2: Problem 2: Backend failures --------------------------- Another situation is when the changes are already committed in Neutron but an exception is raised upon trying to update the OVN database (e.g lost connectivity to the ``ovsdb-server``). We currently don't have a good way of handling this problem, obviously it would be possible to try to immediately rollback the changes in the Neutron database and raise an exception but, that rollback itself is an operation that could also fail. Plus, rollbacks is not very straight forward when it comes to updates or deletes. In a case where a VM is being teared down and OVN fail to delete a port, re-creating that port in Neutron doesn't necessary fix the problem. The decommission of a VM involves many other things, in fact, we could make things even worse by leaving some dirty data around. I believe this is a problem that would be better dealt with by other methods. Proposed change =============== In order to fix the problems presented at the `Problem description`_ section this document proposes a solution based on the Neutron's ``revision_number`` attribute. In summary, for every resource in Neutron there's an attribute called ``revision_number`` which gets incremented on each update made on that resource. For example:: $ openstack port create --network nettest porttest ... | revision_number | 2 | ... $ openstack port set porttest --mac-address 11:22:33:44:55:66 $ mysql -e "use neutron; select standard_attr_id from ports where id=\"91c08021-ded3-4c5a-8d57-5b5c389f8e39\";" +------------------+ | standard_attr_id | +------------------+ | 1427 | +------------------+ $ mysql -e "use neutron; SELECT revision_number FROM standardattributes WHERE id=1427;" +-----------------+ | revision_number | +-----------------+ | 3 | +-----------------+ This document proposes a solution that will use the `revision_number` attribute for three things: #. Perform a compare-and-swap operation based on the resource version #. Guarantee the order of the updates (`Problem 1 `_) #. Detecting when resources in Neutron and OVN are out-of-sync But, before any of points above can be done we need to change the networking-ovn code to: #1 - Store the revision_number referent to a change in OVNDB ------------------------------------------------------------ To be able to compare the version of the resource in Neutron against the version in OVN we first need to know which version the OVN resource is present at. Fortunately, each table in the OVNDB contains a special column called ``external_ids`` which external systems (like Neutron/networking-ovn) can use to store information about its own resources that corresponds to the entries in OVNDB. So, every time a resource is created or updated in OVNDB by networking-ovn, the Neutron ``revision_number`` referent to that change will be stored in the ``external_ids`` column of that resource. That will allow networking-ovn to look at both databases and detect whether the version in OVN is up-to-date with Neutron or not. #2 - Ensure correctness when updating OVN ----------------------------------------- As stated in `Problem 1 `_, simultaneous updates to a single resource will race and, with the current code, the order in which these updates are applied is not guaranteed to be the correct order. That means that, if two or more updates arrives we can't prevent an older version of that update to be applied after a newer one. This document proposes creating a special ``OVSDB command`` that runs as part of the same transaction that is updating a resource in OVNDB to prevent changes with a lower ``revision_number`` to be applied in case the resource in OVN is at a higher ``revision_number`` already. This new OVSDB command needs to basically do two things: 1. Add a verify operation to the ``external_ids`` column in OVNDB so that if another client modifies that column mid-operation the transaction will be restarted. A better explanation of what "verify" does is described at the doc string of the `Transaction class`_ in the OVS code itself, I quote: Because OVSDB handles multiple clients, it can happen that between the time that OVSDB client A reads a column and writes a new value, OVSDB client B has written that column. Client A's write should not ordinarily overwrite client B's, especially if the column in question is a "map" column that contains several more or less independent data items. If client A adds a "verify" operation before it writes the column, then the transaction fails in case client B modifies it first. Client A will then see the new value of the column and compose a new transaction based on the new contents written by client B. 2. Compare the ``revision_number`` from the update against what is presently stored in OVNDB. If the version in OVNDB is already higher than the version in the update, abort the transaction. So basically this new command is responsible for guarding the OVN resource by not allowing old changes to be applied on top of new ones. Here's a scenario where two concurrent updates comes in the wrong order and how the solution above will deal with it: Neutron worker 1 (NW-1): Updates a port with address A (revision_number: 2) Neutron worker 2 (NW-2): Updates a port with address B (revision_number: 3) TXN 1: NW-2 transaction is committed first and the OVN resource now has RN 3 TXN 2: NW-1 transaction detects the change in the external_ids column and is restarted TXN 2: NW-1 the new command now sees that the OVN resource is at RN 3, which is higher than the update version (RN 2) and aborts the transaction. There's a bit more for the above to work with the current networking-ovn code, basically we need to tidy up the code to do two more things. 1. Consolidate changes to a resource in a single transaction. This is important regardless of this spec, having all changes to a resource done in a single transaction minimizes the risk of having half-changes written to the database in case of an eventual problem. This `should be done already `_ but it's important to have it here in case we find more examples like that as we code. 2. When doing partial updates, use the OVNDB as the source of comparison to create the deltas. Being able to do a partial update in a resource is important for performance reasons; it's a way to minimize the number of changes that will be performed in the database. Right now, some of the update() methods in networking-ovn creates the deltas using the *current* and *original* parameters that are passed to it. The *current* parameter is, as the name says, the current version of the object present in the Neutron DB. The *original* parameter is the previous version (current - 1) of that object. The problem of creating the deltas by comparing these two objects is because only the data in the Neutron DB is used for it. We need to stop using the *original* object for it and instead we should create the delta based on the *current* version of the Neutron DB against the data stored in the OVNDB to be able to detect the real differences between the two databases. So in summary, to guarantee the correctness of the updates this document proposes to: #. Create a new OVSDB command is responsible for comparing revision numbers and aborting the transaction, when needed. #. Consolidate changes to a resource in a single transaction (should be done already) #. When doing partial updates, create the deltas based in the current version in the Neutron DB and the OVNDB. #3 - Detect and fix out-of-sync resources ----------------------------------------- When things are working as expected the above changes should ensure that Neutron DB and OVNDB are in sync but, what happens when things go bad ? As per `Problem 2 `_, things like temporarily losing connectivity with the OVNDB could cause changes to fail to be committed and the databases getting out-of-sync. We need to be able to detect the resources that were affected by these failures and fix them. We do already have the means to do it, similar to what the `ovn_db_sync.py`_ script does we could fetch all the data from both databases and compare each resource. But, depending on the size of the deployment this can be really slow and costy. This document proposes an optimization for this problem to make it efficient enough so that we can run it periodically (as a periodic task) and not manually as a script anymore. First, we need to create an additional table in the Neutron database that would serve as a cache for the revision numbers in **OVNDB**. The new table schema could look this: ================ ======== =========== Column name Type Description ================ ======== =========== standard_attr_id Integer Primary key. The reference ID from the standardattributes table in Neutron for that resource. ONDELETE SET NULL. resource_uuid String The UUID of the resource resource_type String The type of the resource (e.g, Port, Router, ...) revision_number Integer The version of the object present in OVN acquired_at DateTime The time that the entry was create. For troubleshooting purposes updated_at DateTime The time that the entry was updated. For troubleshooting purposes ================ ======== =========== For the different actions: Create, update and delete; this table will be used as: 1. Create: In the create_*_precommit() method, we will create an entry in the new table within the same Neutron transaction. The revision_number column for the new entry will have a placeholder value until the resource is successfully created in OVNDB. In case we fail to create the resource in OVN (but succeed in Neutron) we still have the entry logged in the new table and this problem can be detected by fetching all resources where the revision_number column value is equal to the placeholder value. The pseudo-code will look something like this: .. code-block:: python def create_port_precommit(ctx, port): create_initial_revision(port['id'], revision_number=-1, session=ctx.session) def create_port_postcommit(ctx, port): create_port_in_ovn(port) bump_revision(port['id'], revision_number=port['revision_number']) 2. Update: For update it's simpler, we need to bump the revision number for that resource **after** the OVN transaction is committed in the update_*_postcommit() method. That way, if an update fails to be applied to OVN the inconsistencies can be detected by a JOIN between the new table and the ``standardattributes`` table where the revision_number columns does not match. The pseudo-code will look something like this: .. code-block:: python def update_port_postcommit(ctx, port): update_port_in_ovn(port) bump_revision(port['id'], revision_number=port['revision_number']) 3. Delete: The ``standard_attr_id`` column in the new table is a foreign key constraint with a ``ONDELETE=SET NULL`` set. That means that, upon Neutron deleting a resource the ``standard_attr_id`` column in the new table will be set to *NULL*. If deleting a resource succeeds in Neutron but fails in OVN, the inconsistency can be detect by looking at all resources that has a ``standard_attr_id`` equals to NULL. The pseudo-code will look something like this: .. code-block:: python def delete_port_postcommit(ctx, port): delete_port_in_ovn(port) delete_revision(port['id']) With the above optimization it's possible to create a periodic task that can run quite frequently to detect and fix the inconsistencies caused by random backend failures. .. note:: There's no lock linking both database updates in the postcommit() methods. So, it's true that the method bumping the revision_number column in the new table in Neutron DB could still race but, that should be fine because this table acts like a cache and the real revision_number has been written in OVNDB. The mechanism that will detect and fix the out-of-sync resources should detect this inconsistency as well and, based on the revision_number in OVNDB, decide whether to sync the resource or only bump the revision_number in the cache table (in case the resource is already at the right version). Refereces ========= * There's a chain of patches with a proof of concept for this approach, they start at: https://review.openstack.org/#/c/517049/ Alternatives ============ Journaling ---------- An alternative solution to this problem is *journaling*. The basic idea is to create another table in the Neutron database and log every operation (create, update and delete) instead of passing it directly to the SDN controller. A separated thread (or multiple instances of it) is then responsible for reading this table and applying the operations to the SDN backend. This approach has been used and validated by drivers such as `networking-odl `_. An attempt to implement this approach in *networking-ovn* can be found `here `_. Some things to keep in mind about this approach: * The code can get quite complex as this approach is not only about applying the changes to the SDN backend asynchronously. The dependencies between each resource as well as their operations also needs to be computed. For example, before attempting to create a router port the router that this port belongs to needs to be created. Or, before attempting to delete a network all the dependent resources on it (subnets, ports, etc...) needs to be processed first. * The number of journal threads running can cause problems. In my tests I had three controllers, each one with 24 CPU cores (Intel Xeon E5-2620 with hyperthreading enabled) and 64GB RAM. Running 1 journal thread per Neutron API worker has caused ``ovsdb-server`` to misbehave when under heavy pressure [1]_. Running multiple journal threads seem to be causing other types of problems `in other drivers as well `_. * When under heavy pressure [1]_, I noticed that the journal threads could come to a halt (or really slowed down) while the API workers were handling a lot of requests. This resulted in some operations taking more than a minute to be processed. This behaviour can be seem `in this screenshot `_. .. TODO find a better place to host that image * Given that the 1 journal thread per Neutron API worker approach is problematic, determining the right number of journal threads is also difficult. In my tests, I've noticed that 3 journal threads per controller worked better but that number was pure based on ``trial & error``. In production this number should probably be calculated based in the environment, perhaps something like `TripleO `_ (or any upper layer) would be in a better position to make that decision. * At least temporarily, the data in the Neutron database is duplicated between the normal tables and the journal one. * Some operations like creating a new resource via Neutron's API will return `HTTP 201 `_, which indicates that the resource has been created and is ready to be used, but as these resources are created asynchronously one could argue that the HTTP codes are now misleading. As a note, the resource will be created at the Neutron database by the time the HTTP request returns but it may not be present in the SDN backend yet. Given all considerations, this approach is still valid and the fact that it's already been used by other ML2 drivers makes it more open for collaboration and code sharing. .. _`Transaction class`: https://github.com/openvswitch/ovs/blob/3728b3b0316b44d1f9181be115b63ea85ff5883c/python/ovs/db/idl.py#L1014-L1055 .. _`ovn_db_sync.py`: https://github.com/openstack/networking-ovn/blob/a9af75cd3ce6cd6685b6435b325c97cacc83ce0e/networking_ovn/ovn_db_sync.py .. rubric:: Footnotes .. [1] I ran the tests using `Browbeat `_ which is basically orchestrate `Openstack Rally `_ and monitor the machine's usage of resources. networking-ovn-4.0.0/doc/source/contributor/design/data_model.rst0000666000175100017510000001607713245511145025257 0ustar zuulzuul00000000000000Mapping between Neutron and OVN data models ======================================================== The primary job of the Neutron OVN ML2 driver is to translate requests for resources into OVN's data model. Resources are created in OVN by updating the appropriate tables in the OVN northbound database (an ovsdb database). This document looks at the mappings between the data that exists in Neutron and what the resulting entries in the OVN northbound DB would look like. Network ---------- :: Neutron Network: id name subnets admin_state_up status tenant_id Once a network is created, we should create an entry in the Logical Switch table. :: OVN northbound DB Logical Switch: external_ids: { 'neutron:network_name': network.name } Subnet --------- :: Neutron Subnet: id name ip_version network_id cidr gateway_ip allocation_pools dns_nameservers host_routers tenant_id enable_dhcp ipv6_ra_mode ipv6_address_mode Once a subnet is created, we should create an entry in the DHCP Options table with the DHCPv4 or DHCPv6 options. :: OVN northbound DB DHCP_Options: cidr options external_ids: { 'subnet_id': subnet.id } Port ------- :: Neutron Port: id name network_id admin_state_up mac_address fixed_ips device_id device_owner tenant_id status When a port is created, we should create an entry in the Logical Switch Ports table in the OVN northbound DB. :: OVN Northbound DB Logical Switch Port: switch: reference to OVN Logical Switch router_port: (empty) name: port.id up: (read-only) macs: [port.mac_address] port_security: external_ids: {'neutron:port_name': port.name} If the port has extra DHCP options defined, we should create an entry in the DHCP Options table in the OVN northbound DB. :: OVN northbound DB DHCP_Options: cidr options external_ids: { 'subnet_id': subnet.id, 'port_id': port.id } Router ---------- :: Neutron Router: id name admin_state_up status tenant_id external_gw_info: network_id external_fixed_ips: list of dicts ip_address subnet_id ... :: OVN Northbound DB Logical Router: ip: default_gw: external_ids: Router Port -------------- ... :: OVN Northbound DB Logical Router Port: router: (reference to Logical Router) network: (reference to network this port is connected to) mac: external_ids: Security Groups ---------------- :: Neutron Port: id security_group: id network_id Neutron Security Group id name tenant_id security_group_rules Neutron Security Group Rule id tenant_id security_group_id direction remote_group_id ethertype protocol port_range_min port_range_max remote_ip_prefix ... :: OVN Northbound DB ACL Rule: lswitch: (reference to Logical Switch - port.network_id) priority: (0..65535) match: boolean expressions according to security rule Translation map (sg_rule ==> match expression) ----------------------------------------------- sg_rule.direction="Ingress" => "inport=port.id" sg_rule.direction="Egress" => "outport=port.id" sg_rule.ethertype => "eth.type" sg_rule.protocol => "ip.proto" sg_rule.port_range_min/port_range_max => "port_range_min <= tcp.src <= port_range_max" "port_range_min <= udp.src <= port_range_max" sg_rule.remote_ip_prefix => "ip4.src/mask, ip4.dst/mask, ipv6.src/mask, ipv6.dst/mask" (all match options for ACL can be found here: http://openvswitch.org/support/dist-docs/ovn-nb.5.html) action: "allow-related" log: true/false external_ids: {'neutron:port_id': port.id} {'neutron:security_rule_id': security_rule.id} Security groups maps between three neutron objects to one OVN-NB object, this enable us to do the mapping in various ways, depending on OVN capabilities The current implementation will use the first option in this list for simplicity, but all options are kept here for future reference 1) For every pair, define an ACL entry:: Leads to many ACL entries. acl.match = sg_rule converted example: ((inport==port.id) && (ip.proto == "tcp") && (1024 <= tcp.src <= 4095) && (ip.src==192.168.0.1/16)) external_ids: {'neutron:port_id': port.id} {'neutron:security_rule_id': security_rule.id} 2) For every pair, define an ACL entry:: Reduce the number of ACL entries. Means we have to manage the match field in case specific rule changes example: (((inport==port.id) && (ip.proto == "tcp") && (1024 <= tcp.src <= 4095) && (ip.src==192.168.0.1/16)) || ((outport==port.id) && (ip.proto == "udp") && (1024 <= tcp.src <= 4095)) || ((inport==port.id) && (ip.proto == 6) ) || ((inport==port.id) && (eth.type == 0x86dd))) (This example is a security group with four security rules) external_ids: {'neutron:port_id': port.id} {'neutron:security_group_id': security_group.id} 3) For every pair, define an ACL entry:: Reduce even more the number of ACL entries. Manage complexity increase example: (((inport==port.id) && (ip.proto == "tcp") && (1024 <= tcp.src <= 4095) && (ip.src==192.168.0.1/16)) || ((outport==port.id) && (ip.proto == "udp") && (1024 <= tcp.src <= 4095)) || ((inport==port.id) && (ip.proto == 6) ) || ((inport==port.id) && (eth.type == 0x86dd))) || (((inport==port2.id) && (ip.proto == "tcp") && (1024 <= tcp.src <= 4095) && (ip.src==192.168.0.1/16)) || ((outport==port2.id) && (ip.proto == "udp") && (1024 <= tcp.src <= 4095)) || ((inport==port2.id) && (ip.proto == 6) ) || ((inport==port2.id) && (eth.type == 0x86dd))) external_ids: {'neutron:security_group': security_group.id} Which option to pick depends on OVN match field length capabilities, and the trade off between better performance due to less ACL entries compared to the complexity to manage them. If the default behaviour is not "drop" for unmatched entries, a rule with lowest priority must be added to drop all traffic ("match==1") Spoofing protection rules are being added by OVN internally and we need to ignore the automatically added rules in Neutron networking-ovn-4.0.0/doc/source/contributor/design/metadata_api.rst0000666000175100017510000003663013245511164025575 0ustar zuulzuul00000000000000OpenStack Metadata API and OVN ============================== Introduction ------------ OpenStack Nova presents a metadata API to VMs similar to what is available on Amazon EC2. Neutron is involved in this process because the source IP address is not enough to uniquely identify the source of a metadata request since networks can have overlapping IP addresses. Neutron is responsible for intercepting metadata API requests and adding HTTP headers which uniquely identify the source of the request before forwarding it to the metadata API server. The purpose of this document is to propose a design for how to enable this functionality when OVN is used as the backend for OpenStack Neutron. Neutron and Metadata Today -------------------------- The following blog post describes how VMs access the metadata API through Neutron today. https://www.suse.com/communities/blog/vms-get-access-metadata-neutron/ In summary, we run a metadata proxy in either the router namespace or DHCP namespace. The DHCP namespace can be used when there’s no router connected to the network. The one downside to the DHCP namespace approach is that it requires pushing a static route to the VM through DHCP so that it knows to route metadata requests to the DHCP server IP address. * Instance sends a HTTP request for metadata to 169.254.169.254 * This request either hits the router or DHCP namespace depending on the route in the instance * The metadata proxy service in the namespace adds the following info to the request: * Instance IP (X-Forwarded-For header) * Router or Network-ID (X-Neutron-Network-Id or X-Neutron-Router-Id header) * The metadata proxy service sends this request to the metadata agent (outside the namespace) via a UNIX domain socket. * The neutron-metadata-agent service forwards the request to the Nova metadata API service by adding some new headers (instance ID and Tenant ID) to the request [0]. For proper operation, Neutron and Nova must be configured to communicate together with a shared secret. Neutron uses this secret to sign the Instance-ID header of the metadata request to prevent spoofing. This secret is configured through metadata_proxy_shared_secret on both nova and neutron configuration files (optional). [0] https://github.com/openstack/neutron/blob/master/neutron/agent/metadata/agent.py#L167 Neutron and Metadata with OVN ----------------------------- The current metadata API approach does not translate directly to OVN. There are no Neutron agents in use with OVN. Further, OVN makes no use of its own network namespaces that we could take advantage of like the original implementation makes use of the router and dhcp namespaces. We must use a modified approach that fits the OVN model. This section details a proposed approach. Overview of Proposed Approach ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The proposed approach would be similar to the *isolated network* case in the current ML2+OVS implementation. Therefore, we would be running a metadata proxy (haproxy) instance on every hypervisor for each network a VM on that host is connected to. The downside of this approach is that we'll be running more metadata proxies than we're doing now in case of routed networks (one per virtual router) but since haproxy is very lightweight and they will be idling most of the time, it shouldn't be a big issue overall. However, the major benefit of this approach is that we don't have to implement any scheduling logic to distribute metadata proxies across the nodes, nor any HA logic. This, however, can be evolved in the future as explained below in this document. Also, this approach relies on a new feature in OVN that we must implement first so that an OVN port can be present on *every* chassis (similar to *localnet* ports). This new type of logical port would be *localport* and we will never forward packets over a tunnel for these ports. We would only send packets to the local instance of a *localport*. **Step 1** - Create a port for the metadata proxy When using the DHCP agent today, Neutron automatically creates a port for the DHCP agent to use. We could do the same thing for use with the metadata proxy (haproxy). We'll create an OVN *localport* which will be present on every chassis and this port will have the same MAC/IP address on every host. Eventually, we can share the same neutron port for both DHCP and metadata. **Step 2** - Routing metadata API requests to the correct Neutron port This works similarly to the current approach. We would program OVN to include a static route in DHCP responses that routes metadata API requests to the *localport* that is hosting the metadata API proxy. Also, in case DHCP isn't enabled or the client ignores the route info, we will program a static route in the OVN logical router which will still get metadata requests directed to the right place. If the DHCP route does not work and the network is isolated, VMs won't get metadata, but this already happens with the current implementation so this approach doesn't introduce a regression. **Step 3** - Management of the namespaces and haproxy instances We propose a new agent in networking-ovn called ``neutron-ovn-metadata-agent``. We will run this agent on every hypervisor and it will be responsible for spawning the haproxy instances for managing the OVS interfaces, network namespaces and haproxy processes used to proxy metadata API requests. **Step 4** - Metadata API request processing Similar to the existing neutron metadata agent, ``neutron-ovn-metadata-agent`` must act as an intermediary between haproxy and the Nova metadata API service. ``neutron-ovn-metadata-agent`` is the process that will have access to the host networks where the Nova metadata API exists. Each haproxy will be in a network namespace not able to reach the appropriate host network. Haproxy will add the necessary headers to the metadata API request and then forward it to ``neutron-ovn-metadata-agent`` over a UNIX domain socket, which matches the behavior of the current metadata agent. Metadata Proxy Management Logic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In neutron-ovn-metadata-agent. * On startup: * Do a full sync. Ensure we have all the required metadata proxies running. For that, the agent would watch the ``Port_Binding`` table of the OVN Southbound database and look for all rows with the ``chassis`` column set to the host the agent is running on. For all those entries, make sure a metadata proxy instance is spawned for every ``datapath`` (Neutron network) those ports are attached to. The agent will keep record of the list of networks it currently has proxies running on by updating the ``external-ids`` key ``neutron-metadata-proxy-networks`` of the OVN ``Chassis`` record in the OVN Southbound database that corresponds to this host. As an example, this key would look like ``neutron-metadata-proxy-networks=NET1_UUID,NET4_UUID`` meaning that this chassis is hosting one or more VM's connected to networks 1 and 4 so we should have a metadata proxy instance running for each. Ensure any running metadata proxies no longer needed are torn down. * Open and maintain a connection to the OVN Northbound database (using the ovsdbapp library). On first connection, and anytime a reconnect happens: * Do a full sync. * Register a callback for creates/updates/deletes to Logical_Switch_Port rows to detect when metadata proxies should be started or torn down. ``neutron-ovn-metadata-agent`` will watch OVN Southbound database (``Port_Binding`` table) to detect when a port gets bound to its chassis. At that point, the agent will make sure that there's a metadata proxy attached to the OVN *localport* for the network which this port is connected to. * When a new network is created, we must create an OVN *localport* for use as a metadata proxy. * When a network is deleted, we must tear down the metadata proxy instance (if present) on the host and delete the corresponding OVN *localport*. Launching a metadata proxy includes: * Creating a network namespace:: $ sudo ip netns add * Creating a VETH pair (OVS upgrades that upgrade the kernel module will make internal ports go away and then brought back by OVS scripts. This may cause some disruption. Therefore, veth pairs are preferred over internal ports):: $ sudo ip link add 0 type veth peer name 1 * Creating an OVS interface and placing one end in that namespace:: $ sudo ovs-vsctl add-port br-int 0 $ sudo ip link set 1 netns * Setting the IP and MAC addresses on that interface:: $ sudo ip netns exec \ > ip link set 1 address $ sudo ip netns exec \ > ip addr add / dev 1 * Bringing the VETH pair up:: $ sudo ip netns exec ip link set 1 up $ sudo ip link set 0 up * Set ``external-ids:iface-id=NEUTRON_PORT_UUID`` on the OVS interface so that OVN is able to correlate this new OVS interface with the correct OVN logical port:: $ sudo ovs-vsctl set Interface 0 external_ids:iface-id= * Starting haproxy in this network namespace. * Add the network UUID to ``external-ids:neutron-metadata-proxy-networks`` on the Chassis table for our chassis in OVN Southbound database. Tearing down a metadata proxy includes: * Removing the network UUID from our chassis. * Stopping haproxy. * Deleting the OVS interface. * Deleting the network namespace. **Other considerations** This feature will be enabled by default in ``networking-ovn``, but there should be a way to disable it in case operators who don't need metadata don't have to deal with the complexity of it (haproxy instances, network namespaces, etcetera). In this case, the agent would not create the neutron ports needed for metadata. There could be a race condition when the first VM for a certain network boots on a hypervisor if it does so before the metadata proxy instance has been spawned. Right now, the ``vif-plugged`` event to Nova is sent out when the up column in the OVN Northbound database's Logical_Switch_Port table changes to True, indicating that the VIF is now up. To overcome this race condition we want to wait until all network UUID's to which this VM is connected to are present in ``external-ids:neutron-metadata-proxy-networks`` on the Chassis table for our chassis in OVN Southbound database. This will delay the event to Nova until the metadata proxy instance is up and running on the host ensuring the VM will be able to get the metadata on boot. Alternatives Considered ----------------------- Alternative 1: Build metadata support into ovn-controller ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We’ve been building some features useful to OpenStack directly into OVN. DHCP and DNS are key examples of things we’ve replaced by building them into ovn-controller. The metadata API case has some key differences that make this a less attractive solution: The metadata API is an OpenStack specific feature. DHCP and DNS by contrast are more clearly useful outside of OpenStack. Building metadata API proxy support into ovn-controller means embedding an HTTP and TCP stack into ovn-controller. This is a significant degree of undesired complexity. This option has been ruled out for these reasons. Alternative 2: Distributed metadata and High Availability ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this approach, we would spawn a metadata proxy per virtual router or per network (if isolated), thus, improving the number of metadata proxy instances running in the cloud. However, scheduling and HA have to be considered. Also, we wouldn't need the OVN *localport* implementation. ``neutron-ovn-metadata-agent`` would run on any host that we wish to be able to host metadata API proxies. These hosts must also be running ovn-controller. Each of these hosts will have a Chassis record in the OVN southbound database created by ovn-controller. The Chassis table has a column called ``external_ids`` which can be used for general metadata however we see fit. ``neutron-ovn-metadata-agent`` will update its corresponding Chassis record with an external-id of ``neutron-metadata-proxy-host=true`` to indicate that this OVN chassis is one capable of hosting metadata proxy instances. Once we have a way to determine hosts capable of hosting metadata API proxies, we can add logic to the networking-ovn ML2 driver that schedules metadata API proxies. This would be triggered by Neutron API requests. The output of the scheduling process would be setting an ``external_ids`` key on a Logical_Switch_Port in the OVN northbound database that corresponds with a metadata proxy. The key could be something like ``neutron-metadata-proxy-chassis=CHASSIS_HOSTNAME``. ``neutron-ovn-metadata-agent`` on each host would also be watching for updates to these Logical_Switch_Port rows. When it detects that a metadata proxy has been scheduled locally, it will kick off the process to spawn the local haproxy instance and get it plugged into OVN. HA must also be considered. We must know when a host goes down so that all metadata proxies scheduled to that host can be rescheduled. This is almost the exact same problem we have with L3 HA. When a host goes down, we need to trigger rescheduling gateways to other hosts. We should ensure that the approach used for rescheduling L3 gateways can be utilized for rescheduling metadata proxies, as well. In neutron-server (networking-ovn). Introduce a new networking-ovn configuration option: * ``[ovn] isolated_metadata=[True|False]`` Events that trigger scheduling a new metadata proxy: * If isolated_metadata is True * When a new network is created, we must create an OVN logical port for use as a metadata proxy and then schedule this to one of the ``neutron-ovn-metadata-agent`` instances. * If isolated_metadata is False * When a network is attached to or removed from a logical router, ensure that at least one of the networks has a metadata proxy port already created. If not, pick a network and create a metadata proxy port and then schedule it to an agent. At this point, we need to update the static route for metadata API. Events that trigger unscheduling an existing metadata proxy: * When a network is deleted, delete the metadata proxy port if it exists and unschedule it from a ``neutron-ovn-metadata-agent``. To schedule a new metadata proxy: * Determine the list of available OVN Chassis that can host metadata proxies by reading the ``Chassis`` table of the OVN Southbound database. Look for chassis that have an external-id of ``neutron-metadata-proxy-host=true``. * Of the available OVN chassis, choose the one “least loadedâ€, or currently hosting the fewest number of metadata proxies. * Set ``neutron-metadata-proxy-chassis=CHASSIS_HOSTNAME`` as an external-id on the Logical_Switch_Port in the OVN Northbound database that corresponds to the neutron port used for this metadata proxy. ``CHASSIS_HOSTNAME`` maps to the hostname row of a Chassis record in the OVN Southbound database. This approach has been ruled out for its complexity although we have analyzed the details deeply because, eventually, and depending on the implementation of L3 HA, we will want to evolve to it. Other References ---------------- * Haproxy config -- https://review.openstack.org/#/c/431691/34/neutron/agent/metadata/driver.py * https://engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html networking-ovn-4.0.0/doc/source/contributor/design/index.rst0000666000175100017510000000023113245511145024256 0ustar zuulzuul00000000000000============ Design Notes ============ .. toctree:: :maxdepth: 1 data_model native_dhcp ovn_worker metadata_api database_consistency networking-ovn-4.0.0/doc/source/contributor/design/ovn_worker.rst0000666000175100017510000000667013245511145025357 0ustar zuulzuul00000000000000OVN Neutron Worker and Port status handling =========================================== When the logical switch port's VIF is attached or removed to/from the ovn integration bridge, ovn-northd updates the Logical_Switch_Port.up to 'True' or 'False' accordingly. In order for the OVN Neutron ML2 driver to update the corresponding neutron port's status to 'ACTIVE' or 'DOWN' in the db, it needs to monitor the OVN Northbound db. A neutron worker is created for this purpose. The implementation of the ovn worker can be found here - 'networking_ovn.ovsdb.ovsdb_monitor.OvnWorker'. Neutron service will create 'n' api workers and 'm' rpc workers and 1 ovn worker (all these workers are separate processes). Api workers and rpc workers will create ovsdb idl client object ('ovs.db.idl.Idl') to connect to the OVN_Northbound db. See 'networking_ovn.ovsdb.impl_idl_ovn.OvsdbNbOvnIdl' and 'ovsdbapp.backend.ovs_idl.connection.Connection' classes for more details. Ovn worker will create 'networking_ovn.ovsdb.ovsdb_monitor.OvnIdl' class object (which inherits from 'ovs.db.idl.Idl') to connect to the OVN_Northbound db. On receiving the OVN_Northbound db updates from the ovsdb-server, 'notify' function of 'OVnIdl' is called by the parent class object. OvnIdl.notify() function passes the received events to the ovsdb_monitor.OvnDbNotifyHandler class. ovsdb_monitor.OvnDbNotifyHandler checks for any changes in the 'Logical_Switch_Port.up' and updates the neutron port's status accordingly. If 'notify_nova_on_port_status_changes' configuration is set, then neutron would notify nova on port status changes. ovsdb locks ----------- If there are multiple neutron servers running, then each neutron server will have one ovn worker which listens for the notify events. When the 'Logical_Switch_Port.up' is updated by ovn-northd, we do not want all the neutron servers to handle the event and update the neutron port status. In order for only one neutron server to handle the events, ovsdb locks are used. At start, each neutron server's ovn worker will try to acquire a lock with id - 'neutron_ovn_event_lock'. The ovn worker which has acquired the lock will handle the notify events. In case the neutron server with the lock dies, ovsdb-server will assign the lock to another neutron server in the queue. More details about the ovsdb locks can be found here [1] and [2] [1] - https://tools.ietf.org/html/draft-pfaff-ovsdb-proto-04#section-4.1.8 [2] - https://github.com/openvswitch/ovs/blob/branch-2.4/python/ovs/db/idl.py#L67 One thing to note is the ovn worker (with OvnIdl) do not carry out any transactions to the OVN Northbound db. Since the api and rpc workers are not configured with any locks, using the ovsdb lock on the OVN_Northbound and OVN_Southbound DBs by the ovn workers will not have any side effects to the transactions done by these api and rpc workers. Handling port status changes when neutron server(s) are down ------------------------------------------------------------ When neutron server starts, ovn worker would receive a dump of all logical switch ports as events. 'ovsdb_monitor.OvnDbNotifyHandler' would sync up if there are any inconsistencies in the port status. OVN Southbound DB Access ------------------------ The OVN Neutron ML2 driver has a need to acquire chassis information (hostname and physnets combinations). This is required initially to support routed networks. Thus, the plugin will initiate and maintain a connection to the OVN SB DB during startup. networking-ovn-4.0.0/doc/source/contributor/testing.rst0000666000175100017510000007337313245511145023374 0ustar zuulzuul00000000000000Testing with DevStack ===================== This document describes how to test OpenStack with OVN using DevStack. We will start by describing how to test on a single host. Single Node Test Environment ---------------------------- 1. Create a test system. It's best to use a throwaway dev system for running DevStack. Your best bet is to use either CentOS 7 or the latest Ubuntu LTS (16.04, Xenial). 2. Create the ``stack`` user. :: $ git clone https://git.openstack.org/openstack-dev/devstack.git $ sudo ./devstack/tools/create-stack-user.sh 3. Switch to the ``stack`` user and clone DevStack and networking-ovn. :: $ sudo su - stack $ git clone https://git.openstack.org/openstack-dev/devstack.git $ git clone https://git.openstack.org/openstack/networking-ovn.git 4. Configure DevStack to use networking-ovn. networking-ovn comes with a sample DevStack configuration file you can start with. For example, you may want to set some values for the various PASSWORD variables in that file so DevStack doesn't have to prompt you for them. Feel free to edit it if you'd like, but it should work as-is. :: $ cd devstack $ cp ../networking-ovn/devstack/local.conf.sample local.conf 5. Run DevStack. This is going to take a while. It installs a bunch of packages, clones a bunch of git repos, and installs everything from these git repos. :: $ ./stack.sh Once DevStack completes successfully, you should see output that looks something like this:: This is your host IP address: 172.16.189.6 This is your host IPv6 address: ::1 Horizon is now available at http://172.16.189.6/dashboard Keystone is serving at http://172.16.189.6/identity/ The default users are: admin and demo The password: password 2017-03-09 15:10:54.117 | stack.sh completed in 2110 seconds. Environment Variables --------------------- Once DevStack finishes successfully, we're ready to start interacting with OpenStack APIs. OpenStack provides a set of command line tools for interacting with these APIs. DevStack provides a file you can source to set up the right environment variables to make the OpenStack command line tools work. :: $ . openrc If you're curious what environment variables are set, they generally start with an OS prefix:: $ env | grep OS OS_REGION_NAME=RegionOne OS_IDENTITY_API_VERSION=2.0 OS_PASSWORD=password OS_AUTH_URL=http://192.168.122.8:5000/v2.0 OS_USERNAME=demo OS_TENANT_NAME=demo OS_VOLUME_API_VERSION=2 OS_CACERT=/opt/stack/data/CA/int-ca/ca-chain.pem OS_NO_CACHE=1 Default Network Configuration ----------------------------- By default, DevStack creates networks called ``private`` and ``public``. Run the following command to see the existing networks:: $ openstack network list +--------------------------------------+---------+----------------------------------------------------------------------------+ | ID | Name | Subnets | +--------------------------------------+---------+----------------------------------------------------------------------------+ | 40080dad-0064-480a-b1b0-592ae51c1471 | private | 5ff81545-7939-4ae0-8365-1658d45fa85c, da34f952-3bfc-45bb-b062-d2d973c1a751 | | 7ec986dd-aae4-40b5-86cf-8668feeeab67 | public | 60d0c146-a29b-4cd3-bd90-3745603b1a4b, f010c309-09be-4af2-80d6-e6af9c78bae7 | +--------------------------------------+---------+----------------------------------------------------------------------------+ A Neutron network is implemented as an OVN logical switch. networking-ovn creates logical switches with a name in the format neutron-. We can use ``ovn-nbctl`` to list the configured logical switches and see that their names correlate with the output from ``neutron net-list``:: $ ovn-nbctl ls-list 71206f5c-b0e6-49ce-b572-eb2e964b2c4e (neutron-40080dad-0064-480a-b1b0-592ae51c1471) 8d8270e7-fd51-416f-ae85-16565200b8a4 (neutron-7ec986dd-aae4-40b5-86cf-8668feeeab67) $ ovn-nbctl get Logical_Switch neutron-40080dad-0064-480a-b1b0-592ae51c1471 external_ids {"neutron:network_name"=private} Booting VMs ----------- In this section we'll go through the steps to create two VMs that have a virtual NIC attached to the ``private`` Neutron network. DevStack uses libvirt as the Nova backend by default. If KVM is available, it will be used. Otherwise, it will just run qemu emulated guests. This is perfectly fine for our testing, as we only need these VMs to be able to send and receive a small amount of traffic so performance is not very important. 1. Get the Network UUID. Start by getting the UUID for the ``private`` network from the output of ``neutron net-list`` from earlier and save it off:: $ PRIVATE_NET_ID=40080dad-0064-480a-b1b0-592ae51c1471 2. Create an SSH keypair. Next create an SSH keypair in Nova. Later, when we boot a VM, we'll ask that the public key be put in the VM so we can SSH into it. :: $ openstack keypair create demo > id_rsa_demo $ chmod 600 id_rsa_demo 3. Choose a flavor. We need minimal resources for these test VMs, so the ``m1.nano`` flavor is sufficient. :: $ openstack flavor list +----+-----------+-------+------+-----------+-------+-----------+ | ID | Name | RAM | Disk | Ephemeral | VCPUs | Is Public | +----+-----------+-------+------+-----------+-------+-----------+ | 1 | m1.tiny | 512 | 1 | 0 | 1 | True | | 2 | m1.small | 2048 | 20 | 0 | 1 | True | | 3 | m1.medium | 4096 | 40 | 0 | 2 | True | | 4 | m1.large | 8192 | 80 | 0 | 4 | True | | 42 | m1.nano | 64 | 0 | 0 | 1 | True | | 5 | m1.xlarge | 16384 | 160 | 0 | 8 | True | | 84 | m1.micro | 128 | 0 | 0 | 1 | True | | c1 | cirros256 | 256 | 0 | 0 | 1 | True | | d1 | ds512M | 512 | 5 | 0 | 1 | True | | d2 | ds1G | 1024 | 10 | 0 | 1 | True | | d3 | ds2G | 2048 | 10 | 0 | 2 | True | | d4 | ds4G | 4096 | 20 | 0 | 4 | True | +----+-----------+-------+------+-----------+-------+-----------+ $ FLAVOR_ID=42 4. Choose an image. DevStack imports the CirrOS image by default, which is perfect for our testing. It's a very small test image. :: $ openstack image list +--------------------------------------+--------------------------+--------+ | ID | Name | Status | +--------------------------------------+--------------------------+--------+ | 849a8db2-3754-4cf6-9271-491fa4ff7195 | cirros-0.3.5-x86_64-disk | active | +--------------------------------------+--------------------------+--------+ $ IMAGE_ID=849a8db2-3754-4cf6-9271-491fa4ff7195 5. Setup a security rule so that we can access the VMs we will boot up next. By default, DevStack does not allow users to access VMs, to enable that, we will need to add a rule. We will allow both ICMP and SSH. :: $ openstack security group rule create --ingress --ethertype IPv4 --dst-port 22 --protocol tcp default $ openstack security group rule create --ingress --ethertype IPv4 --protocol ICMP default $ openstack security group rule list +--------------------------------------+-------------+-----------+------------+--------------------------------------+--------------------------------------+ | ID | IP Protocol | IP Range | Port Range | Remote Security Group | Security Group | +--------------------------------------+-------------+-----------+------------+--------------------------------------+--------------------------------------+ ... | ade97198-db44-429e-9b30-24693d86d9b1 | tcp | 0.0.0.0/0 | 22:22 | None | a47b14da-5607-404a-8de4-3a0f1ad3649c | | d0861a98-f90e-4d1a-abfb-827b416bc2f6 | icmp | 0.0.0.0/0 | | None | a47b14da-5607-404a-8de4-3a0f1ad3649c | ... +--------------------------------------+-------------+-----------+------------+--------------------------------------+--------------------------------------+ $ neutron security-group-rule-create --direction ingress --ethertype IPv4 --port-range-min 22 --port-range-max 22 --protocol tcp default $ neutron security-group-rule-create --direction ingress --ethertype IPv4 --protocol ICMP default $ neutron security-group-rule-list +--------------------------------------+----------------+-----------+-----------+---------------+-----------------+ | id | security_group | direction | ethertype | protocol/port | remote | +--------------------------------------+----------------+-----------+-----------+---------------+-----------------+ | 8b2edbe6-790e-40ef-af54-c7b64ced8240 | default | ingress | IPv4 | 22/tcp | any | | 5bee0179-807b-41d7-ab16-6de6ac051335 | default | ingress | IPv4 | icmp | any | ... +--------------------------------------+----------------+-----------+-----------+---------------+-----------------+ 6. Boot some VMs. Now we will boot two VMs. We'll name them ``test1`` and ``test2``. :: $ openstack server create --nic net-id=$PRIVATE_NET_ID --flavor $FLAVOR_ID --image $IMAGE_ID --key-name demo test1 +-----------------------------+-----------------------------------------------------------------+ | Field | Value | +-----------------------------+-----------------------------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | | | OS-EXT-STS:power_state | NOSTATE | | OS-EXT-STS:task_state | scheduling | | OS-EXT-STS:vm_state | building | | OS-SRV-USG:launched_at | None | | OS-SRV-USG:terminated_at | None | | accessIPv4 | | | accessIPv6 | | | addresses | | | adminPass | BzAWWA6byGP6 | | config_drive | | | created | 2017-03-09T16:56:08Z | | flavor | m1.nano (42) | | hostId | | | id | d8b8084e-58ff-44f4-b029-a57e7ef6ba61 | | image | cirros-0.3.5-x86_64-disk (849a8db2-3754-4cf6-9271-491fa4ff7195) | | key_name | demo | | name | test1 | | progress | 0 | | project_id | b6522570f7344c06b1f24303abf3c479 | | properties | | | security_groups | name='default' | | status | BUILD | | updated | 2017-03-09T16:56:08Z | | user_id | c68f77f1d85e43eb9e5176380a68ac1f | | volumes_attached | | +-----------------------------+-----------------------------------------------------------------+ $ openstack server create --nic net-id=$PRIVATE_NET_ID --flavor $FLAVOR_ID --image $IMAGE_ID --key-name demo test2 +-----------------------------+-----------------------------------------------------------------+ | Field | Value | +-----------------------------+-----------------------------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | | | OS-EXT-STS:power_state | NOSTATE | | OS-EXT-STS:task_state | scheduling | | OS-EXT-STS:vm_state | building | | OS-SRV-USG:launched_at | None | | OS-SRV-USG:terminated_at | None | | accessIPv4 | | | accessIPv6 | | | addresses | | | adminPass | YB8dmt5v88JV | | config_drive | | | created | 2017-03-09T16:56:50Z | | flavor | m1.nano (42) | | hostId | | | id | 170d4f37-9299-4a08-b48b-2b90fce8e09b | | image | cirros-0.3.5-x86_64-disk (849a8db2-3754-4cf6-9271-491fa4ff7195) | | key_name | demo | | name | test2 | | progress | 0 | | project_id | b6522570f7344c06b1f24303abf3c479 | | properties | | | security_groups | name='default' | | status | BUILD | | updated | 2017-03-09T16:56:51Z | | user_id | c68f77f1d85e43eb9e5176380a68ac1f | | volumes_attached | | +-----------------------------+-----------------------------------------------------------------+ Once both VMs have been started, they will have a status of ``ACTIVE``:: $ openstack server list +--------------------------------------+-------+--------+---------------------------------------------------------+--------------------------+ | ID | Name | Status | Networks | Image Name | +--------------------------------------+-------+--------+---------------------------------------------------------+--------------------------+ | 170d4f37-9299-4a08-b48b-2b90fce8e09b | test2 | ACTIVE | private=fd5d:9d1b:457c:0:f816:3eff:fe24:49df, 10.0.0.3 | cirros-0.3.5-x86_64-disk | | d8b8084e-58ff-44f4-b029-a57e7ef6ba61 | test1 | ACTIVE | private=fd5d:9d1b:457c:0:f816:3eff:fe3f:953d, 10.0.0.10 | cirros-0.3.5-x86_64-disk | +--------------------------------------+-------+--------+---------------------------------------------------------+--------------------------+ Our two VMs have addresses of ``10.0.0.3`` and ``10.0.0.10``. If we list Neutron ports, there are two new ports with these addresses associated with them:: $ openstack port list +--------------------------------------+------+-------------------+-----------------------------------------------------------------------------------------------------+--------+ | ID | Name | MAC Address | Fixed IP Addresses | Status | +--------------------------------------+------+-------------------+-----------------------------------------------------------------------------------------------------+--------+ ... | 97c970b0-485d-47ec-868d-783c2f7acde3 | | fa:16:3e:3f:95:3d | ip_address='10.0.0.10', subnet_id='da34f952-3bfc-45bb-b062-d2d973c1a751' | ACTIVE | | | | | ip_address='fd5d:9d1b:457c:0:f816:3eff:fe3f:953d', subnet_id='5ff81545-7939-4ae0-8365-1658d45fa85c' | | | e003044d-334a-4de3-96d9-35b2d2280454 | | fa:16:3e:24:49:df | ip_address='10.0.0.3', subnet_id='da34f952-3bfc-45bb-b062-d2d973c1a751' | ACTIVE | | | | | ip_address='fd5d:9d1b:457c:0:f816:3eff:fe24:49df', subnet_id='5ff81545-7939-4ae0-8365-1658d45fa85c' | | ... +--------------------------------------+------+-------------------+-----------------------------------------------------------------------------------------------------+--------+ $ TEST1_PORT_ID=97c970b0-485d-47ec-868d-783c2f7acde3 $ TEST2_PORT_ID=e003044d-334a-4de3-96d9-35b2d2280454 Now we can look at OVN using ``ovn-nbctl`` to see the logical switch ports that were created for these two Neutron ports. The first part of the output is the OVN logical switch port UUID. The second part in parentheses is the logical switch port name. Neutron sets the logical switch port name equal to the Neutron port ID. :: $ ovn-nbctl lsp-list neutron-$PRIVATE_NET_ID ... fde1744b-e03b-46b7-b181-abddcbe60bf2 (97c970b0-485d-47ec-868d-783c2f7acde3) 7ce284a8-a48a-42f5-bf84-b2bca62cd0fe (e003044d-334a-4de3-96d9-35b2d2280454) ... These two ports correspond to the two VMs we created. VM Connectivity --------------- We can connect to our VMs by associating a floating IP address from the public network. :: $ openstack floating ip create --port $TEST1_PORT_ID public +---------------------+--------------------------------------+ | Field | Value | +---------------------+--------------------------------------+ | created_at | 2017-03-09T18:58:12Z | | description | | | fixed_ip_address | 10.0.0.10 | | floating_ip_address | 172.24.4.8 | | floating_network_id | 7ec986dd-aae4-40b5-86cf-8668feeeab67 | | id | 24ff0799-5a72-4a5b-abc0-58b301c9aee5 | | name | None | | port_id | 97c970b0-485d-47ec-868d-783c2f7acde3 | | project_id | b6522570f7344c06b1f24303abf3c479 | | revision_number | 1 | | router_id | ee51adeb-0dd8-4da0-ab6f-7ce60e00e7b0 | | status | DOWN | | updated_at | 2017-03-09T18:58:12Z | +---------------------+--------------------------------------+ Devstack does not wire up the public network by default so we must do that before connecting to this floating IP address. :: $ sudo ip link set br-ex up $ sudo ip route add 172.24.4.0/24 dev br-ex $ sudo ip addr add 172.24.4.1/24 dev br-ex Now you should be able to connect to the VM via its floating IP address. First, ping the address. :: $ ping -c 1 172.24.4.8 PING 172.24.4.8 (172.24.4.8) 56(84) bytes of data. 64 bytes from 172.24.4.8: icmp_seq=1 ttl=63 time=0.823 ms --- 172.24.4.8 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.823/0.823/0.823/0.000 ms Now SSH to the VM:: $ ssh -i id_rsa_demo cirros@172.24.4.8 hostname test1 Adding Another Compute Node --------------------------- After completing the earlier instructions for setting up devstack, you can use a second VM to emulate an additional compute node. This is important for OVN testing as it exercises the tunnels created by OVN between the hypervisors. Just as before, create a throwaway VM but make sure that this VM has a different host name. Having same host name for both VMs will confuse Nova and will not produce two hypervisors when you query nova hypervisor list later. Once the VM is setup, create the ``stack`` user:: $ git clone https://git.openstack.org/openstack-dev/devstack.git $ sudo ./devstack/tools/create-stack-user.sh Switch to the ``stack`` user and clone DevStack and networking-ovn:: $ sudo su - stack $ git clone https://git.openstack.org/openstack-dev/devstack.git $ git clone https://git.openstack.org/openstack/networking-ovn.git networking-ovn comes with another sample configuration file that can be used for this:: $ cd devstack $ cp ../networking-ovn/devstack/computenode-local.conf.sample local.conf You must set SERVICE_HOST in local.conf. The value should be the IP address of the main DevStack host. You must also set HOST_IP to the IP address of this new host. See the text in the sample configuration file for more information. Once that is complete, run DevStack:: $ cd devstack $ ./stack.sh This should complete in less time than before, as it's only running a single OpenStack service (nova-compute) along with OVN (ovn-controller, ovs-vswitchd, ovsdb-server). The final output will look something like this:: This is your host IP address: 172.16.189.30 This is your host IPv6 address: ::1 2017-03-09 18:39:27.058 | stack.sh completed in 1149 seconds. Now go back to your main DevStack host. You can use admin credentials to verify that the additional hypervisor has been added to the deployment:: $ cd devstack $ . openrc admin $ openstack hypervisor list +----+------------------------+-----------------+---------------+-------+ | ID | Hypervisor Hostname | Hypervisor Type | Host IP | State | +----+------------------------+-----------------+---------------+-------+ | 1 | centos7-ovn-devstack | QEMU | 172.16.189.6 | up | | 2 | centos7-ovn-devstack-2 | QEMU | 172.16.189.30 | up | +----+------------------------+-----------------+---------------+-------+ You can also look at OVN and OVS to see that the second host has shown up. For example, there will be a second entry in the Chassis table of the OVN_Southbound database. You can use the ``ovn-sbctl`` utility to list chassis, their configuration, and the ports bound to each of them:: $ ovn-sbctl show Chassis "ddc8991a-d838-4758-8d15-71032da9d062" hostname: "centos7-ovn-devstack" Encap vxlan ip: "172.16.189.6" options: {csum="true"} Encap geneve ip: "172.16.189.6" options: {csum="true"} Port_Binding "97c970b0-485d-47ec-868d-783c2f7acde3" Port_Binding "e003044d-334a-4de3-96d9-35b2d2280454" Port_Binding "cr-lrp-08d1f28d-cc39-4397-b12b-7124080899a1" Chassis "b194d07e-0733-4405-b795-63b172b722fd" hostname: "centos7-ovn-devstack-2.os1.phx2.redhat.com" Encap geneve ip: "172.16.189.30" options: {csum="true"} Encap vxlan ip: "172.16.189.30" options: {csum="true"} You can also see a tunnel created to the other compute node:: $ ovs-vsctl show ... Bridge br-int fail_mode: secure ... Port "ovn-b194d0-0" Interface "ovn-b194d0-0" type: geneve options: {csum="true", key=flow, remote_ip="172.16.189.30"} ... ... Provider Networks ----------------- Neutron has a "provider networks" API extension that lets you specify some additional attributes on a network. These attributes let you map a Neutron network to a physical network in your environment. The OVN ML2 driver is adding support for this API extension. It currently supports "flat" and "vlan" networks. Here is how you can test it: First you must create an OVS bridge that provides connectivity to the provider network on every host running ovn-controller. For trivial testing this could just be a dummy bridge. In a real environment, you would want to add a local network interface to the bridge, as well. :: $ ovs-vsctl add-br br-provider ovn-controller on each host must be configured with a mapping between a network name and the bridge that provides connectivity to that network. In this case we'll create a mapping from the network name "providernet" to the bridge 'br-provider". :: $ ovs-vsctl set open . \ external-ids:ovn-bridge-mappings=providernet:br-provider Now create a Neutron provider network. :: $ neutron net-create provider --shared \ --provider:physical_network providernet \ --provider:network_type flat Alternatively, you can define connectivity to a VLAN instead of a flat network: :: $ neutron net-create provider-101 --shared \ --provider:physical_network providernet \ --provider:network_type vlan \ --provider:segmentation_id 101 Observe that the OVN ML2 driver created a special logical switch port of type localnet on the logical switch to model the connection to the physical network. :: $ ovn-nbctl show ... switch 5bbccbbd-f5ca-411b-bad9-01095d6f1316 (neutron-729dbbee-db84-4a3d-afc3-82c0b3701074) port provnet-729dbbee-db84-4a3d-afc3-82c0b3701074 addresses: ["unknown"] ... $ ovn-nbctl lsp-get-type provnet-729dbbee-db84-4a3d-afc3-82c0b3701074 localnet $ ovn-nbctl lsp-get-options provnet-729dbbee-db84-4a3d-afc3-82c0b3701074 network_name=providernet If VLAN is used, there will be a VLAN tag shown on the localnet port as well. Finally, create a Neutron port on the provider network. :: $ neutron port-create provider or if you followed the VLAN example, it would be: :: $ neutron port-create provider-101 Run Unit Tests -------------- Run the unit tests in the local environment with ``tox``. :: $ tox -e py27 $ tox -e py27 networking_ovn.tests.unit.test_ovn_db_sync $ tox -e py27 networking_ovn.tests.unit.test_ovn_db_sync.TestOvnSbSyncML2 $ tox -e py27 networking_ovn.tests.unit.test_ovn_db_sync.TestOvnSbSyncML2 .test_ovn_sb_sync Run Functional Tests -------------------- you can run the functional tests with ``tox`` in your devstack environment: :: $ cd networking_ovn/tests/functional $ tox -e dsvm-functional $ tox -e dsvm-functional networking_ovn.tests.functional.test_mech_driver\ .TestPortBinding.test_port_binding_create_port If you want to run functional tests in your local clean environment, you may need a new working directory. :: $ export BASE=/opt/stack $ mkdir -p /opt/stack/new $ cd /opt/stack/new Next, get networking_ovn, neutron and devstack. :: $ git clone https://git.openstack.org/openstack/networking-ovn.git $ git clone https://git.openstack.org/openstack/neutron.git $ git clone https://git.openstack.org/openstack-dev/devstack.git Then execute the script to prepare the environment. :: $ cd networking-ovn/ $ ./networking_ovn/tests/contrib/gate_hook.sh Finally, run the functional tests with ``tox`` :: $ cd networking_ovn/tests/functional $ tox -e dsvm-functional $ tox -e dsvm-functional networking_ovn.tests.functional.test_mech_driver\ .TestPortBinding.test_port_binding_create_port Skydive ------- `Skydive `_ is an open source real-time network topology and protocols analyzer. It aims to provide a comprehensive way of understanding what is happening in the network infrastructure. Skydive works by utilizing agents to collect host-local information, and sending this information to a central agent for further analysis. It utilizes elasticsearch to store the data. To enable Skydive support with OVN and devstack, enable it on the control and compute nodes. On the control node, enable it as follows: :: enable_plugin skydive https://github.com/skydive-project/skydive.git enable_service skydive-analyzer On the compute nodes, enable it as follows: :: enable_plugin skydive https://github.com/skydive-project/skydive.git enable_service skydive-agent Troubleshooting --------------- If you run into any problems, take a look at our :doc:`/admin/troubleshooting` page. Additional Resources -------------------- See the documentation and other references linked from the :doc:`/admin/ovn` page. networking-ovn-4.0.0/doc/source/contributor/contributing.rst0000666000175100017510000000011613245511145024407 0ustar zuulzuul00000000000000============ Contributing ============ .. include:: ../../../CONTRIBUTING.rst networking-ovn-4.0.0/doc/source/contributor/index.rst0000666000175100017510000000023013245511164023005 0ustar zuulzuul00000000000000========================= Contributor Documentation ========================= .. toctree:: :maxdepth: 2 contributing testing design/index networking-ovn-4.0.0/doc/source/index.rst0000666000175100017510000000075213245511145020443 0ustar zuulzuul00000000000000.. networking-ovn documentation master file, created by sphinx-quickstart on Tue Jul 9 22:26:36 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. the main title comes from README.rst .. include:: ../../README.rst Contents -------- .. toctree:: :maxdepth: 2 admin/index install/index configuration/index contributor/index .. rubric:: Indices and tables * :ref:`genindex` * :ref:`search` networking-ovn-4.0.0/doc/source/install/0000775000175100017510000000000013245511554020246 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/install/index.rst0000666000175100017510000002413213245511145022107 0ustar zuulzuul00000000000000.. _installation: Install & Configuration ======================= The ``networking-ovn`` repository includes integration with DevStack that enables creation of a simple Open Virtual Network (OVN) development and test environment. This document discusses what is required for manual installation or integration into a production OpenStack deployment tool of conventional architectures that include the following types of nodes: * Controller - Runs OpenStack control plane services such as REST APIs and databases. * Network - Runs the layer-2, layer-3 (routing), DHCP, and metadata agents for the Networking service. Some agents optional. Usually provides connectivity between provider (public) and project (private) networks via NAT and floating IP addresses. .. note:: Some tools deploy these services on controller nodes. * Compute - Runs the hypervisor and layer-2 agent for the Networking service. Packaging --------- Open vSwitch (OVS) includes OVN beginning with version 2.5 and considers it experimental. The Networking service integration for OVN uses an independent package, typically ``networking-ovn``. Building OVS from source automatically installs OVN. For deployment tools using distribution packages, the ``openvswitch-ovn`` package for RHEL/CentOS and compatible distributions automatically installs ``openvswitch`` as a dependency. Ubuntu/Debian includes ``ovn-central``, ``ovn-host``, ``ovn-docker``, and ``ovn-common`` packages that pull in the appropriate Open vSwitch dependencies as needed. A ``python-networking-ovn`` RPM may be obtained for Fedora or CentOS from the RDO project. A package based on the ``master`` branch of ``networking-ovn`` can be found at https://trunk.rdoproject.org/. Fedora and CentOS RPM builds of OVS and OVN from the ``master`` branch of ``ovs`` can be found in this COPR repository: https://copr.fedorainfracloud.org/coprs/leifmadsen/ovs-master/. Controller nodes ---------------- Each controller node runs the OVS service (including dependent services such as ``ovsdb-server``) and the ``ovn-northd`` service. However, only a single instance of the ``ovsdb-server`` and ``ovn-northd`` services can operate in a deployment. However, deployment tools can implement active/passive high-availability using a management tool that monitors service health and automatically starts these services on another node after failure of the primary node. See the :ref:`faq` for more information. #. Install the ``openvswitch-ovn`` and ``networking-ovn`` packages. #. Start the OVS service. The central OVS service starts the ``ovsdb-server`` service that manages OVN databases. Using the *systemd* unit: .. code-block:: console # systemctl start openvswitch Using the ``ovs-ctl`` script: .. code-block:: console # /usr/share/openvswitch/scripts/ovs-ctl start --system-id="random" #. Configure the ``ovsdb-server`` component. By default, the ``ovsdb-server`` service only permits local access to databases via Unix socket. However, OVN services on compute nodes require access to these databases. * Permit remote database access. .. code-block:: console # ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640:IP_ADDRESS Replace ``IP_ADDRESS`` with the IP address of the management network interface on the controller node. .. note:: Permit remote access to TCP port 6640 on any host firewall. #. Start the ``ovn-northd`` service. Using the *systemd* unit: .. code-block:: console # systemctl start ovn-northd Using the ``ovn-ctl`` script: .. code-block:: console # /usr/share/openvswitch/scripts/ovn-ctl start_northd Options for *start_northd*: .. code-block:: console # /usr/share/openvswitch/scripts/ovn-ctl start_northd --help # ... # DB_NB_SOCK="/usr/local/etc/openvswitch/nb_db.sock" # DB_NB_PID="/usr/local/etc/openvswitch/ovnnb_db.pid" # DB_SB_SOCK="usr/local/etc/openvswitch/sb_db.sock" # DB_SB_PID="/usr/local/etc/openvswitch/ovnsb_db.pid" # ... #. Configure the Networking server component. The Networking service implements OVN as an ML2 driver. Edit the ``/etc/neutron/neutron.conf`` file: * Enable the ML2 core plug-in. .. code-block:: ini [DEFAULT] ... core_plugin = neutron.plugins.ml2.plugin.Ml2Plugin * Enable the OVN layer-3 service. .. code-block:: ini [DEFAULT] ... service_plugins = networking_ovn.l3.l3_ovn.OVNL3RouterPlugin #. Configure the ML2 plug-in. Edit the ``/etc/neutron/plugins/ml2/ml2_conf.ini`` file: * Configure the OVN mechanism driver, network type drivers, self-service (tenant) network types, and enable the port security extension. .. code-block:: ini [ml2] ... mechanism_drivers = ovn type_drivers = local,flat,vlan,geneve tenant_network_types = geneve extension_drivers = port_security overlay_ip_version = 4 .. note:: To enable VLAN self-service networks, add ``vlan`` to the ``tenant_network_types`` option. The first network type in the list becomes the default self-service network type. To use IPv6 for all overlay (tunnel) network endpoints, set the ``overlay_ip_version`` option to ``6``. * Configure the Geneve ID range and maximum header size. The IP version overhead (20 bytes for IPv4 (default) or 40 bytes for IPv6) is added to the maximum header size based on the ML2 ``overlay_ip_version`` option. .. code-block:: ini [ml2_type_geneve] ... vni_ranges = 1:65536 max_header_size = 38 .. note:: The Networking service uses the ``vni_ranges`` option to allocate network segments. However, OVN ignores the actual values. Thus, the ID range only determines the quantity of Geneve networks in the environment. For example, a range of ``5001:6000`` defines a maximum of 1000 Geneve networks. * Optionally, enable support for VLAN provider and self-service networks on one or more physical networks. If you specify only the physical network, only administrative (privileged) users can manage VLAN networks. Additionally specifying a VLAN ID range for a physical network enables regular (non-privileged) users to manage VLAN networks. The Networking service allocates the VLAN ID for each self-service network using the VLAN ID range for the physical network. .. code-block:: ini [ml2_type_vlan] ... network_vlan_ranges = PHYSICAL_NETWORK:MIN_VLAN_ID:MAX_VLAN_ID Replace ``PHYSICAL_NETWORK`` with the physical network name and optionally define the minimum and maximum VLAN IDs. Use a comma to separate each physical network. For example, to enable support for administrative VLAN networks on the ``physnet1`` network and self-service VLAN networks on the ``physnet2`` network using VLAN IDs 1001 to 2000: .. code-block:: ini network_vlan_ranges = physnet1,physnet2:1001:2000 * Enable security groups. .. code-block:: ini [securitygroup] ... enable_security_group = true .. note:: The ``firewall_driver`` option under ``[securitygroup]`` is ignored since the OVN ML2 driver itself handles security groups. * Configure OVS database access and L3 scheduler .. code-block:: ini [ovn] ... ovn_nb_connection = tcp:IP_ADDRESS:6641 ovn_sb_connection = tcp:IP_ADDRESS:6642 ovn_l3_scheduler = OVN_L3_SCHEDULER .. note:: Replace ``IP_ADDRESS`` with the IP address of the controller node that runs the ``ovsdb-server`` service. Replace ``OVN_L3_SCHEDULER`` with ``leastloaded`` if you want the scheduler to select a compute node with the least number of gateway ports or ``chance`` if you want the scheduler to randomly select a compute node from the available list of compute nodes. #. Start the ``neutron-server`` service. Network nodes ------------- Deployments using OVN native layer-3 and DHCP services do not require conventional network nodes because connectivity to external networks (including VTEP gateways) and routing occurs on compute nodes. Compute nodes ------------- Each compute node runs the OVS and ``ovn-controller`` services. The ``ovn-controller`` service replaces the conventional OVS layer-2 agent. #. Install the ``openvswitch-ovn`` and ``networking-ovn`` packages. #. Start the OVS service. Using the *systemd* unit: .. code-block:: console # systemctl start openvswitch Using the ``ovs-ctl`` script: .. code-block:: console # /usr/share/openvswitch/scripts/ovs-ctl start --system-id="random" #. Configure the OVS service. * Use OVS databases on the controller node. .. code-block:: console # ovs-vsctl set open . external-ids:ovn-remote=tcp:IP_ADDRESS:6642 Replace ``IP_ADDRESS`` with the IP address of the controller node that runs the ``ovsdb-server`` service. * Enable one or more overlay network protocols. At a minimum, OVN requires enabling the ``geneve`` protocol. Deployments using VTEP gateways should also enable the ``vxlan`` protocol. .. code-block:: console # ovs-vsctl set open . external-ids:ovn-encap-type=geneve,vxlan .. note:: Deployments without VTEP gateways can safely enable both protocols. * Configure the overlay network local endpoint IP address. .. code-block:: console # ovs-vsctl set open . external-ids:ovn-encap-ip=IP_ADDRESS Replace ``IP_ADDRESS`` with the IP address of the overlay network interface on the compute node. #. Start the ``ovn-controller`` service. Using the *systemd* unit: .. code-block:: console # systemctl start ovn-controller Using the ``ovn-ctl`` script: .. code-block:: console # /usr/share/openvswitch/scripts/ovn-ctl start_controller Verify operation ---------------- #. Each compute node should contain an ``ovn-controller`` instance. .. code-block:: console # ovn-sbctl show networking-ovn-4.0.0/doc/source/admin/0000775000175100017510000000000013245511554017670 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/admin/dpdk.rst0000666000175100017510000000164013245511145021343 0ustar zuulzuul00000000000000DPDK Support in OVN =================== Configuration Settings ---------------------- The following configuration parameter needs to be set in the Neutron ML2 plugin configuration file under the 'ovn' section to enable DPDK support. **vhost_sock_dir** This is the directory path in which vswitch daemon in all the compute nodes creates the virtio socket. Follow the instructions in INSTALL.DPDK.md in openvswitch source tree to know how to configure DPDK support in vswitch daemons. Configuration Settings in compute hosts --------------------------------------- Compute nodes configured with OVS DPDK should set the datapath_type as "netdev" for the integration bridge (managed by OVN) and all other bridges if connected to the integration bridge via patch ports. The below command can be used to set the datapath_type. .. code-block:: console $ sudo ovs-vsctl set Bridge br-int datapath_type=netdev networking-ovn-4.0.0/doc/source/admin/troubleshooting.rst0000666000175100017510000000300013245511145023640 0ustar zuulzuul00000000000000Troubleshooting =============== The following section describe common problems that you might encounter after/during the installation of OVN ML2 driver with Devstack and possible solutions to these problems. Launching VM's failure ----------------------- 1. Disable AppArmor Using Ubuntu you might encounter libvirt permission errors when trying to create OVS ports after launching a VM (from the nova compute log). Disabling AppArmor might help with this problem, check out https://help.ubuntu.com/community/AppArmor for instructions on how to disable it. Multi-Node setup not working ----------------------------- 1. Geneve kernel module not supported: By default OVN creates tunnels between compute nodes using the Geneve protocol. Older kernels (< 3.18) don't support the Geneve module and hence tunneling can't work. You can check it with this command 'lsmod | grep openvswitch' (geneve should show up in the result list) For more information about which upstream Kernel version is required for support of each tunnel type, see the answer to " Why do tunnels not work when using a kernel module other than the one packaged with Open vSwitch?" in the OVS FAQ: http://docs.openvswitch.org/en/latest/faq/ 2. MTU configuration: This problem is not unique to OVN but is amplified due to the possible larger size of geneve header compared to other common tunneling protocols (VXLAN). If you are using VM's as compute nodes make sure that you either lower the MTU size on the virtual interface or enable fragmentation on it. networking-ovn-4.0.0/doc/source/admin/features.rst0000666000175100017510000001020313245511164022233 0ustar zuulzuul00000000000000.. _features: Features ======== Open Virtual Network (OVN) offers the following virtual network services: * Layer-2 (switching) Native implementation. Replaces the conventional Open vSwitch (OVS) agent. * Layer-3 (routing) Native implementation that supports distributed routing. Replaces the conventional Neutron L3 agent. * DHCP Native distributed implementation. Replaces the conventional Neutron DHCP agent. Note that the native implementation does not yet support DNS or Metadata features. * DPDK OVN and networking-ovn may be used with OVS using either the Linux kernel datapath or the DPDK datapath. * Trunk driver Uses OVN's functionality of parent port and port tagging to support trunk service plugin. One has to enable the 'trunk' service plugin in neutron configuration files to use this feature. The following Neutron API extensions are supported with OVN: +----------------------------------+---------------------------+ | Extension Name | Extension Alias | +==================================+===========================+ | Allowed Address Pairs | allowed-address-pairs | +----------------------------------+---------------------------+ | Auto Allocated Topology Services | auto-allocated-topology | +----------------------------------+---------------------------+ | Availability Zone | availability_zone | +----------------------------------+---------------------------+ | Default Subnetpools | default-subnetpools | +----------------------------------+---------------------------+ | Multi Provider Network | multi-provider | +----------------------------------+---------------------------+ | Network IP Availability | network-ip-availability | +----------------------------------+---------------------------+ | Neutron external network | external-net | +----------------------------------+---------------------------+ | Neutron Extra DHCP opts | extra_dhcp_opt | +----------------------------------+---------------------------+ | Neutron Extra Route | extraroute | +----------------------------------+---------------------------+ | Neutron L3 external gateway | ext-gw-mode | +----------------------------------+---------------------------+ | Neutron L3 Router | router | +----------------------------------+---------------------------+ | Network MTU | net-mtu | +----------------------------------+---------------------------+ | Port Binding | binding | +----------------------------------+---------------------------+ | Port Security | port-security | +----------------------------------+---------------------------+ | Provider Network | provider | +----------------------------------+---------------------------+ | Quality of Service | qos | +----------------------------------+---------------------------+ | Quota management support | quotas | +----------------------------------+---------------------------+ | RBAC Policies | rbac-policies | +----------------------------------+---------------------------+ | Resource revision numbers | revisions | +----------------------------------+---------------------------+ | security-group | security-group | +----------------------------------+---------------------------+ | standard-attr-description | standard-attr-description | +----------------------------------+---------------------------+ | Subnet Allocation | subnet_allocation | +----------------------------------+---------------------------+ | Tag support | tag | +----------------------------------+---------------------------+ | Time Stamp Fields | timestamp_core | +----------------------------------+---------------------------+ networking-ovn-4.0.0/doc/source/admin/refarch/0000775000175100017510000000000013245511554021302 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/admin/refarch/launch-instance-provider-network.rst0000666000175100017510000012010313245511145030422 0ustar zuulzuul00000000000000.. _refarch-launch-instance-provider-network: Launch an instance on a provider network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #. On the controller node, source the credentials for a regular (non-privileged) project. The following example uses the ``demo`` project. #. On the controller node, launch an instance using the UUID of the provider network. .. code-block:: console $ openstack server create --flavor m1.tiny --image cirros \ --nic net-id=0243277b-4aa8-46d8-9e10-5c9ad5e01521 \ --security-group default --key-name mykey provider-instance +--------------------------------------+-----------------------------------------------+ | Property | Value | +--------------------------------------+-----------------------------------------------+ | OS-DCF:diskConfig | MANUAL | | OS-EXT-AZ:availability_zone | nova | | OS-EXT-STS:power_state | 0 | | OS-EXT-STS:task_state | scheduling | | OS-EXT-STS:vm_state | building | | OS-SRV-USG:launched_at | - | | OS-SRV-USG:terminated_at | - | | accessIPv4 | | | accessIPv6 | | | adminPass | hdF4LMQqC5PB | | config_drive | | | created | 2015-09-17T21:58:18Z | | flavor | m1.tiny (1) | | hostId | | | id | 181c52ba-aebc-4c32-a97d-2e8e82e4eaaf | | image | cirros (38047887-61a7-41ea-9b49-27987d5e8bb9) | | key_name | mykey | | metadata | {} | | name | provider-instance | | os-extended-volumes:volumes_attached | [] | | progress | 0 | | security_groups | default | | status | BUILD | | tenant_id | f5b2ccaa75ac413591f12fcaa096aa5c | | updated | 2015-09-17T21:58:18Z | | user_id | 684286a9079845359882afc3aa5011fb | +--------------------------------------+-----------------------------------------------+ OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations when launching an instance. #. The OVN mechanism driver creates a logical port for the instance. .. code-block:: console _uuid : cc891503-1259-47a1-9349-1c0293876664 addresses : ["fa:16:3e:1c:ca:6a 203.0.113.103"] enabled : true external_ids : {"neutron:port_name"=""} name : "cafd4862-c69c-46e4-b3d2-6141ce06b205" options : {} parent_name : [] port_security : ["fa:16:3e:1c:ca:6a 203.0.113.103"] tag : [] type : "" up : true #. The OVN mechanism driver updates the appropriate Address Set entry with the address of this instance: .. code-block:: console _uuid : d0becdea-e1ed-48c4-9afc-e278cdef4629 addresses : ["203.0.113.103"] external_ids : {"neutron:security_group_name"=default} name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" #. The OVN mechanism driver creates ACL entries for this port and any other ports in the project. .. code-block:: console _uuid : f8d27bfc-4d74-4e73-8fac-c84585443efd action : drop direction : from-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "inport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip" priority : 1001 _uuid : a61d0068-b1aa-4900-9882-e0671d1fc131 action : allow direction : to-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "outport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip4 && ip4.src == 203.0.113.0/24 && udp && udp.src == 67 && udp.dst == 68" priority : 1002 _uuid : a5a787b8-7040-4b63-a20a-551bd73eb3d1 action : allow-related direction : from-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "inport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip6" priority : 1002 _uuid : 7b3f63b8-e69a-476c-ad3d-37de043232b2 action : allow-related direction : to-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "outport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip4 && ip4.src = $as_ip4_90a78a43_b5649_4bee_8822_21fcccab58dc" priority : 1002 _uuid : 36dbb1b1-cd30-4454-a0bf-923646eb7c3f action : allow direction : from-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "inport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip4 && (ip4.dst == 255.255.255.255 || ip4.dst == 203.0.113.0/24) && udp && udp.src == 68 && udp.dst == 67" priority : 1002 _uuid : 05a92f66-be48-461e-a7f1-b07bfbd3e667 action : allow-related direction : from-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "inport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip4" priority : 1002 _uuid : 37f18377-d6c3-4c44-9e4d-2170710e50ff action : drop direction : to-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "outport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip" priority : 1001 _uuid : 6d4db3cf-c1f1-4006-ad66-ae582a6acd21 action : allow-related direction : to-lport external_ids : {"neutron:lport"="cafd4862-c69c-46e4-b3d2-6141ce06b205"} log : false match : "outport == \"cafd4862-c69c-46e4-b3d2-6141ce06b205\" && ip6 && ip6.src = $as_ip6_90a78a43_b5649_4bee_8822_21fcccab58dc" priority : 1002 #. The OVN mechanism driver updates the logical switch information with the UUIDs of these objects. .. code-block:: console _uuid : 924500c4-8580-4d5f-a7ad-8769f6e58ff5 acls : [05a92f66-be48-461e-a7f1-b07bfbd3e667, 36dbb1b1-cd30-4454-a0bf-923646eb7c3f, 37f18377-d6c3-4c44-9e4d-2170710e50ff, 7b3f63b8-e69a-476c-ad3d-37de043232b2, a5a787b8-7040-4b63-a20a-551bd73eb3d1, a61d0068-b1aa-4900-9882-e0671d1fc131, f8d27bfc-4d74-4e73-8fac-c84585443efd] external_ids : {"neutron:network_name"=provider} name : "neutron-670efade-7cd0-4d87-8a04-27f366eb8941" ports : [38cf8b52-47c4-4e93-be8d-06bf71f6a7c9, 5e144ab9-3e08-4910-b936-869bbbf254c8, a576b812-9c3e-4cfb-9752-5d8500b3adf9, cc891503-1259-47a1-9349-1c0293876664] #. The OVN northbound service creates port bindings for the logical ports and adds them to the appropriate multicast group. * Port bindings .. code-block:: console _uuid : e73e3fcd-316a-4418-bbd5-a8a42032b1c3 chassis : fc5ab9e7-bc28-40e8-ad52-2949358cc088 datapath : bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 logical_port : "cafd4862-c69c-46e4-b3d2-6141ce06b205" mac : ["fa:16:3e:1c:ca:6a 203.0.113.103"] options : {} parent_port : [] tag : [] tunnel_key : 4 type : "" * Multicast groups .. code-block:: console _uuid : 39b32ccd-fa49-4046-9527-13318842461e datapath : bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 name : _MC_flood ports : [030024f4-61c3-4807-859b-07727447c427, 904c3108-234d-41c0-b93c-116b7e352a75, cc5bcd19-bcae-4e29-8cee-3ec8a8a75d46, e73e3fcd-316a-4418-bbd5-a8a42032b1c3] tunnel_key : 65535 #. The OVN northbound service translates the Address Set change into the new Address Set in the OVN southbound database. .. code-block:: console _uuid : 2addbee3-7084-4fff-8f7b-15b1efebdaff addresses : ["203.0.113.103"] name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" #. The OVN northbound service translates the ACL and logical port objects into logical flows in the OVN southbound database. .. code-block:: console Datapath: bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.src == {fa:16:3e:1c:ca:6a}), action=(next;) table= 1( ls_in_port_sec_ip), priority= 90, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.src == fa:16:3e:1c:ca:6a && ip4.src == {203.0.113.103}), action=(next;) table= 1( ls_in_port_sec_ip), priority= 90, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.src == fa:16:3e:1c:ca:6a && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(next;) table= 1( ls_in_port_sec_ip), priority= 80, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.src == fa:16:3e:1c:ca:6a && ip), action=(drop;) table= 2( ls_in_port_sec_nd), priority= 90, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.src == fa:16:3e:1c:ca:6a && arp.sha == fa:16:3e:1c:ca:6a && (arp.spa == 203.0.113.103 )), action=(next;) table= 2( ls_in_port_sec_nd), priority= 80, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && (arp || nd)), action=(drop;) table= 3( ls_in_pre_acl), priority= 110, match=(nd), action=(next;) table= 3( ls_in_pre_acl), priority= 100, match=(ip), action=(reg0[0] = 1; next;) table= 6( ls_in_acl), priority=65535, match=(ct.inv), action=(drop;) table= 6( ls_in_acl), priority=65535, match=(nd), action=(next;) table= 6( ls_in_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv), action=(next;) table= 6( ls_in_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv), action=(next;) table= 6( ls_in_acl), priority= 2002, match=(ct.new && (inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip6)), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2002, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip4 && (ip4.dst == 255.255.255.255 || ip4.dst == 203.0.113.0/24) && udp && udp.src == 68 && udp.dst == 67), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2002, match=(ct.new && (inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip4)), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2001, match=(inport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip), action=(drop;) table= 6( ls_in_acl), priority= 1, match=(ip), action=(reg0[1] = 1; next;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 203.0.113.103 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:1c:ca:6a; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:1c:ca:6a; arp.tpa = arp.spa; arp.spa = 203.0.113.103; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:1c:ca:6a), action=(outport = "cafd4862-c69c-46e4-b3d2-6141ce06b205"; output;) Datapath: bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 Pipeline: egress table= 1( ls_out_pre_acl), priority= 110, match=(nd), action=(next;) table= 1( ls_out_pre_acl), priority= 100, match=(ip), action=(reg0[0] = 1; next;) table= 4( ls_out_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv), action=(next;) table= 4( ls_out_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv), action=(next;) table= 4( ls_out_acl), priority=65535, match=(ct.inv), action=(drop;) table= 4( ls_out_acl), priority=65535, match=(nd), action=(next;) table= 4( ls_out_acl), priority= 2002, match=(ct.new && (outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip6 && ip6.src == $as_ip6_90a78a43_b549_4bee_8822_21fcccab58dc)), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2002, match=(ct.new && (outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip4 && ip4.src == $as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc)), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2002, match=(outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip4 && ip4.src == 203.0.113.0/24 && udp && udp.src == 67 && udp.dst == 68), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2001, match=(outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && ip), action=(drop;) table= 4( ls_out_acl), priority= 1, match=(ip), action=(reg0[1] = 1; next;) table= 6( ls_out_port_sec_ip), priority= 90, match=(outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.dst == fa:16:3e:1c:ca:6a && ip4.dst == {255.255.255.255, 224.0.0.0/4, 203.0.113.103}), action=(next;) table= 6( ls_out_port_sec_ip), priority= 80, match=(outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.dst == fa:16:3e:1c:ca:6a && ip), action=(drop;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "cafd4862-c69c-46e4-b3d2-6141ce06b205" && eth.dst == {fa:16:3e:1c:ca:6a}), action=(output;) #. The OVN controller service on each compute node translates these objects into flows on the integration bridge ``br-int``. Exact flows depend on whether the compute node containing the instance also contains a DHCP agent on the subnet. * On the compute node containing the instance, the Compute service creates a port that connects the instance to the integration bridge and OVN creates the following flows: .. code-block:: console # ovs-ofctl show br-int OFPT_FEATURES_REPLY (xid=0x2): dpid:000022024a1dc045 n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst 9(tapcafd4862-c6): addr:fe:16:3e:1c:ca:6a config: 0 state: 0 current: 10MB-FD COPPER speed: 10 Mbps now, 0 Mbps max .. code-block:: console cookie=0x0, duration=184.992s, table=0, n_packets=175, n_bytes=15270, idle_age=15, priority=100,in_port=9 actions=load:0x3->NXM_NX_REG5[],load:0x4->OXM_OF_METADATA[], load:0x4->NXM_NX_REG6[],resubmit(,16) cookie=0x0, duration=191.687s, table=16, n_packets=175, n_bytes=15270, idle_age=15, priority=50,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=resubmit(,17) cookie=0x0, duration=191.687s, table=17, n_packets=2, n_bytes=684, idle_age=112, priority=90,udp,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a,nw_src=0.0.0.0, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=resubmit(,18) cookie=0x0, duration=191.687s, table=17, n_packets=146, n_bytes=12780, idle_age=20, priority=90,ip,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a,nw_src=203.0.113.103 actions=resubmit(,18) cookie=0x0, duration=191.687s, table=17, n_packets=17, n_bytes=1386, idle_age=92, priority=80,ipv6,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=191.687s, table=17, n_packets=0, n_bytes=0, idle_age=191, priority=80,ip,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=191.687s, table=18, n_packets=10, n_bytes=420, idle_age=15, priority=90,arp,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a,arp_spa=203.0.113.103, arp_sha=fa:16:3e:1c:ca:6a actions=resubmit(,19) cookie=0x0, duration=191.687s, table=18, n_packets=0, n_bytes=0, idle_age=191, priority=80,icmp6,reg6=0x4,metadata=0x4, icmp_type=136,icmp_code=0 actions=drop cookie=0x0, duration=191.687s, table=18, n_packets=0, n_bytes=0, idle_age=191, priority=80,icmp6,reg6=0x4,metadata=0x4, icmp_type=135,icmp_code=0 actions=drop cookie=0x0, duration=191.687s, table=18, n_packets=0, n_bytes=0, idle_age=191, priority=80,arp,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.033s, table=19, n_packets=0, n_bytes=0, idle_age=75, priority=110,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=75.032s, table=19, n_packets=0, n_bytes=0, idle_age=75, priority=110,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=75.032s, table=19, n_packets=34, n_bytes=5170, idle_age=49, priority=100,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=75.032s, table=19, n_packets=0, n_bytes=0, idle_age=75, priority=100,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=65535,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=65535,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=13, n_bytes=1118, idle_age=49, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x4 actions=resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x4 actions=resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=65535,ct_state=+inv+trk,metadata=0x4 actions=drop cookie=0x0, duration=75.033s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=2002,ct_state=+new+trk,ipv6,reg6=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=15, n_bytes=1816, idle_age=49, priority=2002,ct_state=+new+trk,ip,reg6=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=2002,udp,reg6=0x4,metadata=0x4, nw_dst=203.0.113.0/24,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=2002,udp,reg6=0x4,metadata=0x4, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=75.033s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=2001,ip,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=2001,ipv6,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.032s, table=22, n_packets=6, n_bytes=2236, idle_age=54, priority=1,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=75.032s, table=22, n_packets=0, n_bytes=0, idle_age=75, priority=1,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=67.064s, table=25, n_packets=0, n_bytes=0, idle_age=67, priority=50,arp,metadata=0x4,arp_tpa=203.0.113.103, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:1c:ca:6a,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ed63dca->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a81268->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=75.033s, table=26, n_packets=19, n_bytes=2776, idle_age=44, priority=50,metadata=0x4,dl_dst=fa:16:3e:1c:ca:6a actions=load:0x4->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=221031.310s, table=33, n_packets=72, n_bytes=6292, idle_age=20, hard_age=65534, priority=100,reg7=0x3,metadata=0x4 actions=load:0x1->NXM_NX_REG7[],resubmit(,33) cookie=0x0, duration=184.992s, table=34, n_packets=2, n_bytes=684, idle_age=112, priority=100,reg6=0x4,reg7=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.034s, table=49, n_packets=0, n_bytes=0, idle_age=75, priority=110,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=75.033s, table=49, n_packets=0, n_bytes=0, idle_age=75, priority=110,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=75.033s, table=49, n_packets=38, n_bytes=6566, idle_age=49, priority=100,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=75.033s, table=49, n_packets=0, n_bytes=0, idle_age=75, priority=100,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x4 actions=resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=13, n_bytes=1118, idle_age=49, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x4 actions=resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=65535,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=65535,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=65535,ct_state=+inv+trk,metadata=0x4 actions=drop cookie=0x0, duration=75.034s, table=52, n_packets=4, n_bytes=1538, idle_age=54, priority=2002,udp,reg7=0x4,metadata=0x4, nw_src=203.0.113.0/24,tp_src=67,tp_dst=68 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=2002,ct_state=+new+trk,ip,reg7=0x4, metadata=0x4,nw_src=203.0.113.103 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=2.041s, table=52, n_packets=0, n_bytes=0, idle_age=2, priority=2002,ct_state=+new+trk,ipv6,reg7=0x4, metadata=0x4,ipv6_src=::2/::2 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=2, n_bytes=698, idle_age=54, priority=2001,ip,reg7=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.033s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=2001,ipv6,reg7=0x4,metadata=0x4 actions=drop cookie=0x0, duration=75.034s, table=52, n_packets=0, n_bytes=0, idle_age=75, priority=1,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=75.033s, table=52, n_packets=19, n_bytes=3212, idle_age=49, priority=1,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=75.034s, table=54, n_packets=17, n_bytes=2656, idle_age=49, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=203.0.113.103 actions=resubmit(,55) cookie=0x0, duration=75.033s, table=54, n_packets=0, n_bytes=0, idle_age=75, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=255.255.255.255 actions=resubmit(,55) cookie=0x0, duration=75.033s, table=54, n_packets=0, n_bytes=0, idle_age=75, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=224.0.0.0/4 actions=resubmit(,55) cookie=0x0, duration=75.033s, table=54, n_packets=0, n_bytes=0, idle_age=75, priority=80,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=75.033s, table=54, n_packets=0, n_bytes=0, idle_age=75, priority=80,ipv6,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=75.033s, table=55, n_packets=21, n_bytes=2860, idle_age=44, priority=50,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=resubmit(,64) cookie=0x0, duration=184.992s, table=64, n_packets=166, n_bytes=15088, idle_age=15, priority=100,reg7=0x4,metadata=0x4 actions=output:9 * For each compute node that only contains a DHCP agent on the subnet, OVN creates the following flows: .. code-block:: console cookie=0x0, duration=189.649s, table=16, n_packets=0, n_bytes=0, idle_age=189, priority=50,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=resubmit(,17) cookie=0x0, duration=189.650s, table=17, n_packets=0, n_bytes=0, idle_age=189, priority=90,udp,reg6=0x4,metadata=0x4, dl_src=fa:14:3e:1c:ca:6a,nw_src=0.0.0.0, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=resubmit(,18) cookie=0x0, duration=189.649s, table=17, n_packets=0, n_bytes=0, idle_age=189, priority=90,ip,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a,nw_src=203.0.113.103 actions=resubmit(,18) cookie=0x0, duration=189.650s, table=17, n_packets=0, n_bytes=0, idle_age=189, priority=80,ipv6,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=189.650s, table=17, n_packets=0, n_bytes=0, idle_age=189, priority=80,ip,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=189.650s, table=18, n_packets=0, n_bytes=0, idle_age=189, priority=90,arp,reg6=0x4,metadata=0x4, dl_src=fa:16:3e:1c:ca:6a,arp_spa=203.0.113.103, arp_sha=fa:16:3e:1c:ca:6a actions=resubmit(,19) cookie=0x0, duration=189.650s, table=18, n_packets=0, n_bytes=0, idle_age=189, priority=80,icmp6,reg6=0x4,metadata=0x4, icmp_type=136,icmp_code=0 actions=drop cookie=0x0, duration=189.650s, table=18, n_packets=0, n_bytes=0, idle_age=189, priority=80,icmp6,reg6=0x4,metadata=0x4, icmp_type=135,icmp_code=0 actions=drop cookie=0x0, duration=189.649s, table=18, n_packets=0, n_bytes=0, idle_age=189, priority=80,arp,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=79.452s, table=19, n_packets=0, n_bytes=0, idle_age=79, priority=110,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=79.450s, table=19, n_packets=0, n_bytes=0, idle_age=79, priority=110,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=79.452s, table=19, n_packets=0, n_bytes=0, idle_age=79, priority=100,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=79.450s, table=19, n_packets=18, n_bytes=3164, idle_age=57, priority=100,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=79.450s, table=22, n_packets=6, n_bytes=510, idle_age=57, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x4 actions=resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x4 actions=resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=65535,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=65535,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=65535,ct_state=+inv+trk,metadata=0x4 actions=drop cookie=0x0, duration=79.453s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2002,ct_state=+new+trk,ipv6,reg6=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2002,ct_state=+new+trk,ip,reg6=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2002,udp,reg6=0x4,metadata=0x4, nw_dst=203.0.113.0/24,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2002,udp,reg6=0x4,metadata=0x4, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=79.452s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2001,ip,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=2001,ipv6,reg6=0x4,metadata=0x4 actions=drop cookie=0x0, duration=79.450s, table=22, n_packets=0, n_bytes=0, idle_age=79, priority=1,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=79.450s, table=22, n_packets=12, n_bytes=2654, idle_age=57, priority=1,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=71.483s, table=25, n_packets=0, n_bytes=0, idle_age=71, priority=50,arp,metadata=0x4,arp_tpa=203.0.113.103, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:1c:ca:6a,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ed63dca->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a81268->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=79.450s, table=26, n_packets=8, n_bytes=1258, idle_age=57, priority=50,metadata=0x4,dl_dst=fa:16:3e:1c:ca:6a actions=load:0x4->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=182.952s, table=33, n_packets=74, n_bytes=7040, idle_age=18, priority=100,reg7=0x4,metadata=0x4 actions=load:0x1->NXM_NX_REG7[],resubmit(,33) cookie=0x0, duration=79.451s, table=49, n_packets=0, n_bytes=0, idle_age=79, priority=110,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=79.450s, table=49, n_packets=0, n_bytes=0, idle_age=79, priority=110,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=79.450s, table=49, n_packets=18, n_bytes=3164, idle_age=57, priority=100,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=79.450s, table=49, n_packets=0, n_bytes=0, idle_age=79, priority=100,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x4 actions=resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=6, n_bytes=510, idle_age=57, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x4 actions=resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=65535,icmp6,metadata=0x4,icmp_type=135, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=65535,icmp6,metadata=0x4,icmp_type=136, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=65535,ct_state=+inv+trk,metadata=0x4 actions=drop cookie=0x0, duration=79.452s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=2002,udp,reg7=0x4,metadata=0x4, nw_src=203.0.113.0/24,tp_src=67,tp_dst=68 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=2002,ct_state=+new+trk,ip,reg7=0x4, metadata=0x4,nw_src=203.0.113.103 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=71.483s, table=52, n_packets=0, n_bytes=0, idle_age=71, priority=2002,ct_state=+new+trk,ipv6,reg7=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=2001,ipv6,reg7=0x4,metadata=0x4 actions=drop cookie=0x0, duration=79.450s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=2001,ip,reg7=0x4,metadata=0x4 actions=drop cookie=0x0, duration=79.453s, table=52, n_packets=0, n_bytes=0, idle_age=79, priority=1,ipv6,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=79.450s, table=52, n_packets=12, n_bytes=2654, idle_age=57, priority=1,ip,metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=79.452s, table=54, n_packets=0, n_bytes=0, idle_age=79, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=255.255.255.255 actions=resubmit(,55) cookie=0x0, duration=79.452s, table=54, n_packets=0, n_bytes=0, idle_age=79, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=203.0.113.103 actions=resubmit(,55) cookie=0x0, duration=79.452s, table=54, n_packets=0, n_bytes=0, idle_age=79, priority=90,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a,nw_dst=224.0.0.0/4 actions=resubmit(,55) cookie=0x0, duration=79.450s, table=54, n_packets=0, n_bytes=0, idle_age=79, priority=80,ip,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=79.450s, table=54, n_packets=0, n_bytes=0, idle_age=79, priority=80,ipv6,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=drop cookie=0x0, duration=79.450s, table=55, n_packets=0, n_bytes=0, idle_age=79, priority=50,reg7=0x4,metadata=0x4, dl_dst=fa:16:3e:1c:ca:6a actions=resubmit(,64) networking-ovn-4.0.0/doc/source/admin/refarch/selfservice-networks.rst0000666000175100017510000006201313245511145026220 0ustar zuulzuul00000000000000.. _refarch-selfservice-networks: Self-service networks --------------------- A self-service (project) network includes only virtual components, thus enabling projects to manage them without additional configuration of the underlying physical network. The OVN mechanism driver supports Geneve and VLAN network types with a preference toward Geneve. Projects can choose to isolate self-service networks, connect two or more together via routers, or connect them to provider networks via routers with appropriate capabilities. Similar to provider networks, self-service networks can use arbitrary names. .. note:: Similar to provider networks, self-service VLAN networks map to a unique bridge on each compute node that supports launching instances on those networks. Self-service VLAN networks also require several commands at the host and OVS levels. The following example assumes use of Geneve self-service networks. Create a self-service network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Creating a self-service network involves several commands at the Networking service level that yield a series of operations at the OVN level to create the virtual network components. The following example creates a Geneve self-service network and binds a subnet to it. The subnet uses DHCP to distribute IP addresses to instances. #. On the controller node, source the credentials for a regular (non-privileged) project. The following example uses the ``demo`` project. #. On the controller node, create a self-service network in the Networking service. .. code-block:: console $ openstack network create selfservice +-------------------------+--------------------------------------+ | Field | Value | +-------------------------+--------------------------------------+ | admin_state_up | UP | | availability_zone_hints | | | availability_zones | | | created_at | 2016-06-09T15:42:41 | | description | | | id | f49791f7-e653-4b43-99b1-0f5557c313e4 | | ipv4_address_scope | None | | ipv6_address_scope | None | | mtu | 1442 | | name | selfservice | | port_security_enabled | True | | project_id | 1ef26f483b9d44e8ac0c97388d6cb609 | | router_external | Internal | | shared | False | | status | ACTIVE | | subnets | | | tags | [] | | updated_at | 2016-06-09T15:42:41 | +-------------------------+--------------------------------------+ OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations during creation of a self-service network. #. The mechanism driver translates the network into a logical switch in the OVN northbound database. .. code-block:: console uuid : 0ab40684-7cf8-4d6c-ae8b-9d9143762d37 acls : [] external_ids : {"neutron:network_name"="selfservice"} name : "neutron-d5aadceb-d8d6-41c8-9252-c5e0fe6c26a5" ports : [] #. The OVN northbound service translates this object into new datapath bindings and logical flows in the OVN southbound database. * Datapath bindings .. code-block:: console _uuid : 0b214af6-8910-489c-926a-fd0ed16a8251 external_ids : {logical-switch="15e2c80b-1461-4003-9869-80416cd97de5"} tunnel_key : 5 * Logical flows .. code-block:: console Datapath: 0b214af6-8910-489c-926a-fd0ed16a8251 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 100, match=(eth.src[40]), action=(drop;) table= 0( ls_in_port_sec_l2), priority= 100, match=(vlan.present), action=(drop;) table= 1( ls_in_port_sec_ip), priority= 0, match=(1), action=(next;) table= 2( ls_in_port_sec_nd), priority= 0, match=(1), action=(next;) table= 3( ls_in_pre_acl), priority= 0, match=(1), action=(next;) table= 4( ls_in_pre_lb), priority= 0, match=(1), action=(next;) table= 5( ls_in_pre_stateful), priority= 100, match=(reg0[0] == 1), action=(ct_next;) table= 5( ls_in_pre_stateful), priority= 0, match=(1), action=(next;) table= 6( ls_in_acl), priority= 0, match=(1), action=(next;) table= 7( ls_in_lb), priority= 0, match=(1), action=(next;) table= 8( ls_in_stateful), priority= 100, match=(reg0[2] == 1), action=(ct_lb;) table= 8( ls_in_stateful), priority= 100, match=(reg0[1] == 1), action=(ct_commit; next;) table= 8( ls_in_stateful), priority= 0, match=(1), action=(next;) table= 9( ls_in_arp_rsp), priority= 0, match=(1), action=(next;) table=10( ls_in_l2_lkup), priority= 100, match=(eth.mcast), action=(outport = "_MC_flood"; output;) Datapath: 0b214af6-8910-489c-926a-fd0ed16a8251 Pipeline: egress table= 0( ls_out_pre_lb), priority= 0, match=(1), action=(next;) table= 1( ls_out_pre_acl), priority= 0, match=(1), action=(next;) table= 2(ls_out_pre_stateful), priority= 100, match=(reg0[0] == 1), action=(ct_next;) table= 2(ls_out_pre_stateful), priority= 0, match=(1), action=(next;) table= 3( ls_out_lb), priority= 0, match=(1), action=(next;) table= 4( ls_out_acl), priority= 0, match=(1), action=(next;) table= 5( ls_out_stateful), priority= 100, match=(reg0[1] == 1), action=(ct_commit; next;) table= 5( ls_out_stateful), priority= 100, match=(reg0[2] == 1), action=(ct_lb;) table= 5( ls_out_stateful), priority= 0, match=(1), action=(next;) table= 6( ls_out_port_sec_ip), priority= 0, match=(1), action=(next;) table= 7( ls_out_port_sec_l2), priority= 100, match=(eth.mcast), action=(output;) .. note:: These actions do not create flows on any nodes. Create a subnet on the self-service network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A self-service network requires at least one subnet. In most cases, the environment provides suitable values for IP address allocation for instances, default gateway IP address, and metadata such as name resolution. #. On the controller node, create a subnet bound to the self-service network ``selfservice``. .. code-block:: console $ openstack subnet create --network selfservice --subnet-range 192.168.1.0/24 selfservice-v4 +-------------------+--------------------------------------+ | Field | Value | +-------------------+--------------------------------------+ | allocation_pools | 192.168.1.2-192.168.1.254 | | cidr | 192.168.1.0/24 | | created_at | 2016-06-16 00:19:08+00:00 | | description | | | dns_nameservers | | | enable_dhcp | True | | gateway_ip | 192.168.1.1 | | headers | | | host_routes | | | id | 8f027f25-0112-45b9-a1b9-2f8097c57219 | | ip_version | 4 | | ipv6_address_mode | None | | ipv6_ra_mode | None | | name | selfservice-v4 | | network_id | 8ed4e43b-63ef-41ed-808b-b59f1120aec0 | | project_id | b1ebf33664df402693f729090cfab861 | | subnetpool_id | None | | updated_at | 2016-06-16 00:19:08+00:00 | +-------------------+--------------------------------------+ OVN operations ^^^^^^^^^^^^^^ .. todo: Update this part with the new agentless DHCP details The OVN mechanism driver and OVN perform the following operations during creation of a subnet on a self-service network. #. If the subnet uses DHCP for IP address management, create logical ports ports for each DHCP agent serving the subnet and bind them to the logical switch. In this example, the subnet contains two DHCP agents. .. code-block:: console _uuid : 1ed7c28b-dc69-42b8-bed6-46477bb8b539 addresses : ["fa:16:3e:94:db:5e 192.168.1.2"] enabled : true external_ids : {"neutron:port_name"=""} name : "0cfbbdca-ff58-4cf8-a7d3-77daaebe3056" options : {} parent_name : [] port_security : [] tag : [] type : "" up : true _uuid : ae10a5e0-db25-4108-b06a-d2d5c127d9c4 addresses : ["fa:16:3e:90:bd:f1 192.168.1.3"] enabled : true external_ids : {"neutron:port_name"=""} name : "74930ace-d939-4bca-b577-fccba24c3fca" options : {} parent_name : [] port_security : [] tag : [] type : "" up : true _uuid : 0ab40684-7cf8-4d6c-ae8b-9d9143762d37 acls : [] external_ids : {"neutron:network_name"="selfservice"} name : "neutron-d5aadceb-d8d6-41c8-9252-c5e0fe6c26a5" ports : [1ed7c28b-dc69-42b8-bed6-46477bb8b539, ae10a5e0-db25-4108-b06a-d2d5c127d9c4] #. The OVN northbound service creates port bindings for these logical ports and adds them to the appropriate multicast group. * Port bindings .. code-block:: console _uuid : 3e463ca0-951c-46fd-b6cf-05392fa3aa1f chassis : 6a9d0619-8818-41e6-abef-2f3d9a597c03 datapath : 0b214af6-8910-489c-926a-fd0ed16a8251 logical_port : "a203b410-97c1-4e4a-b0c3-558a10841c16" mac : ["fa:16:3e:a1:dc:58 192.168.1.3"] options : {} parent_port : [] tag : [] tunnel_key : 2 type : "" _uuid : fa7b294d-2a62-45ae-8de3-a41c002de6de chassis : d63e8ae8-caf3-4a6b-9840-5c3a57febcac datapath : 0b214af6-8910-489c-926a-fd0ed16a8251 logical_port : "39b23721-46f4-4747-af54-7e12f22b3397" mac : ["fa:16:3e:1a:b4:23 192.168.1.2"] options : {} parent_port : [] tag : [] tunnel_key : 1 type : "" * Multicast groups .. code-block:: console _uuid : c08d0102-c414-4a47-98d9-dd3fa9f9901c datapath : 0b214af6-8910-489c-926a-fd0ed16a8251 name : _MC_flood ports : [3e463ca0-951c-46fd-b6cf-05392fa3aa1f, fa7b294d-2a62-45ae-8de3-a41c002de6de] tunnel_key : 65535 #. The OVN northbound service translates the logical ports into logical flows in the OVN southbound database. .. code-block:: console Datapath: 0b214af6-8910-489c-926a-fd0ed16a8251 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "39b23721-46f4-4747-af54-7e12f22b3397"), action=(next;) table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "a203b410-97c1-4e4a-b0c3-558a10841c16"), action=(next;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 192.168.1.2 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:1a:b4:23; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:1a:b4:23; arp.tpa = arp.spa; arp.spa = 192.168.1.2; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 192.168.1.3 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:a1:dc:58; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:a1:dc:58; arp.tpa = arp.spa; arp.spa = 192.168.1.3; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:a1:dc:58), action=(outport = "a203b410-97c1-4e4a-b0c3-558a10841c16"; output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:1a:b4:23), action=(outport = "39b23721-46f4-4747-af54-7e12f22b3397"; output;) Datapath: 0b214af6-8910-489c-926a-fd0ed16a8251 Pipeline: egress table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "39b23721-46f4-4747-af54-7e12f22b3397"), action=(output;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "a203b410-97c1-4e4a-b0c3-558a10841c16"), action=(output;) #. For each compute node without a DHCP agent on the subnet: * The OVN controller service translates these objects into flows on the integration bridge ``br-int``. .. code-block:: console # ovs-ofctl dump-flows br-int cookie=0x0, duration=9.054s, table=32, n_packets=0, n_bytes=0, idle_age=9, priority=100,reg7=0xffff,metadata=0x5 actions=load:0x5->NXM_NX_TUN_ID[0..23], set_field:0xffff/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30], output:4,output:3 #. For each compute node with a DHCP agent on the subnet: * Creation of a DHCP network namespace adds a virtual switch ports that connects the DHCP agent with the ``dnsmasq`` process to the integration bridge. .. code-block:: console # ovs-ofctl show br-int OFPT_FEATURES_REPLY (xid=0x2): dpid:000022024a1dc045 n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst 9(tap39b23721-46): addr:00:00:00:00:b0:5d config: PORT_DOWN state: LINK_DOWN speed: 0 Mbps now, 0 Mbps max * The OVN controller service translates these objects into flows on the integration bridge. .. code-block:: console cookie=0x0, duration=21.074s, table=0, n_packets=8, n_bytes=648, idle_age=11, priority=100,in_port=9 actions=load:0x2->NXM_NX_REG5[],load:0x5->OXM_OF_METADATA[], load:0x1->NXM_NX_REG6[],resubmit(,16) cookie=0x0, duration=21.076s, table=16, n_packets=0, n_bytes=0, idle_age=21, priority=100,metadata=0x5, dl_src=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop cookie=0x0, duration=21.075s, table=16, n_packets=0, n_bytes=0, idle_age=21, priority=100,metadata=0x5,vlan_tci=0x1000/0x1000 actions=drop cookie=0x0, duration=21.076s, table=16, n_packets=0, n_bytes=0, idle_age=21, priority=50,reg6=0x2,metadata=0x5 actions=resubmit(,17) cookie=0x0, duration=21.075s, table=16, n_packets=8, n_bytes=648, idle_age=11, priority=50,reg6=0x1,metadata=0x5 actions=resubmit(,17) cookie=0x0, duration=21.075s, table=17, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,18) cookie=0x0, duration=21.076s, table=18, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,19) cookie=0x0, duration=21.076s, table=19, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,20) cookie=0x0, duration=21.075s, table=20, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,21) cookie=0x0, duration=5.398s, table=21, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x1/0x1,metadata=0x5 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=5.398s, table=21, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x1/0x1,metadata=0x5 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=5.398s, table=22, n_packets=6, n_bytes=508, idle_age=2, priority=0,metadata=0x5 actions=resubmit(,23) cookie=0x0, duration=5.398s, table=23, n_packets=6, n_bytes=508, idle_age=2, priority=0,metadata=0x5 actions=resubmit(,24) cookie=0x0, duration=5.398s, table=24, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x4/0x4,metadata=0x5 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=5.398s, table=24, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x4/0x4,metadata=0x5 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=5.398s, table=24, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x2/0x2,metadata=0x5 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=5.398s, table=24, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x2/0x2,metadata=0x5 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=5.399s, table=24, n_packets=6, n_bytes=508, idle_age=2, priority=0,metadata=0x5 actions=resubmit(,25) cookie=0x0, duration=5.398s, table=25, n_packets=0, n_bytes=0, idle_age=5, priority=50,arp,metadata=0x5, arp_tpa=192.168.1.2,arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:82:8b:0e,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163e828b0e->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80102->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=5.378s, table=25, n_packets=0, n_bytes=0, idle_age=5, priority=50,arp,metadata=0x5,arp_tpa=192.168.1.3, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:d5:00:02,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ed50002->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80103->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=5.399s, table=25, n_packets=6, n_bytes=508, idle_age=2, priority=0,metadata=0x5 actions=resubmit(,26) cookie=0x0, duration=5.399s, table=26, n_packets=6, n_bytes=508, idle_age=2, priority=100,metadata=0x5, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=load:0xffff->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=5.398s, table=26, n_packets=0, n_bytes=0, idle_age=5, priority=50,metadata=0x5,dl_dst=fa:16:3e:d5:00:02 actions=load:0x2->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=5.398s, table=26, n_packets=0, n_bytes=0, idle_age=5, priority=50,metadata=0x5,dl_dst=fa:16:3e:82:8b:0e actions=load:0x1->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=21.038s, table=32, n_packets=0, n_bytes=0, idle_age=21, priority=100,reg7=0x2,metadata=0x5 actions=load:0x5->NXM_NX_TUN_ID[0..23], set_field:0x2/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30],output:4 cookie=0x0, duration=21.038s, table=32, n_packets=8, n_bytes=648, idle_age=11, priority=100,reg7=0xffff,metadata=0x5 actions=load:0x5->NXM_NX_TUN_ID[0..23], set_field:0xffff/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30], output:4,resubmit(,33) cookie=0x0, duration=5.397s, table=33, n_packets=12, n_bytes=1016, idle_age=2, priority=100,reg7=0xffff,metadata=0x5 actions=load:0x1->NXM_NX_REG7[],resubmit(,34), load:0xffff->NXM_NX_REG7[] cookie=0x0, duration=5.397s, table=33, n_packets=0, n_bytes=0, idle_age=5, priority=100,reg7=0x1,metadata=0x5 actions=resubmit(,34) cookie=0x0, duration=21.074s, table=34, n_packets=8, n_bytes=648, idle_age=11, priority=100,reg6=0x1,reg7=0x1,metadata=0x5 actions=drop cookie=0x0, duration=21.076s, table=48, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,49) cookie=0x0, duration=21.075s, table=49, n_packets=8, n_bytes=648, idle_age=11, priority=0,metadata=0x5 actions=resubmit(,50) cookie=0x0, duration=5.398s, table=50, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x1/0x1,metadata=0x5 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=5.398s, table=50, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x1/0x1,metadata=0x5 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=5.398s, table=50, n_packets=6, n_bytes=508, idle_age=3, priority=0,metadata=0x5 actions=resubmit(,51) cookie=0x0, duration=5.398s, table=51, n_packets=6, n_bytes=508, idle_age=3, priority=0,metadata=0x5 actions=resubmit(,52) cookie=0x0, duration=5.398s, table=52, n_packets=6, n_bytes=508, idle_age=3, priority=0,metadata=0x5 actions=resubmit(,53) cookie=0x0, duration=5.399s, table=53, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x4/0x4,metadata=0x5 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=5.398s, table=53, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x4/0x4,metadata=0x5 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=5.398s, table=53, n_packets=0, n_bytes=0, idle_age=5, priority=100,ip,reg0=0x2/0x2,metadata=0x5 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=5.398s, table=53, n_packets=0, n_bytes=0, idle_age=5, priority=100,ipv6,reg0=0x2/0x2,metadata=0x5 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=5.398s, table=53, n_packets=6, n_bytes=508, idle_age=3, priority=0,metadata=0x5 actions=resubmit(,54) cookie=0x0, duration=5.398s, table=54, n_packets=6, n_bytes=508, idle_age=3, priority=0,metadata=0x5 actions=resubmit(,55) cookie=0x0, duration=5.398s, table=55, n_packets=6, n_bytes=508, idle_age=3, priority=100,metadata=0x5, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,64) cookie=0x0, duration=5.398s, table=55, n_packets=0, n_bytes=0, idle_age=5, priority=50,reg7=0x1,metadata=0x5 actions=resubmit(,64) cookie=0x0, duration=5.398s, table=55, n_packets=0, n_bytes=0, idle_age=5, priority=50,reg7=0x2,metadata=0x5 actions=resubmit(,64) cookie=0x0, duration=5.397s, table=64, n_packets=6, n_bytes=508, idle_age=3, priority=100,reg7=0x1,metadata=0x5 actions=output:9 networking-ovn-4.0.0/doc/source/admin/refarch/figures/0000775000175100017510000000000013245511554022746 5ustar zuulzuul00000000000000networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-hw.graffle0000666000175100017510000001400313245511164025511 0ustar zuulzuul00000000000000‹íisÚH·€?¿ùÜ|¼I »ÕÚf2yK,/`¼—«n …b!IØ!Sóßïi- ´ØÆ3N»]•X–Îé½ÏyºÕêþøßï3»rkx¾å:¼ÅUô¶b8cwb9æoÏN÷>(oÿûéÍÇÿi7N‡½Ven[~PéÕºÊÛµš6ŸÛF­ÖÞËO,:k¬ðü<ûXc÷á±îy:»øÏG?ð ýŸ ºª;s,Ósóê1\µ=ýúÚ6äµX$%eµŠ)¤…¨¡BJäc- :LBÃ3Âø›z`¬"… ÂôÆ0ª`ù7üFÅÊ;?éàV¸^VÿP‚ÊgÝ÷õ;ÝÓ³J{–mœ.ç¹XõEàfe!«óiÓ/f†d‹ÊrÃ4¼O˜~¬%׉Þšþ;¾1&ÙxŽŽs±„Òç–oì\²†­AV¾;ÓM£á. N/Ÿš\b,ç¦,ô|buÓ1‚mä}HÆ`j€Òf+ª­D '\Çí­¤¾åHù€h©¿ô›XRßQ@F¾Â-saØÍ1Ùÿ_ÝJϰõ¥›W¿5XEvtgbÎá‘/cÏ2-çÑâ=¨Ê:4ãÿ²ðGÐu®Ý•hÜ©ãtên¸³C݃$2©.½ úÚvõ Ý}WO(ÞèÕµµ9ˆb踞õÃutÒm9aÞØ@Ö "ªkÁIgß^`d|©Û—ÝŽ«Ñ~½ß´¦Úôø ‰xt±oÍü=9>×ÓQçÜÖày£%žž’ýo—GHë^´´Á7Ùûnß5Ú÷'ÿÀ¸þNñ`åþð{:Xäõø/x?|^úŸÆU5üATˆ()ï+²Jþz RÖ,N fð h}ÐÀÿuõrb™Óg­˜SwþÁ¬¥ÝꉡOŽ{ùpO.2—aÈI‡Žýô8°n}ix]gb|_%~mîQÚÜ'jàÉ´É×…(H% Ð*!™BZ«¥@¯®o8“ÐQZã”â*'±(“ò×Ï×þOô¾‚þz_ùS”h•bAPˆLJe¿¯DTU ID°"+’$üµÑâÐ6xû‚ÀcTL¡_úÕ†îÜê~A¢sáu›éÀVe°YqU˵ßÌx$àOõ‰{—’؉dšž~—NÿŠÛNø –‹ ðÜãY"H]§/£º÷K×=²MFeß´Àÿ2ˆò#7™RNšCØ6;áZÿiZy­óÈövO¨¬Ã€%-(\T®½˜9šm™ÎVƒ¹>†˜R:À ö'zrx±oZþ¸g0Öí‚üÂ8~*ãYåÊê:Ÿ¹¸SølT‘ dm-ó-<×·S›Šj•`"ª¢LÀž¾¯T)…"D(z‚e‘uY¨ÊA™«P †˜¤R¨DE eŒ°¢d»qÿ_% ìùÅ= ¸›HUÜ} "ÖÃŒ¼L$´ÑÇS垉v³¯SQØ;ç†3ÐÿÃÀ˜Y#מä mÐF_µ%Õ@Š Ç†•[·DAÎØ¹Á†JªUÜÛ7\ò*Õ¬nŠ*­a»‹IQeemmAM][¶ýPEåk3#‰6EâBEU „¬¢Õ°Q̉¶Y¢- X‰ºú‘ µ½m,Qѵ¶Z¨íƒÅ0²!$ô陣l#Ê6É|Í;±¢‚­ ȧÅý`s [QΙ=G ²*Ö5H¶ªAª0\ë*P¶ªAµù™k0)%Ï1¼}b-üLa؇ŒÈY‹Sã{pß;9ÝkfjNôäûF±È´¾Œ=bó¡sŽ./NNÙõh6Ö‘n¿{jõ†³¹=úÚôòs+¾w‹Äu»Û¯¾›îœ{<íIWO"Òfö¾Ö·Sˆ» £–“¶ýuÒ¶oG³½e,bôg0x™kâç–=v ‚æˆ|¿»ú`•–A«ŽÄÓ>Qý‘ÐÕX„ûv«Þ¾œÅ"—3½yÚÃJö:j§_3—µÉ—¦RŸÎ?'iÀûxøez)ìOÇí)dÚö/‡íAD§_ÎÑðbß¿<¯Odz=þÝÅ"úÞ2.¾‡ã°ä謽·^L–Ã/u79NÄQûŒ%³ÕGGÚÙ]WK²‹ô¿ìƒÆfïßÞ¸lÄgÃïÄ6:'g±Èð"Ìh£O†Z§«¦Ÿ-­iµAük,âšõ3–AWêjãD¤Û>ÔºëE·\mõ¸ÓJ®“:êvÌÔce}Ý&"ÃÕ-´¾ÞëÃu{ ÜÞ×/ê7PË3u1a³L߬2Þ®Kg³ÕªŸûè°}4©›š¦ÕiÒê4­Y«Ý˜Úe¿îk!û´ö6¼«O‘ÒúfýVkßi— ¬Ac¸4­‘¶ï×c‘Ù]céiOÍ?þX÷¹T)ìgk¿zå×ø hÀ ÿÏM+r5v¡M²G"¢Ñþb+zóçÕ5 E0²¯®ÑÕµgùþÕõxª{¾ JǰoÀë¿ÿ’cfTAô÷+ϘQ¼2=ÃpØÅÈ^ð;~@â”F Õÿ÷Êø>×a˜:YôûÕØ7Á'xÖlÖÕXUITEI‘®ÆŠDdT¯æº7¹ ¾‹‚ÿ1&á/Ia¿¡á/±_‚Šj(Bi(¹F‘6û%áPD† õ“ÅÒÛ0ªXȨ=n>Q)ªŠ¨I^H 6(Œ@0Å2–U ~›ÈåKU©Š$¢**ô•PAÎéK ýy˜›ÙUþ¶ëéÌ÷·cù)ÀÇ KùÙœ.þÊcSó<·d¨‰ò„«¡ S/3*׺íµŒ Ø«ô:±‡ºkâÁtËÞ>UÖ$7|Ël‡ªùí•ôÚ«§O‘¡ŠÁ  J±¨¨•òS$Tx쉄ùÉãÝ¡üÄ)ú“ÞîÄ$B‚¢H²°Dã­Ì«ÈFàÂSÍ+4j‰ àù4ŸáÓ ÷Mƒ¸|„Oƒði> rÿ4y–×€àzª²"Sð<"U#ç×Ä© gÜ]b\á51.V¨€·{»‡Ãwƒ[‘{ÿOéCÅY–³,gÙûXvÈY–³,gYβ÷³,þ)KÚDé}àµJ‰ˆUQRUF¿–•ÌèW)’Š'öÍR%$cY.VòÊ”Ø7õ’ô¯[CG^<+ ¨„‡ª ÈT6Á›Ò·2U€Ê9šs4çè{8Zù~ØÔ¼nkÒhØG(d`ÕKrÑžjûóA¿Õ‹xñ;D~2O¸H8™ˆ8;7!ëýãÆR_P„ìü¹iO@ûèvÔV—Úþ*GÚÅ!àÕɌѴ)5fó¯RS?jö{îQ‚·ÔÐÞ…ßû5ÇEQE„ñ¤½gÚçý$g¾(é7@´¼új!ùŠd…ÛÌz ³wØXi’“½%ˆŠ rÙÜKx?‚>»ü2Eç? A»9šiõåæ7!Mú<¡àNcS¤QÿªE·Øõ·Ôu,ò=u‹¦®kp½ìz[k¯ÅשˆÎàÖÔ C¸°ÍKÈÝåíȤBL'·û Ü©v>:jÖ“P¾ô‡ ‹ ö_­&v5ŠŒ´:a¿“V§ÕçZûPöµ@kuµÑàv» ucvC޾ô5øÝžºÃ/K+Bt£èÓ`ßÇ¢´@Dª^ŠH($&»†Ò¡w•výêÍžë¦ñ<ÿ1uû ýì¦ÞŒB8„ZúÅj±€~¡.öÄhþó¦Í·Eó¢Éz‚¶ºdÚkð4C ýºbŸ~‹M—^ºÇæüÛðI¾öQSjŽduoÏlG“yÓÍ NÆŠ å2Æ}ˆŽ&_N¦ÃÙw›áã øÒ“¬g€Œý¯&™Þ\(É»¼]¢œ8d(s àÀ)€S§€_¶îù¹oø«úÝöÉ„ûdOæ>™ûä_¸¸ý—ÌÒs×¼Û®Yà®™»fîš¹kæ®ùeNš„«àv™Ñ#’*ËìüOLª‚Húü/‚i K ì •$cníE)?fO‡ág½¤Ïz)·F×¾@"[Ô…;Ý3*¶¾tÁžõ‚”Ü BŘ*viP!òATðATðAÅ‹|E˜9lí}%ã}¥hÿg¬’ª,Š*¢‚¢PðÁâûÊæ4!ÅXÞ±Øþ–Ï|â“DzÎ!Äßuq*‚@-¨2„åí\œDÁ7"ðr’¨B?·sq`TE‰ùH$«IòóÈ—4Ðâ.‘»DîC—Hhäîl¾ŒÊ‘;1ü‡G¶¬®z642vøi¡ÍÎ~pðtš¥ ;¦UÓÇ”Êòãü©ãdîO?™ûSîO¹?åþ”ûÓgò§•¦è#Ý<êN;T 稊*:T’w¨êKò§O=(œ(Ü¡r‡Ê*w¨Ü¡>ÛÕ®cÛ†÷3VhÖ¦TlQK$UeÓå¦[füþï«ë•±ÚÏ'—àÐÉLÝ;Ÿi¦UÊÞ#EŽ>÷OÝGê¤K… wzºi¤úÛ:y›© …?ÆüÔŒõ´üx­Û¾Q[KèKÃK™z›™C Z0ôª ݤ‚LuÇ7ˆé3£ˆQÂôTpôàÆfk ¼Åf¨ç–qW&q_;WÜßÈ4ÇšéQPm¹5Lblyc÷Ðrš–ä%Vr­0”ÐõÀrœ ª¢œN´¢å˜–S”®‰$Œ½ût@ãÀpÌ· IDE?DU¡¹ˆÃ0¶LlpçέGëd›þ±g-Éh®!° ý‡ÍåØ1XŸ¹§œ¸wšm™Î#{ˆçÌD”~!mYbó`Á©ltÀ¸>ºs«û™†j9Ö·…‘¦Ý{t¾M8·|kdç®5ùl,³9H­Xÿº«DYš»Ë‹™ööš¾6¬ÛU‹mÙZ™õÝ[êžùFË ,o£:ו­ù±ÀÈt/^õáZž©«e•÷—t¼´•ZãŒÚP´4‘À5ßæHlå6,ÊÚ­žŸ,À©– @ôÔãuÓM?¿°&©^œZA_¡XåÿNõÑ£ n/'†™îw–ƒ„`x*ÈJA1\ºîì\&Z·Ñ”Ùp7÷u¤‰V78×nÓ¦=ÕtW­9´´îlî¾ÌlóMªdšÓØ/ÁNÞ“îŒõ±{$!úÀ@Zm£×Ü+ Ô@<-SX)ùþB·­`™V‰òÖ3%ÎØ‰]—H®›Ç%œîm›ÉÆžëç{\ЧÁÌ‹ët97§b9c{11êúøÆôØäIŸmPž;ö²<ÑÐH]{Á\Ô@ý°—o›IØow÷¶’ïœl¥°ßko%ß;*¯RX.Ë*f¤!RzO§Ý½-²n4ðW¾~khþž­{ÐÚçL½HSÈi.|cÅÄåõR‡aSºWäÈ ŠmLb½>Öæ68ÏOoþZ ì¢#)networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-architecture1.png0000666000175100017510000032302113245511164027017 0ustar zuulzuul00000000000000‰PNG  IHDRv…3iÈsRGB®Îé pHYsˆˆÈ¥†ÕiTXtXML:com.adobe.xmp 5 2 1 °ã2Ý@IDATxì|T7¶ÆqÇØƒÁSLï½÷^BzïÉn²Ivßöݼd7oKvÓ˦mH Ð!ôÞ;ƒmÀ½÷Žß,P.3ãñØ3cû ó3]]I÷“®ô霣#·’’’zòA@A Î#à^çA@A@0! ¬Hú ‚€ &„I?A@AÀ„€°"é‚€ ‚€ ˜V$ý@A@Ф‚€ ‚€ `B@X‘ôA@A@L+’~ ‚€ ‚€ aEÒA@A@0! ¬Hú ‚€ &„I?A@AÀ„€°"é‚€ ‚€ ˜V$ý@A@Ф‚€ ‚€ `B@X‘ôA@A@L+’~ ‚€ ‚€ aEÒA@A@0! ¬Hú ‚€ &„I?A@AÀ„€°"é‚€ ‚€ ˜V$ý@A@Ф‚€ ‚€ `B@X‘ôA@A@L+’~ ‚€ ‚€ aEÒA@A@0! ¬Hú ‚€ &<†„@Qqqn^>öõññôô°¬y~AAAa‘·—·——åU׌É˧ÊE¾õ}<=¬<uVUß›gªåݵ¤¤$+'×Í­žƒ®ÖXå¶BvNnN^5§)+]ù«WKb#¯Äääæ…´lÑ&8¨¡ŸŸYn……Ey¶QÊÎÍ%+o/Oo謹5®Ü.fàTî§³0%-ýÂ¥+zv÷ðp¿zõjvnžYýyçÍÌ"ùYÑ>À-™Ù9–ùxyzÖ÷ñ6‹gŒ=Þ³KÇõë›]’ŸuZ>ÍÔ²&<ñÖ¢Å<Ô¾½™?Çòé¾Y»iû÷MËØ‘–W]3曵w<ò̽wôéÚÙj ¿ß´uÓîýó§Mšz&¬ °Ðx{“† ï˜9e`Ïî:òr\Ük|ÆÏ?>ûXHP ¯³_ûWqñÕ{çÌ=¨ÕßWnýÔU 8ë?]¾ÊÃÝ}pŸžTþJ\Â+ï~döpß fÍÚ†íÛ«w—NújEûë.Þ,}»1иQHPó^;N>DÅÓVoÝy."êžÙÓ)%\G ZlèýÇNž¿è¨ª¸dùc¿{åÐÉ3ŽÊPò©õ8¶Ï¤gfýý£Ï÷?ÅìÕ9´í„aƒçLÇdÙ, IZfæ_/[¶~3²…j‡6­[4 |ðÄi«8;%B˜:°×O\Š”Ž}k¬mŒŒMLâµúÓ›#]!ì"£¹Ï]Œœ9~´&íZ믿¸¤dî­Ï¯úq;2-•¸Ò}¤3'«ÎÌÎ>vañê Ÿ|û=bEòwss›9a4ËË‹—¯˜ÕM~ÖDVTS[ùË•kÿôìãQ*鱯5±@î5fðÀ†þ.§Tr8\H‰^þù“ ÊϹêšµ‚û ª·¿¾ÿIJzèáy³;µk£k ¹ùnÓ–õ;öðEÏzçÌ©êÒ°~}VnÞvðäéÙ“ÆéÄ: ø}ßn],5 •{k*Ù.¥3xI½k¹®žó«äVáI"$îÚÖØÜ*¿ß>õ°1㌬¬­ûý°e¬(7?Á-“ÕÕÊõ'îœØÌ˜?l{ϑ㟷zïÑq%‘B6Øô‹ï×þÝÝEv`¬ö‡¥½k^³¾a­œ˜’ºzÛN׬=“™+†eIËæÍü|}˜§kfåîîÆ“2[­žZÈZ½t"«¯~Ü{Jƒù͙͑˜›Ü>u"r#pûC©éêI‡õíE >)årl¼ŠÑQŸ ¿ÀÏáýúèHUykl·‹±”¶ý€Ž}DÛöF@8nXùÊÖFþþ³&Ž}ðöYܸuïÁªô«­é9°4®ž¼¤Ò°2;dЕ¸x‘ [­vGЬ¨æµ/‚w̘òú'_¬ß±{hŸ^Á-m?C\bòÚí».ÅÄ2 µoÒ½cûqC)Óftùß®ÛÄËOØ…`ßsçÌiûޏx9zìý{tS9'$§|µjá»nÖ¢Ùµ9;âJ ¶&è;fŒ¥’>uv˾ƒ—cãòó ˜Ýûvï:cì(£4ëߟ}ÕÈߎ¥ÿÖý‡Úµjùä]óսƿ¾X¹›Çaýzóe%wüÜy&¹>ÝL†GH¶)Á{“†þ«¶ì@Ê“›Ìk´< %²ñ¶ì¼uùjII¯.÷îa÷®ÃÇõêÞ³t4ªÃÌ;¦žÚÃãU‹æ{õ˜8|°Q„ƒ¤„¹üĹp–'êÚn䀾íB‚u&¬;ùN5 «…Å?¬ÏûçÎÜyð(ÓÏ“wÎ7bÂ-0à¸ÀHŸ.[Iû>4o¶Î*#+ýãuRjÔÞpë„1m[µÔ Øhbc2V­‰«|D/T²C›f }Ë×?¬OJ¾wöt¤8ºš6ilÙgŒL.âJ4Ozöbd^>íÒjÒÈ¡t9§Y‹–Í{öÉã7ô³.œ|¯qÂ6{l/þ»bC£63ô©ó˜Ë=ÿäóØ,TPX€‘Ú×–šžÉ¢Âú>>-ùùÖ׬èÌ…eÆt*ü¢fEÇΆÙ·”©Pè’Õë7ï9€ídë–A>>Þ‘W¢×lÝIé/>r/³ªV¤§JNLH‹æfµågVNÎëŸ|ÉÈÎnH“HæÁŠ­CëzõL¬(.1‰|Z5ß{ä8ÏØ´ c:#Úé Ÿ¹ç=r×[‹–0Ì¡jÚ¤ ÷À]ˆ’ÓÒaºäƒ¥p5nè¯ò´§±ì麖¬¸ÔÊ??þm#4tÎè„Ä»öñ^E&¬VÌì©j¹¯³= ˜Á¨FFÇÐ¥;¶m]Ö¶SRâFìÿ‰k^Ö‘•èú^³€j,£¨Ò¯/òEÞÆšØ,½ü¬Å+ª©Ëž¬çγ"Dø1j`?«‘œšöɲï™ržº{A¿î]HÃÒüý¯¿eLÿ|ÅOÜ5£Åÿ{ég,^Æ\ÂÂWY§z¸{°  ».Læ.¦:•?½Ãèì…"•xæøÙóP"ØÏ/ºGM´Lï}ùÍÉó–¬ÞðøÂÛÕíüE^…PjÚ˜”Õ´±iº5~X"#‹ŽO3xÀݳn±Áö 8ìY»ï¶Ø ÿ³t°vÛ.Ewå|ºlÃÙ¸!o8ÉSïgËWB‰ŒÅY†a!jžûßçžR‹~ªôqãÜÉã™pËŠ[:øàí³a–Äðøï/þªÔ²y «s-y³€Æ÷Ì™ÎðʼÉC.B=¬è`©‘û€žÝ £ŠžêÛyŠÏ¿ûù4ΘŠù0Ä…Òw: ª/<|/)Ëmb›0g÷èÔjœŒR="g@Úb™M ÃW˜,ÁÓ¬¬ö9ÙBgï˜>…<¦ú¯øþ‰ ²,V”˜šÆ½/«ž&t¶A¥J5u©ÈA½z „#¦ˆÙ¬ŠD}FÓÞÿõ™ÎÇž·F'¶À ˜¦yxþ¶G‘IÛëŸ,BBÆ+ÙµC(}>6!éo¾ËŸö„ÊʞƪP×5ë`”‚øJ„,yâõUl2óóÅÛöTr,«3{Rû_gÛ ˜e«²6#ܼ©ÉdÞþO‹RÍrRŠ©Ã¨OåúÀõ»¯ýÏKwäôÙ£gα2á½0^íÜ®-Òtz¤Ü/áÚ€ùrªv?mmz:–2J›°lÝf«®8xXfТ¢bD÷ŠÃ\…ÞA"‡²îB˜ŒDúÂzT!q‰‰ËMPްàcNEŽMT<ü½{ö-Zö@ß9i Ó¤Ñøã˜ýû2@—fxƒÆ³Jø”ˆu­mJDY ©°:(a¦ù©£‡¸ŸÀ_>NœFÚ„ZíŽS•^úòÈ‚Û,ߨôú/ËtÂd®(anA]ÍßLuˆUX;><ÿ6E‰HƒjoÚèv>Ê_ý)*.zîþ»P^ žLS5S¢Ó”@ôõNHØìcOcU¨ëšu0ŠËÍ˃‚×4\:³€©YîiR©„ˆvPŸ¡!ŒžŽÕ<\AMüúvì–cÿ¤ct m§ÔO*4Ðk°†LT ¼­"¨.ÖLTƒdBÛå(°´ß=ùȋܧr¨\«Úªüɇqƒ§YLEšø¢6%Òlõ¬ µñ·GÇÆv nn2zCUæ-¥{­5-+,œK&ç¥WŸìB"R ÛÔPp*^ÅXþ-÷­±¼¥¬¥áÕWÕ$jéŠP'°§±*ÔuÍ:ýú‰‡pã¤; %¦²P¦ßêšØTôu®(­“-*(+ÒÅC׿¢}€>[4*X°Øó²ð‹«œ©t¹¨ÅüÔ½jñCÖâGc]þ§7ßG郌WMÛúaQý°®â§rü¨ã ¨QÒÆÛÞ«k'¬¡AÐÍ©i HZfŠ$fP(S&ù(¤DJ ÙK!Ü6¸%R x•އ¦ÀÉôOøú‡uJ‚‚q4ýŒ¶5:1ÐðFïÏ0®êa?¡”ИmÁ%Ró1 ÃNTmÙ{@Yc ÃÀRPO_:Á#ïy%úspD²JË0™éO7 +cqâLÞJ£ä#ú÷±ÊÕâÞ¨’S9«Ù—p¥›²:¦,¸YÓÄüD¾‚å™jn¨6b³ŽmÛhy˜~(Û# $¥²+W&/Vol~M5ö“ZÄj2êI|óR7E:‚Aj)Œi0ºÒê3µCM'³ Øxk,—C5³šW³uYäÃÎÆªP×µì`Ô–¥ :V:&Diú¥(ëAÌâ+ô:WUVfNÕôf¥Ûø©Æ+Z\Ó#W´¼ôÈ}FÁ$}†Œvîa˜úÛþû¯ß¼h\G!Y¤ÎF­äRíCÀ|«}OX»Ÿˆ)mÖÄq<ã߯Á”Çø°™Y¦¡‡é–ÁÚì‹DSDz¶üpº†5A¢gÁ2‰ý5ÝK…%*õ ÂÄ$V£†ÕéSU‰\ºbn¦¹éŸ:%b[[–ÊWlØ¢ãË ðHe]"ž]lüeŽ7K£¸‹Y¤ÙO$Uþù¨ä”ûôeo±äo¼¯åŒÒ!ÒÃÓà R¤@j¦¸q·xÒ¡ýzñø¨/•&è@©ú¬,#ägÔ m¦Y õϪ4±Ò—њ̂¤(5•8xfÓŠªÏ¨TAWÏžÀuƒ¡­³zV;Äç3~"7RrÐC'MJ4¥>ƒ¾›13Ë m¼5–‰ËŠQ¨¬«–ñv6V…º®e[ºvã¯þöÆò ?"ŸCP7nè`<ô°8±¬OY1z+ ‚*”c‰ ù+«VãÙ&B¼Þd ÒT´ ƒFý­¿,ðú÷èú³ûî„ as‰mµ±hÆ=ãO ×lM-u‚šþ˜“F a/=(ØŸ;_~²rÅžÆÆÌj¼E‡™ãΣbc‡TØÅ¨ÐÖ­à@|™–˜/€£c4Qê 5 Y•<](uÛüFm½.Å`ë¬]Õ«ï}¼eßìx Æ ó¼B,yÊl¨Ü¬˜Np“Ã75#­ÀB˜o»c®²<èÖ¾Ýýso-7ˤ£“ÂÚ¯¾6Œ¨˜ÅUž–‰›6!Aà¨Wi”¢bž±*MÌ"[]ÄJƒ0E c>â@T™üÄ$ÅXnu„Ñ*2³²ßr9Ãj´£¢Xך%Ö¿7{ãQ¢ÑX×ÕgVô³fwñ³¬·Æ2¥£bìl¬ªt]x»¦`Ò‡ƒD½ü`7¨ýOá×Ùvq¬ FòµÌxÒ¼ïØIbØ—`Œ'\é> óáä¾6-[b(Éëf܆¦díå#ê|$P;.\ãÛÍÎ}sf2®Û¾›-!úyn«-ñlUÓ‘*€‡+JTnvIÿT²6qä‚Þ@Ĭ‰,è$ÓVDj¹Ï<й´¾@éÞoÓär}¹ñªYX9àá/§= ¥`ï’Ñ$Ù,q¹?Ù®O¤âÆ”ì¨R"c¤Y˜ùd´ žLÕ÷Ý6“dŠQ)­¶fŠH7r|’Y†–?•dèЩ³×ÔgÖì¬Õ]Ê(G çt>P„?¼ñÞËoxµäjUš84$„éÓ1òg¡Œ²Œ"0ã/1l0„´Y*ït5@`9aø`r[³m—¶¢5Ë|Õ;è TÆrFlß:Á'4Ïë~³S>̲Ò?Ëzkt‡ìl¬Jw]*̶AþÒÁxa5%‚qF'\Û…`ÏC9äu¶]²ÞSR1Û)ÕUD¼ì­Ãgâ%œ‡™ÝRé>`̇wŸlß3F²q•Ÿ6dêÆÄ®5+ª M‰âfâˆ!,§ÌfбCM[Q˜}!ú9YPâ¼'ârŒQ( VÀ: Ez6íÞGX ¨YSEb{¤Ò³¥Ñ¥¯Ü¼]Ũ¿'ø ¢‘ M®³'…Ìa¾½¦ ž»±¨eVؾÿ°Ñ¤iéš ånÄ#§Ïí=zÜø JO`ÚmÇQØ`"-S–U*ŠB¶Ês£[÷Z†ÙˆÔíèé³ûŸd¦äÀpË4*† ùÈðar ¡Óà ±›iO™RÙÙÄúv0YRwî  0¯({g‡Š!ƒ^kÖ«o1¬öcûøi`±ŽyЫï}df˜Ïtˆj˜ŽDk²oQÏôÆÌñ¶ÇO’ÑqJo4^µ.ë­±qK….) <šjmOcUºëR7ÏR3äK1¦}”ê€hÓŒ¯?ñ–»žÜô¿c_gcÎ:Œ‹pbjªŽ±@½ûö¢Å Y¤™;u‚UŽRé> ËÅÅaeÔ¯#ÕpÑÊÚ Ä:jžµï‘êæÍž8_´,šoå}GO øç'‹ðìŒG¾ —£qÍG\-kãh,U9qõ–8HÄ͉2Ça=‡aMBr*j2­ÌR{QÀãrP ¢Tqøò9~6 œôÌLÔ=ŒÎha0ùdæÖ.}³fP^8s*N•Ð[±îg›±Äe]B/ƒ7jüÒþåýOà1LùB¡®ÂÀ7ÄeÝEüØÁ0µÆ‹nN^>F6<ûù¨KxËäÒøÒÓ þÏ›6‘­¼ï|±!!!ãIÉ+ò²t@ÆÁ„õ.·dåäâÚGÑc¦ &†ðïÿù|Ô ~Hòi&„Rä0oÚ$•ÌÎ&Öyð2DB£e\Å€ØVŸYí3ÆÌí³ýç¹îþhé :§¦cê¡Ü;EÇ% Ô%0GwYÖn¸¡ýz¿yæh”¨fGû‹¶úÖØ»í”Ðh —Ü<{EÙaOcUºëR^=œñê}üÍwaŸœ‚³"¬‰ÁºI7¦¿±D±¬˜Ùƒ8ðu6ËYýT;.ÑÞj§JÆd¿}ýý3 ÒõÕ.ÝÇùÉã€NC *}@åã[¿>3f«D˽K·—ã%\»VTKÚ©;k8YÚø‡`E6,‚ñ‹óØÂÛ•|B) ÉŸY—Úô˜é1tÑw?¨õ+ Àgú¸QSG ×D³¬Z©x6Áа’–b¿Øòõ›i •’Y 'LU;›Øj)=;wÀyž–’¬{ÇPXt€Õ»TdY}ÆÆ-6.Á±~ÿô#œ*ƒ\ 9íàŠj WÀ„ËÌÎÚ˜"Oø(S,†´e1'czcØê[cLP•0Ò8ŽÍA3ˆ "t;«r]—ª¢€¾gö œ[b‚£¬pö7O>ŒyÜ–¬Àƒ(²LúeÅÌÓ±¯³YæüDJÇ í|äµÉ,Q K é‚& „ûP³”úgUú€ÊDY^8~rÊÈaJ›è‘Ó„p¾jfß­ •@mEÀMKwkëÊs2¤¨˜8äÃ0-ø1"ÜšîéᎭ¥U=…1qYaÄõh¬.ÇÅãùƒ¡„¡ªÒY•UD%âMJxx(‘ z.æ†Çï¼’¬0Åà vƒ ¦âmKq³ô8Q¼Ï Ip&Æk¦d³ü‰U k}¶¿áâ¥yS< XÙÄWn;°>*+‡ô³Z¡ë‰KJºR²m«`Èó¢Yššõkô­¸qGmªkngcU¢ëR–g¸*¨WR7]í-'’SqàšÈáPVªjX­˜®!j}1mdQñÂC÷¹±t§‡YIâôŸe ^§WF*p3Vt3Ñ–²nˆ¾Ÿ:Ó¦e¢uc‘(h°ºxåù§`‡Æx .‚@éºlYýÝ¿ÞeK‡ñ8 iU~òìü¯/>k¤³.UC©L5! ´jV²u&¨´Ð"âì$| !²B&ŠÙ)”ŒP"g¶”m:Òuñ„'Ž@a'¬ ¾8.Âwzs¡D6{kí¼(²¢ÚÙ®òT_®\ËYôàÀøË°‹F Û¬­Ÿènl`AÀe¨#]m)²[öm<4o¶«µÅÛ‹– ÷üŸÇ´ª­vµÚJ}‹€°"Çâ)¹¹ XEàòSkÓ¾’’ <±×][Z¸JE¥‚ÀԮˆʽGN°ß;ô1pæ/–O±8ìÆæÌzHÙNB@X‘“€—bA@AÀÅ/Ž.Ö RA@A@pŠœ¼+‚€ .†€°"k©Ž ‚€ 8 aEN^ŠA@C@X‘‹5ˆTGA@œ„€°"'/Å ‚€ ‚€‹! ¬ÈÅDª#‚€ NB@X‘“€—bA@AÀÅVäb "ÕA@'! ¬ÈIÀK±‚€ ‚€ àb+r±‘ê‚€ ‚€“Vä$à¥XA@A@p1„¹XƒHuA@AÀI+rðR¬ ‚€ ¸Š\¬A¤:‚€ ‚€ à$„9 x)VA@\ aE.Ö RA@A@pŠœ¼+‚€ .†€°"k©Ž ‚€ 8 aEN^ŠA@C@X‘‹5ˆTGA@œ„€°"'/Å ‚€ ‚€‹! ¬ÈÅDª#‚€ NB@X‘“€—bA@AÀÅVäb "ÕA@'! ¬ÈIÀK±‚€ ‚€ àb+r±‘ê‚€ ‚€“Vä$à¥XA@A@p1<]ª>Ù¹¹'Ï…G'$¦gfeçä–Ô+q©êIeênõÜüø6nèÒ¢y¯®ü|}]yƒ\¼¤z®@{ë]ÒšUC·’—`—cã¾Û¸õTø…«WKš5iܤQþõk”RÛZ‰@Nn^ZFfrZº»»[ÏNçL×&¸¥ >©¼A.Ø(R¥Š@Myëk(¼.^m糢¢¢%«7l?p8(°é¤Cûvë%rqÔ¤zu ˆÑ±³a›vï‹OJ3xÀÂS===\yƒ\¤!¤µ W~ëkÔ.õ8NfE™ÙÙï~ùMTLìüi“Æ èá!vN.Õ=¤27 P\|uûC߬ÛÔ®UðSwÏoèçwÃegü7ȨK™u|ëëúÎxTg²"Ö¸¯òEBrÊÓ÷ÜÑ¡Mˆ3_Ê*ŒÀÅËÑï|±¤E³¦¿xè/OgZæÉTáÆ“J!à:o}¥ª/7UgÊfPœ!%zæ^¡Dh0Iêt`ððxº.ع•‘7ȹøKéu×yëëæÎzR§±"ŒC±%BqÖ¾µH‰œÕúRn%`ˆ¤ëÒéÆ•Ì¢Ê·ÉTe%A ¸Â[_êJÒÊ"à4VÄŽ3Ì«±%ªlÍå>AÀ™ÐuéÀtcgUBÞ g!/åÖYœþÖ×Yäoæƒ;‡áU…Møì8óê›ÙØR– ëÒéÆtffkgVòÙ ”$ˆ€sßz>ˆdeç°¢çÂñKÄ&|5“K‚€‹#@¦Ó™o~=å ºù˜K‰‚8ñ­üoÎaE1 ‰ÊUãÍyH)E¨p¬E7¦3WGæ¶ó”7È6>rU¨&œøÖWÓI¶f8g_1Þ±j®«ÆÔô æ$NhÙ<ÐÓÃU\ùÑ®……9yyš44¹ÁÄ“MñÕ«õ½}êûx›µzEgåæp—¯)3/}{fvNñÕb/oßú>:ÒÎR–l•gýú.£õWÉèÆtæ ÝâÄ5ú r–™¸ì‹iYÕJÄdåä»»¹5ò÷×·ãšA)pûû»¹¹éx;yùùäàááÑ ¾$`'f¦dÎzë+PEIZœÃŠð§^ôˆ¸ýÙ²U±‰I pæò>];ß?÷ÖJpË&‹¸Ÿ”ÜÈ߯G§–Wí‰9rêìÇß~ÏÁï¿ü[Ò¿öÁg‰)©³'›1n”=·ÛHuéõO¿$A÷ŽíŸðnò­ÏGFÇL6xáÌ©:ÒÎ@rZÚo_‡Ä/=r_çжvÞåjɘNèÌ7¿V5ô ª& ªõÅt`¡ ‡Nž!Þ;TÔ(Þnã.s¯ñ}9{1’wÈýö…JÒ÷Õªu{žèÖ!Ï[|ÌZŸ•³ÞúZ¬‹< s4h5ñØ×s#áŠ1¢±2cévøôÙ×>ø4#+«ê͹çÈñO¾ý~ÝöÝUϪúr8s!‚ƒ/ª/ÿš—³';á$A§êš­SÝ/¦Ÿ:/¿€wœo\br¥³]²fƒ‹^YéG¨ñ7:é­¯ñ¸Õp+ª!àüTMTH_®\Ë`äßÀ—µÚ?ýük/={ë„1¤€'m?p䧤õêåàKí’1Ò,Œ¶‹!Ò,ÒÆÏŒ¬lãU{Š0¦×á«W¯"@bͪcÌÔ*¿ Ð,Òøó›µ¡ƒÆ³°íºQ´Ì2UNåVϬ,ùYÇpø‹Iï%Ojaa‘ Y Jj²* uûK1q¬ lÇ0•”š†–Ÿ£*¬&chbü±z‰ÈìœÜ²”¹yùð9j[Ö½/Ôœ£A«qÀ!ŠK2-ïæN™ Ô=ÁŠ.ÇÆ‡ED?{~æøÑ\MNMC‡¥Ýü hòÀÜ[»´o§ž÷o¾Ÿž‘uûÔ 'Â.œ:å~Ž¦Ù¡Më7þûõ…K&ñø…KW~óÏ·Ÿ½waXdÔ·ë6· š2jØ·ë6aLðËGï/·UÕ¿ ÷d¸ëðQ}ö—vlÛæáù³y ÿêïo2ô?4oÖþã§Q:š2«™`M•œºeÏÉ£†Y&°ýøp©oÖlÜyè(œ¬i“Æ·MoÌÁFõŒÉ$,pÈ‹IÿÏÏ/˜wˤÃ'Ïœ¹ááî>¤O¯;fLùhéw'χÃ-zuéøð¼9~ |7îÜ»òÇíxóԻdž{8*½ÕœÉãÆ¹æwíùWÿI?Ÿ §: žÈ±Þþb)ÿ}þÉ+±ñ_¬\«*ÿá’åleºgöôŠv{^Àô̬¶ ìÕÝÇÛŠ½àæ=û¿ß´U­¸xÍGôï˃x{]³äØŠO—}Oµ¹4¼_ì`RÛ/V®á*‘*M3bÒˆ!*AJZú¢ï×0jñ“r;µk3yäÐJëú…JXp5DVdW‹Ä^ßg4¤o/ã úïß½ø›'"³ÇWßûDQ¢æMˆaÅöÏO¾Ð$)77kh¨ ƒ šiØÉñsçßÿjvÇØ0{z˜ª»»;WØ1¶2brHÜÇß|Q…–[„Jfõضÿ…r€«IÈ A¥ûcJ ¬eë7cô`\([æ)$ò‡­;,%aåÖmñë·ì;h²îtwOMOgt6æo£zÆdŒ8äÅdIÀ‹¹tÍ(‚‚Â"¸ûßü€÷ÔÃÝ´âdØÈÂÒ·3¦Eß­NNM'†nëU«¼^¦^^õÚÁO¾hYKš¯mJà}÷.ݵPÑn!cǶ­Ó³²ÖnÛ¥Š0þ]½u'‡À@‰Ø…"|nÇÁ#ïõ­JÃpô>W¤žñÐÉÓúv _ÿô ®Rµ€Æ(@ÖlÝ©¼ÿõ· Ñ,  tÐÛË“ð;_,MÍÈзK@¨5+²«)• s"½ê²¼ “ ö‰°{á¡{^ýÅÓþÙp#øÇò ?ûû5øÛ¯~þ×—že±E<ꤔ´´ÇÞÎpÃÏö­[ýî©G‚›©[Pœ7oþø·ß=ëbì,BÝkü{>ò Œ²zåù§þú³ mìÙÙsäÚhΥĔ´YǾðð½o¼×fuȸŒ }åæmÆx¶ëÆZsç!“ž±o·ÎTàÿ^ú¹ñ¤{ªgVœü@À/f›à–ùÅ3¿úQe¶Œîìµ—~öןUõÌ…‹phrâ7ÿðËgï[Ø´±IÚjù:èÄ:PºSášQó}sf.¸er%º=lÙynصѬΜ •uÛMT‰‘„š¿öâÏ”Šÿäù ˜Ï ]ƒ0=)t…% TŠìVièrØH}öB$W;¶iݵC(àý{t%€jŒ–€úôëÞ…a—ýkÈÀUL–MçÈ,˜3°g÷ $¶³ˆk%þCrÎ/DV²%°u¥e` 1GϜө†ô鉰kûv¶·οe28pXt|‚¾—€íº1ªª!xæø1¬8ÙÚÊæ8}»=ÕÓ‰% høbB÷‘‘0Ù‡5'ÿNíZ#niè× k“\é¤T¹HzxSx…{wé4b@_"èdåþ­\· i5¼dºËn\n!ÁRc00žø^WÛõÏ–²"¥£G¡?´_o*O&ƽŸ‘WLCD‹fGNŸÝwì”üD„uC}üüàëeÿþì+Ôˆνõ×O<سsÇrŸQ5±+²«É8ñŠtéY™,¶Pô=ÈÞsóóù‰7?¶šmÝJ_UaV`)ééZüÓ¤Ô”‡4Je¦[ xyz*eœºj»«9¨HD54?[¾Ê˜ ù¿þ©ˆ—þYV(& ¼q×¾¥k6ÓØ®Í –Rê.ãsÙS=cY|1½¼® †žž¦€– +baœ®«#Û^¬¢Œ®ïc%ìÿTºÛß6eüáSgž8Ý¢TS¯JÔ¢£Ðëçmã1¤e`3¶ƒ$•¾þ)i&…¤J×ôð!·bTûŽä«`ˆCþêœmüØhŸ¿ÈwÕ–íƒ{÷¼gö £÷2ã]j.Šìj»àæ&É üæÀñS¬±ô=ÿþìK¤ ¡!Á¿yòaèNVN.ö×úª]VVÈ×C³ÉÖÎ"tt#M ‚BéHF†§ ŒWË Ï?ÕbyÖ£:íº)7qˆ‹â“S˜Ñq?‰šì©ž.H‚€FÀ/¦ÎÓjÀø²KK§¹wí}Wï8Kââ‚âë»Ø°ŒV)­zR¨t·ÇMë-cG}·q z4]]*ÌX¬¡ cÏ”b2hdrëŠ ˜%œv·FŒ®|ƒú&é5K>$Á=:ݰÓBmÈ@$6 g÷“aáP¢SáÑ©Až+ÔŸ|ä#Ô&l©„jÓsVñY÷é‰4ˆLØv."ŠÛºe©B¿t$T9È_A Ö ²"»šMFŽlÄÀ¤ñŸ/BéŽÀ™¡‡›Y–áÜ™Àôq£v9FüŸÞú€}"QѱŠâ¨}[å£ùìyç‹% gLµš¾ÒEôéÚ…*±íŸ}gÝ:´‹IHÂkE(‹o«eÙŽ5°ÿÖ}¯ä=¶ëƈŒõ±³çQ½:u6A×à1ÅáÕ³]y¹Zk¸ /¦%Vˆ{ß^´f½Pïøô±£T2Ø?þ„èçШ;¹Œ·×÷f«©I$óõêõ왟ÄNH‚aTztc ÄCáÁˆáîÏo~ˆ“6ÖE—ÚY#+Ò¥K@¨5ˆ¬ÈÞ¦ì×½ë¯{==7 )cŒ`pÄOÉ3÷Ü¡–hÈÃuøcÊwγÓ&‘U/ûYXü1¤ªãÌ,kVé" Ù}6 g7„êä%b?Ý3÷Þm©e)öÄáÓM{aô§Üº=0w–rp‚9à<¶ðv}¯Ã«§s–@­G º_Ìk4ßAÍšáE ¯tcºîœIã´V}ÞÔ‰jÏ{KÑ/ß3kººÝ ‘K½z˜.6ˆ@lBBå*v{6ah_hª”Û§Nœ>v$Ê>ؘ¢Dð!6Ê©«íB‚]p2-, DX—2@]âo¯.š7›aƃ'(Û/î›;“Kø]{lá\F'Šýül×`¿ŒØOß.A Ö à† õæ?Ì[‹Lg÷à-ðæ]õ1«DFÂàÂ.3µ‰×,Ïä´tN4c½Ô¬)þ‡Ì®ÚøI[`þ ßjä篘VY‰+][ýã“ØT‚Ì¿Bu+«&–ñ¶ëÆU<· 2Ú$éLnBõtY 8«';«\‡€VM™Tß‹©+¼fÛ.¬yX½üܓȊa좰´8Æðˆ…M³&Mà=ú^àFÎZæLe½ÙÓ±Ý1llbbAA!Š0X )#¤²”Ï"Ï¿Aƒ²®ã+];lÕ&[cnŽ Û®›í«7¡zŽ}XÉÍu¨¾Óê3"øiÛêšµœY¸ˆ%Ñi¸1Àëše´Štl·‡¥7šérU€Ò®ÂÌ®¢SƒK™EªŸ\bÈ×êU‰j dÔšg–A@AÀ‘Yb"1‚€ XAãè[Ǝĵ£•k%µaEµ¢å!A ú`וl¼ª~˜¥AÀ™ˆÍ™èKÙ‚€ ‚€ à:+r¶š‚€ ‚€3VäLô¥lA@A@p„¹N[HMA@AÀ™+r&úR¶ ‚€ ¸Š\§-¤&‚€ ‚€ àL„9})[A@\aE®ÓRA@A@p&Šœ‰¾”-‚€ ®ƒ€°"×i ©‰ ‚€ 8aEÎD_ÊA@×A@ÎA«R[dåäœ8Ÿ–™•“›WRÏôO>Vp«Ç¿¾õ›4ôoÔ¢w×Nþ äˆM+8¹ZTII üä¹ðè„ÄtÕÉKêFw35Ö¯oã†þ!¥ÖÏ××Y Tw¢Bˆ—¶šn8F®W×NNl¸ U_»Š*Ó ŒP—bâ¾Û´åLxÄÕ’’¦ý›øûúùú\%+“g-¿§t&MLJ?™²3ËÝÍ­{§ös&oÛª¥››ÀærOç³ró¶³"ëb'¿Îý`ƒa¹ëÓM¶G§s&kÛ*ø¦5˜4DÅ ¾ÞjÜ¥.E5\ç·Mß&¸eÅr“ÔuaEnöü‚‚Å?¬ßuøX‹¦NÚ§KÛ& EìQÓ2sއ]Ú´ÿô«ï}4z@Ww1̪€*)$rÌÀn#ûwÙqøÜ·›FÇ'áé{4ôó«t†rc­G@&u{›˜qŠ4”()5õ…{§Ô](‘½ØYKz`’à ª` ÂÖJÜMBü‹ŠŠ233?X²<%=]:¹%îºÓ&¤¤¼óÅÒ‚ÂBË4UÑ ñá’¥ q‹Œ6UDU7\br2 WXTTÅ åöZŒ€°"»WS(Î=½`bûævÝ&‰ÊC$ÁTÁ–)YˆQy€U×uÕÃsss—®Ý›˜$ÜЪÓ^Ž_¼jÃ{¬4„ ä«x‰†{jÁDLBmª˜•Ü^‹VdWã¢V¸uyﱓó& Jddv'OP[g»ï“„ŽDäóòòÎGD:6oÒ`éä¶ÁUvçác‘Wbl§¬èUiˆŠ"V¡ôªávÌÒcÒœâ’ÓÓ2s›5ñÇz†'`lÉÉ+àe×CJ^~áÕ’«zÀILÍ(.¾Ú2°‰Ùã²}½ ðÚî‡ú>^Œf *ñ“¢‹K ÝÝÝ|}¼U GEÅź>•ȶ·pAjFF%n”[j=Šl51 CU~~~N~^“†ÕÅ^ûtu|rúó÷LíÚÎt’À_>^…éñ³ '[Öìâ•„ý§"¦ìÓÈ¿Jç1½ø¯Å#úvZ8u˜.âÕWviüØÜq:ææ82%=3´ÁäÅ©ãMhÕÃa¢Øåå´ll>5Þ„:Ôè"øú`žRõë†HHÉøpùÖ+ñ) Òî탹m\ƒúÞ¯|´rôן- ž‚~ÿî²&üöáYç¢b—¬ß“˜F<êöIƒûtn£›cÓ¾Óßo=¬~Bb¸t¼¤:/Û|°w§ž[ëH«7¾Þ}MBÓªy“q»áÐuù‡¶:ûÞoî·zK5EÒp¦“+eœ©&|kr¶ ÿ5ùñË©; /ìò®Wûƨo6À°Æ¬B¼´Ù¹ù:26)}ëÁ39y?ÅèKfªŒøÇ2C–ef)ÕÏûgŽš<´§ñ’YJ²bgLàð0Ç¡3hƒ¹C 5^ÃÚ—!8+ÀMæ\%övrz=,*.+'OaÂKBGå¯ú©úm>^‹ŠˆWâ.©ŸVaäd†ø” Ëk5q¥#éÕêõÑJg¥n¤Ó–\5­ªØc+×ÆÊ“Ã'ßog}5yX¯ï»exŸNg"L¤‡ÕÅ î¡©™9Ñ ©¤ŠMÎÌÉÜ£=áUÛŽr¸Û¯œùÒýÓ‘'q»åSÌŸ4ø¡Ùcúvi{,ìò;K6éaB´¦úÉÓ3.EÆ$Y½ª#U–öèÜq÷ß:ÊÛËs鯖£™’›Ý¾”bŒ¤t:˜1¦BaÓhSÂãÞg…rĵ‘•Ó²Œï y%õÌùJ9·Uðr`ÿ˜ÄÔÝGÏêßEßzèLä—kv3Za»ðÈmc==<¾Ù¸Ÿ«ÿ|í¸AÝÖï>ù?Î`lúç¢uãw¿}â ÿûlµ§‡û ÷Þ²fç±Õ;Ž1Ž ÷¾gúˆA=Ú±f÷ùKñBš3rýññ9ªˆ¼‚¹žüŸ¿{êWk÷tnÛgK6ì;zîR§Ö-(=0 áƒ³FIÌWíd´ÂÇîÎ#a·MÈ&]OÀÙ„¶;t`’•L `n?ìûO]¤·(ŠŒaæè~ÓGõ½—B÷£WÌŸ<„üÃ.Åýë‹õ·ùX½óØØ]ïœ6œxºåºÝ'Þxénƒ‚ ó±šS}¼<çM<ºü_TH€ªÅ:`†F%~Ò]2¹V´!̪ ÝÃ!=;0p©S› „F4ÖÓ† ìÑ~ã¾S§.\ ip2ü W‰a|ºNmƒÐµ¡:öÎÉ4DQñUï5e=;† \Ò«ÃÇ+¶8Á0Õ< á–o;qþ²›»CÊÏïšòEkÉsýž“H’FöëlvÕËó†Y†cÈv%=„ìÓ•; n„Õçr\2²®ÄÔL˜ÓøAÝoÛæýö’Mƒz„21*Ü2²÷”὜m÷±p_=:†<2g¬oýkʸë9Ù÷õêöÕAR¹"+²Õ&¼~|­øk+]•¯6iáø~Ûa-¡Äo7îïØºÅswMa¬yÙ–æþJ¹Èݬ;·a]~1:1*6‰”ŒnL?Q1Ièà’Ò2Wn;Ò¯kÛgN lòõº½j™Å"òlD c–ªlñÕ’–oKJ‡oa挅3ò.!CÈäîî~ïÌ‘l¹üǃÌhÜbS)×!s€UÌ4ÚÕ ¸ÕÒë`$8óQó±=Ì?6)íóU;ý}ëß3cÄó&´ jJgcÆ‚:ÃÝO]ˆVž 7öU?·c6Ua³¿¼Z· RszhΘV-¾\³çläµ™’¾fU䩺´Êʪ•×A]-+³jX

6¥fˆn×Ñó¬Í™;îÌŘﮫùÌ *÷§j¸r“I‚º†@™ï@]¢¬ç­úxWVÎfñ3G÷Ýwâš]ÇT<BïÖAMœ»„tFVnbZcWÑdzzkìï«4ôˆ‚Ÿ$ÌÒ»SëÃg"I3wâ c,ùÁ²-g¯¯Æ~÷èlm°÷x8ãˤ¡=Û¶l¦JÔ¹ß;ssJ]¿@IDAT¢©÷ŸŽNLE¯Á"òîéÃYÁ3¼2€ê”Õ¸i€WGåkbž pÓ_;h³â„ù“£RáaCZ4ùý»Ë¡Î°p“@bïIH9]…€¤E@CÄ,ñaVFȺG “º¶kùÝ–ÃÙ&G2–"OKÉA·ÐVZ€úÛGf½úÑJ ¼ º‡ÎÛß2Ë ðÜf‚ï}óã„ÁÝw c†F¾by× 1¥³ë 1•úQ¡†°,Ñ ‘09}‰œ0ëþÂP7í;§aí4¯TžGä³FãÒðàéÖKˆñÿå™ùÆM:+J=J›È3 '¾½iM½z ©#úšÖZØ/cïHO0»jÌ„0Âi¥‰Ã’ A#TìÀ©•fá´aGÏEí=qzÍR49ýš#(„‘ˆ—…°sBÓ £â¡’Ó²ø2 i.nVPù?«w©[~ù’Â5Y‘«´ [0Ç!"hý©“eP®õïÖ©xÃ7ˆõèÂ謻i–}LW ýê· n†ˆÛÕ©¶¨õ.º M‰TF7N­g”1ƒ5”È”¦tÈ¥Lò<´yß©[•‘ÊAÑ×—Ý ye*Ù”‰€îávŽjjb$Ç$y”²g:!³ šk–+¥ýPܱM fåo70J2Ô%2#`)ŸÐ"O¶5MÖ‹A‹<•䀥R‡ÜüB-@UT>ô‹{¦A³lä`„C DwL‚–°‘_ù›=•ÄÁNEÃmã½*ÌŠˆMgGÏF}¶rÇ‘³Q˜ãÀMÇê¦L¸OÐ@¡ëìØ&H)­Øe†½ ¤±y3ùà°Ê,g¬¨‘~ˆÈ92#Ì¥ØdN¬ÓàU<»iŒ,[X_aªhåê9’Cㆠôhf¼x).¹sÛ Œ&¯$˜¶Ñ•e¶€1´G°ós‘±Úäߘ•á*¶š¥H²š…€°"j/¦œ×…ÛУ{gŒÀfâï-G¢Ãžôh0´½»tsJFv÷ö­_X2êµoˆ@¨W§Ö< †5Ü®cçÿöß5®$°LW³…Ùsr/Ãè°Þ_cˆmvÕì'&´ŒA˜ÍrH—¸×,ü¬# `"ѲlꆲkW7»·?‡ÎµÆjFLPéÒi÷ž¼`Œ$T*É8}Ý Ñë‹Öa#\–È“[Ì$fêrmä`¼¥,ÁR(c²¾ÿÖÑÛµD…ÞnŠÈ ƒw]s%½Ó2<†…'ægœÁô±Bƒy2Ò·`}E‚ˆ˜$È †ÛÄ îÄüèÿüêЙظ;bpèÕ1„1gû¡³–W¹Ù³käÄù+/¼þµ²Ð÷YMtÅë4œ˜`W€êÖj2‰*‡€ØU7GÞõç'nÓÙukßêýß> ~¢­çËÆ%›DëÚß$œ0…¿>¯7¿Þð³;­œoe?’²î ðÃŽ£ìÀ‡a¯¿Á‡f¶Üàm¯~¼²K»àÇæŽ³'qOŸ@ rÚÆÓ &áéYõÔá½ØÈzì__¬_8mh¿.myññçÄÁ øRnÌ4t/øž¦ÿøøVeþ`CƒÀªíGv=o$Ûp¢"Ìj çãðŽ;üø»í(³:µi×òîBhµMÃQ+Ã¥f•ºåœ<´‚¢–÷Ÿ¼øêG+‘-)·Ô[ž7¨;EXdéÁBˆ@h+“C|làFÈÈô¸…"ˆäˆ_Ä?hö¹ܼ ”ˆ¡ìR|rhpàѰKT L²´Œlþ¶/͇€|jB@XQ5kW¶¬†qé^º,Ö7Øyp´åÙÔýº¶# }˜ªT†:_nT‘ônyù…l¸Õ5—€ `Ž¡àÈ?¬n{ul¢–ÓÚ¿Ý´ŸôHøx³5Im@ù«2ùÝ£³±ÏåH,Ä¢§#L‚ NXÚ«ƒ‘š3k²¢@؉†å÷Îf²/+7«Å—èòt×sñJF‡þ~õ•æ1¨icÔUänŸ4˜PÉW?ZÕ·K›ï›ïä5˜Ð}3GñÅñ7 –¿kv›>ºo#?_ìýÖ7Þòä¼ Czu Ûz%õQ“†6Ù·3¦BH¡¹P”õÄ+B#\ƒ¨q¬,‘ SàW>ú~Ó¾S0¡È˜dEz`QŒ-Ü¢Nµ;|&Û²Íu>ä#T¢A«&`íÊV™aWÈâIßÀÁј_`(›H´ø˜ïè«:Àˆsðt'Wwk¬N®Æ|{âÈãP.?™ùÃò˜'ƒe€1Èþô,âYÛ½ûÍfuˆ¬en#@ƒ-º’Á=;°óèͯ7î9~‹7&×¶-›bæObÎèà]ÀÅÇËS‰búDP=Ü^y¢}Ög~!RBÈd™›1}] s< òž=ÇÃù¢¢‚’Â2["À„t¢ã'2›‚¢¢¿ýw ¶ÌÛ´˜?ÙD•ŒŸ f¦dð¡ ƒ»`gäQ_”¡˜y!éñóõæÖ?„Ý깡A»eTb`BüeDâ/­ b¯Æ­cús®" ÍÖ{û„Añ)H4yR·ÀÆ8xûásl[ƒ¨¡'UüŒ«òª aEÕl9Ù²hV)8RJ00äKØÎƒ£ÑÖ›MÑO»VŒt |òýFF.ˆ‘Ù1OñöÿÜ« 5‹­Ï~²Lÿú w©ô¬üt2ØÛØAÝ J ÔUù+Ø@MʪíG1BÃ+‚RÃæë{{C‰’Ò²ztAJÊV)5}²MIPñ À@P¥-‡ëx½;µùñÀiì˜P¿Ýt5œõÜŒ÷Ô¥0'¶þâžiع¸ñxù)Ã{óÕHp¬ïß~~ `úW}•÷ê_ÅÿÏC3Iïíé©ÏiéÙ±µNó‹{§éۃ̽3FÞ1e(ƒ’Éné±ÙœËëWß[ñZ¸”¾%_•ßž¸ óGxª7§êC@XQõa[ùœõ*ÙvfgS#Á^²~‚h\a„¨\ë*zÌ“é§8gÊv%åª  èݹÍÜ ÷¼øÖâÈ$º·ž1º—PÓ,Û|à•VÒu1©F‡ Ë1‚†-ðàí=3F6ž Ö¾¶ævõZKýêJD’ A¦žÑüâòŠâXý«õ{ÈùqY0e(_óçïTáæpÊ•”šI†Jä`™›¾K‚€ ”‹€°¢r!ªI Ü`IP“j,u­{ Øµ|hlV,#í‰AhaikRéÜì)QÒ‚@-F@ö ÕâÆ•GA@ ¬¨`IRA@A@¨Å+ªkû éšñ RKA@AÀµVä„ö1;Ý ?Ôø™µ]³ƒ¦IÌ‘CúÜ(\ì[=Bܬ c–‡c—U ™pÉêU«‘ÆÒ%,Ü|è–êEÓ›_)Q\±¶¾I ľ°ß¿»χxõMËÊá”DÎl¢lš­ß}‚ãWÙ5öÜ]Sðëø×OV±=‡³‡ ./þkñ˜þ]qסšÆ:â oÿ÷ó;â’Sñ›LVœ¨ðÔ‚‰œ†fµ ýœð'ËñͪÁq |oň¾p)‹S¢UÛŽš°Ý·s[¶CãZÍÿÃûvºgú*©o™;qÎÜt‰ª d¨øÂž7i[ýË-‚Ëñ ~¡”{‹$º†€ÈŠnR‹ã¼Ž*Ôûýq²BÁxLJâÀ!؇›”†GÒ\ŠKá¨E®r^&þg[·ljftmgyÂö÷ÛÇ%§sv~Šñ.CU·t)=·ÈX¨„k+eÉ)-Ÿ—¾m©c8áA‡U ¬œùØ–¡â½sBt¶tQå:Y¸T ©ÎP‚€ PûYÑMjSb<³>1o|cÿ8°/LÉÀgÝòÍñ “h:kšçe"õ!þšÛûèD"9U1¶4M#IÂ}>G"üú¡™i #ÜK¸]p í>Ž(!%ì 8ÍË~¯¿æŒ!Òèñ9öÕ²‘¥Eÿæ¡[Õžg³¶¹¢9ròøù+Y¹yªæf·˜žG>µ˜Ds9%§Ó¼öéj3±â¡Ó)J7 ‹ŠãìO\-ë3:<0›?âÜ\ qvDÛàf–9[•€þgùÖ²d¨ûN†ãê}ûö9%× S½a½;꥛‰Hy¿þþß5¸0ååÂÕòcsǹ~2\dçä¸p…¡€Cy:Žq­#åëW P­špð­Yת#8Èc:aEŽÅ³ÌÜÔqÐP"R\ŽKîÖ¾UBjæåøNdÔ'@Á3Я‘ ]é±hÈ~Lçþ4Üsì<ƒ;dˆQåï˜]•Óê£ç¢<=Ü¡DÜÂa±Œ›À‰›Ì=Æ‚ÔñפñððÀË-ž÷Ÿá^ÇŽŒMÂ9že5–ÿxy•vC€ÑVŸ°Í\ÂXŒÂ÷HT¾[h0u[½ã¨ñŠ“O-F rJôVÈ)éK0rä”ý»™‹‡õꀿuoÏYcúÃ98Όɛ“¹42qIi‹VïF³< {»ÏWí:|6á¨eμˆˆ”¨õû-‡µ¢€F†J`ÊÐ^ÿ·dÃ>-C…µ¾jçÈ~]úwkË‘¢È5Gè H¡¸6Õ%"]8m kÔÐHjoÛÕ ëæ×±»êJº`€OTL"‡¨œŒ£96à\€rÁÚVk•²C¸S3O1Hr($$˜áHÕZ®d^[Vt3Z–ñ‹ã t3iÏ!ˆ,MÇA_+Ù­Oç6[Åá/çCq¤+Ñç/Çï?M/9¯·>hš!›«J’D€õwQñUΈe8X¿ç䈾9%ʲ ˜‡*¨Ò88ví®ãˆ¬8.íåÿ|ÇáØ–Õ€u¡Åã(lu 9aûÀé‹EEWÿðø~ª_|Ð ÈP×Ð`$Uf·è{%P+Øs,ÜRNI‡4+Æ&g ÃâÌ×1»Ñm`EHb¾Z»çôÅh`A„sæb œjÅÏÐà@ŠÕœ-% ¤Ï+=ƒÏR†Ê¹³œx×ôá¼QOÔsêŒR–[žá^&Î7¾Ú`&©Ĕ9ãLa:ÖÔe?ð¶Ü¼üo6Ø{ò"Ë’Ùcûöh¬Ï#sÙjWkÅX1žŽˆÝq4ü¯Ÿü0¢OÇR²+‡U+äµ6saE7£iÇA_[ª¶ d2àÜÄm‡ÎþñýôóÀ¬QL¬e9úûË5{veý‡úŒú郦Õy×LªÒ“‡÷:ÚŸ½:†Ì?ÐjAÆ'´<™³Y5P4°.×Ü‹ÛÍNØÆ†ébtÂWëöÂöuÁƘxÌn1*áÚ‡€U9eiW¹A¬¸ãÈ9"‘&ò7*.™3=8áõøy%.¢{/ݰ_ñotd¨~ІXÍÙRJ†¼J–2Tú!”‹Ó¸d*46Å\“F~hx»’¿ dçXŠHÕª£·PntÍo\qqqJZæGßmIJ‡ íÕT]³¶7³VÂa½; îºïdÄê'‘Ÿ7¾IC?ÝÌV¨e +ºíزYc}4ãµ>äaïôQ}2²óØY¦ÞÞ¾]Ú¾ñÒÝHe0kxôzÕŒMsËõhÓz—mh‰) öê)DèV Ò·X=Û²:u£Ù Û¨çž^0‰ÅæÞª\þšÝ¢K”@­DÀRN ùàIÍÄŠð Ô¬AÍs‰0”¥›±7lPÿ|TÜùKñöœ€¦üù‰Û,s¶*%C«2Tœ¢b“Â/Ç£#C%ýìÂÉ0$«ÃpTÀRDФ‘-({Ž,uJ›B‰ŠŠŠ²²rþ³b[JfÎãsÇ´miÚ–!AQëŸý°ÓÉŸß5Å·¾#ìA@ö Ti¼<=›5ö7¾·Ä@‰ÌŠd G’dÉOFNºVÔÄòªÕ²âJ1^µ¬†ñªÕ0‹³ •k5‰¬¡ §ôoàƒœòÃå[™›‘SªQbÅñƒ»+n)A´I÷F„“j@ªôýº¶ÅÀîŸ‹ÖÆ&¥?1oÒP˜˒€"CÅÉVn¥6CJ† ëš5¶?$ÿñùZ|^† !þÄ0®}«@P’Úó—âÔž8Ë$Þ/x›e Ëm ë«Ü„UJ (QnnÞ7› —|`æ¡De 2àƒ ¬Íà‘@WVJ‰,¸aj´¼,1‚€ P‹Ptœ¿|¼<<²Êó&ª¡°”SªKfbÅ?Þ¸Eµ*^5Ö¡"Rãí„ÓÒ³üêû˜E:ü'æÕ…ÙÙ9÷Ÿ lì‡-‘˨}‚T›¶@§ü’Ô¾g”'ª„Uª’§ Pc€Ž¨O¯ÔÌD,5¦êή(X¥fåúlßZ’gòÚU}Ô@yyy‰)iQ i£ûw†5V_Yµ&gPݯSø•¤Ôô ¬5Ï%RÝÈÛUÝKþ‚€ë" ø»»;¾¬1ù¯WïxØ%×­®‹Õ ¬ÜJJšïÝ÷ܳÅɦSwªãƒœ£°°(''³M°a~u”R+ó+ŒŠŽ‡]@ÕÊ&®Ž‡VT¨Jž‚@A@Q"XQ}oo/ö‚á³§ÆÔÞy¥{O5KŠ÷åˆä°°è‡(ˆˆ¨Žê`QTPP“›Ÿ’‰;¨:î—¨BƒUHJ@`¬Ð½’¸Î" ¬¨Î6½<¸ POQ"lQ½½½½¼¼ΟJJËÚqØädH>¶¥Ä´Ì¡#‡z7kÎîЫ±1ÑÞ—{ø°í»*qµ”²ûŒCQûׯDeÝrõjI\rFØ¥x½¬4ŠÇ©,~§Œ_ŠÈÍ/db…òq`bˆ'scZ$¬È¨Öî¬dZín_y:AÀhÐA‰êׯæ±{gýüâoÜMÇÈàPÔÖuûØ!ß.¨Yhß>ï¼›ó§ß»ED\ÍÌŒyêñ —_ñŸ2ÕQð *.¾ZPXÈ4|y6j䨜aB_¬Ý‡C•aç6-îš6¤Š†l?¶~ïic Ÿ˜;æ‹uû:†4'scüM óDÙ°¢BXnqKèð7­h)¨†" ²¢ÚpRmAÀ1(Väëë[´ò{ß’’&ûwyäd½½x£:’Ï1eÔ®\@æí%ý}¼‡õì n BBÚ|ø±ïàÒY¿°0þ׿Jûü¿Žzb¬ar`,\PXà@¿;ˆpo8€÷¦1:?qû˜AÝÛ¿œÀ>vcµüæ¨"n4‹äD³˜)C{Ü1yú6ð7»Š$ ßQf‘ÕúójÉUF1-ªVœkMæ"+ª5M)"TÅŠê?îvÖÏýÀ¿AƒÀÆ1y…xAœ?yðè]eÇ“†•yÅ5ðöÖ£S“Æ4h€¤ÍÛ×7ø­w^þSÖšÕ$N~ã_…±1/þAœ¾·ÒEŒ”¨£Ò™˜Ýx%!õr|j¿.mfŒìÍ%ü[rDÆÑ°ËsÆõ[µýØ¥øœpr¬¤[Fô lœ’ž½hí>Òàì`Âànu]¹ýØ© 1íZ5;q>ºic?8v¡Ô-4H9:7+RõÕºýÐ/âÉóÞéÃNGĬÛ}ê™ã¡V,ß>¢oGêóÎÒ­8͇«™Ý^韥ò6¡D•ƯÎÝ(¬¨Zšœ÷PTü¬–’\/S-£Vû›Ô_׫¦Ôè˜yº¹å}ö±¿»G®ÇÕú³fç4 ðÈÌŒÍÊ[¼~ßæ}§'ëÙ§K[®Ë± Ÿg˜WcKèW¿_Ƕ-›5jÔV„=b¢ô¿¯z¶ Nûä#€ÊXº¤(..è/¯¹ûúV75Œ ¡AÔáÀ1âÔ ú¢ëÖµ]«!=b³VbjV×¶-áC÷ÙrðʯÝÇ/dfç=x눳‘që÷œ n†$)-+·½›Û¼‰Vl=º÷ÄEÍŠ–l<äíe¢ƒm‚fí§‹8p:2ür¬1}9L¹Ô–ƒgGöíTT|‚³êLÊ­’X ÁØ ^ú®ªÈÕ§ê¹IµaEŽlbõâ1~q¾ÀÉ Ñ,­8,,'ß‘¢oGV·Úò‚ 5ðñÆÎ‘¬8¶–cÚH=ª6¼«šqöŠåîÑÑŠêuêæ6qRRR9""ÊÌÍOÍÌüzÝ^N†4lÀYÂU-¬¦ÝŸ•“§Ü8aâçíÑ=( $¨9Ÿ¦M›6lØ{,£ßäfO?ãÕªUâ__­W\œ³}[Ìã¶ü÷›žM«tZÙµùÜ¡Ë*ut®QG¦Âœ"GûÔ÷ö¼uLqIé'/ÄpéDx4TJ”_j7ÇULtæMèéá¾óh8‡è†EtäWzf‘“>vþ 2¤‘};’òlT9ß6®?ŽË‘ZÓ§skJ! éÚRçæ€@I=¡D€±Îd!¬ÈaM í5'r¯Þuüü¥Þí& }ùùbî!pX15$£¤ô¬‹1‰[cüíܶŌ‘}BCš3˜VÕòq%Š32Ò?úÐËÍÍ×ݽù3?ËiÞœîJ3!ñòÊö/(È/,Ì.,.ÈÍIÈɉc‚±Vù«éiÅ©i¦+î^Á­êÕü®½±nõ óžînÁ ¼ü|5ôG>Ô¬Y³ÀÀÀ&Mšøùù¡=3ëÒn›ë÷ËKrsóOŒ~àÞà·Þõn×ÎlN‹ jÚ²ÏEÅîª*ÁN4¹E€)†J eYiOpCù”žÒ<€C¬¹Š’+¤E(GÎA‰ˆqsÇm“ÊÆôwònV5hl¼<¯½þ &¦R¯^×¶A—âR`cÈN_ŒÝ*ÒßׇüÊNB‚ÀÍE@X‘ðæíF>„›µo7Ø: 7óT„1ñ,‚¨ ë„GÃ_ÿjãíæMÜÀ·¾’9wɤ}úñÕŒ M'Oiݺ…|¶(öÙ§ #.^MOyⱯüÅâ$×gÁ¤P"8 _jÁHUñö¹Èx ‘ýL ¯ù®ÙuâÅ›ïVtøœé® }†ô …fQ(w5kì7n`3$é-š6„µiÙ4üJbWǪÏ*TEI,Ô«'¬¨J½@Q¢Ì¬ì¿ßñÁãsÇTnU¥J¸üÍpÄ}:¶nðÙªÝõÔüq ýý„¹B»¥¼ÿnIA5i¼àïÖ­éÏl5W¬ˆÓ.Þóò”k`mðK2]ùŒÕ?¸¥$×óòò jÙüî{`WúRÍ (®£¢BdÚ„_ú!Œèˆø²(‘zp¯ààO?‹{áy‡rü¯^*zþ…&wßã"°°byô¶Ñl’OÏÊ @ÓïóSÃ!ÂyzÁ¸Œì\bÕ-cÏ…‰’3¨0w|¾êY~vÇ0mO³0”þýÃ3ÔUŒÒØnFq*5mÔ@¡Gn¯=3W%˜6¼'_–¿‚€³VT%ä™'ð®öí¦ƒq)B‰lC _|àÖì¿®;§ C*1²}‹\­VòÃÃ3XEn~~=b `CSjûÅ_$"ùù˜jw/ŠiVTUòãf–ùÜôË_ùtèP­¾i™« ›¿Øh .È‘=•ñhبÕ;ï%üéYëÖ¢ÅI~ýE±1Í~ñ"ùÚsûMHïákµ ˜’Y¼Clí9´Ä,[ù)¸2-U¾9P(àH>üRÜá°+(ÎDJT.”@4cTo6åî×µ}SŸrï’Õ„@Ê;o™ këÕ ¸ÿ€] ¢æ~H¨?KõÆt1ÿÏÞwÀÇU\_oïU«Þe[’{“{¯¸ÆcÓ!´ÐBzã )„„ @€ü©6\pï½ÈU²%Y]²z×JÛûwÞŽü¼V×jWZIo~ëõhÞÔ3oßœwçν¿ù•:º<žâ¾û5sæÐ5 ŽXMŒÀH ‰Ý „gØ;ƒ MûÉÿPªiӗصD G §ùf>!1ºûdr2 2Väý„â› R³‚•Rèy_ÑP*  N§å´¸ GùšJ£ ±š3®áè8:ÄÕh”´ÞÖ!k?èä" ‘@z8‰4~ú ¿ Ÿµ›è˜èü0`—yï@¤ ¢§dȳQ”Õ¼ô^ddݯ³œNÃÑ#P3‚þµ'õÌß¿qü0û·Lë ý‹ÃмÄ¯ÎØX¨oÔW5¬™?‘Q¯î&Žjî¤ÄíÇÓ¶$º¿ÑÍú™lÝD á·INõ÷žìÄÒ`GlÀ^_¯ÿßG8ÏJ"÷{¡œ:ÑÍ„NP®»Vý‹Ÿº°é~íjùãÂ6?&¦“"Ì%¾G€Ù¿ðsì)@5³°oÎ8Úêe-C²àh€ŽèðI úyЦKMçSÑ ^x¸bí:/zÓ5m“ åwÞ-žÔ¢uëE=CªˆtÞ¼Èÿ~Äuu´•–”=öˆùÚµ!…3XÀG€aE^Α›ÙªšU21c—¨G .€è •{T–Éìn ŠžzÆ‹ScÖ‚üæíÛжHôüó>éÒ©D4z Nìóãã1^§¶±â™'±¡6DÆÎ “A`@ À°"o¦ ªP?µÚlͳB Z“Þ ¬¯Ê4@}ë󲯺?°Û1ž;kNOÃø1±òÕwz1˜úþú1(¨zä1^H¨5 å"ü¨¨¨?¹l.‹¥ê'¯“QCfì ƒ£WäÍ\@©BJÛÚj“ËeÞT1´ËHDBNOŸ÷Æéž¡GŸŽ¾áý÷H{ê§ŸÁAüž¶mL=gÔà'dÙŒÑðCÒ7M3­ )VäÍtcEG þ¼)Ï”q{&ºé@b@¿Ä°¹l¤EO= ×=í½ñ|ª5;¥ÇËWßÕÓâL~Op¾/øåWø8±ÿ׿@bd<~L_^îx¸Wâ7ÜüîÄUøX RlX6}|Rl+Çõžˆq­Îx5·äÐù¬·6 ‘»çO$jâX˜>& +òr^1j߸—U±bð¾îClØý9\Šh"Ù²^t¥é³OI)hŽf/8E”÷o„DzŸÿ”e2Y²¯×¿ù†k©—t>:>Û}®¢® |hîääAi.$o^ÊÈÙ“’N^ÎùæÐì>ºz¦¬óÜ3ËLOŒ¶õš,¦« ½B ñÃÿ’òj¯EÖ‚¢PïvßzÕûÁ[Xº`!ÔŒ8Jj?È^]c+¹ÑÓ±âýÂl±|ºëlƒÎøÊÃ+L5() F‡1b¤ ͆Ovž„Œ¾ÄDz‰ÃмÏ £$Þ”fÊP:F‘>@À”vÅ|ùâEGË–{#(ÒÞ)7nôâ<Œqà6!7ÓxánËg=4Wß.À‘Ne}óóë—$D… \zÔsŒã…l cgÆ=‚ŽÉÜ  +êæƒÀàA@ûчd0êG÷B£Ƭu{÷ ¶DâáÇÁ¥F‚ûпæ÷ÐÃ.¡DEåµ—sÊÖ-™:t(™Œ£¾u£´ªž!Fþ¹1‡\­ +rSÎ x"`¹~½eó+4T~§7j+º]ß±l6@§X³–Ëø÷ðÏ=ÄU(4/ý-뱘1[¬‡/愪åÐ%òO¿ºVŒªåûÏe1&azžNçmë3WLO¼E ñãA‘êáG{ºùe0™2rò%ß~+q·¾“+Ô¶ÙÛŽ °rl[*+岨бÉ#¤b±¿Àø‘QÝl†-` ¾AÛt£ªq㲃[—¨#L0ê%ÓFoÚŸÚ¤3ª•2¸4î(çI7šÍF“Ùd¶@Õ W¡!àÀ£€À÷L"‘IÄ PÝ +ê&A` `-.69ŒÁpTjÅ=k»?ªÒʪmfå*ôºgÊËPP¤Ñ«ä,‡¹û• èœÐ~«®Ñæšö5é9ö˜Ã×,]îßAQ.w» Nd±X®VAÇð»Uf0fÂØ¿Üw.«°bú¸a`0±Ã1é Æü%%UøÔÔ×66ëáH©ÃÜ7/¸­bòƒÕê°ÐðͰ˜(|DBáÍëCú† éég?Ð~úd˜ªì¦ÝE8cùj÷þ“ÓÂÜfo¯\2Ÿ êˆY»âùû—Ú†Ô71“sø|Öïßùpî”IW/çñzlíÉ·ˆAPd³Ù!¨jhVË%ƒÌ.Q°ÂØÕ Ie}áñxCA ‚mÓÓ—®œKK»QQ ¬„‹œS%cWk8M"Q³cà³,<¶™ËÆé<ÛmBÆáâ;X|»KhqI¬.©É©0ÔåÕ†\r„8\ÔÍ<"6rúÄ 3&Ž ø=ÂefXÑ ›Pf8 ·!`¯­ÑíÞ…$JKzýúÛ®uðG“N÷î[J+«i³7åÿ~‡ä•-žÛA¡Až|»™œ‹åÕ5Ï?´^.•öã°¡Fc³ÙL&3z¨d{³»ÓÏM«äÒf½ €`rp³"l‡ºxeËÞý«-œ—3I”Â+”pš¼žl¯éœÁuŽ„šŠ¤/KÊ¿;tøÁ»ïš  V'¸^Ë ’'F‹(MAŸG?Y¼MkˆÙuô„’[ÆÍ#.Vp«Åì¦.E/.µÖ©µFæ$]μŽˆš?}jʘQý¾ãÜ}¬VÔ}¬|ŸÓI½šS§…8ó¡ŽN@(êpºð‡€ÏCÊž¢noñ’²¨j¡t)<),V»Ïãr½9šWÛV›}(«,ø~Öû°Ææo¶¸ ÔÛ¤|Å*^hX'-J”_\r.-gžfo,™9¤ pl‹P½“z†È%·™œ)›÷§ÎŸ696Òmz±oGND;µ‰†¹ëÛÆ´5øU ”Çi§›;ÚË›Ý2Mç¯fœ½r¥¸¼¼GÁ­QsnL•K8bv³ˆ£ã±©¥`z6–ÈêÛY%§ŠN÷GdŽø¿.ÊI=ª´IÊn”r£ø™,ÖAˆªìIe“>ÚRþÕ®= fL_6w¦p èÂ3¬¨GóîãÌ5 Íÿø’¢ü÷,˜8cÜ0<ÝþüÉ~‘”‘±ë—NùÏ·'À™~þØrÏV_ÊMÍ,Ë™˜3gâO®óáŽÓ%U $s¸F1sÜ0ÔYRÙðÞÖ­˜>n„7{°rîZá/öàà’go™x?"à²Ùš6o"P=úhç=Á6¶bv= Ó/­ÌÞX²ZX‘hôP4‡Ón@éÈ…ëÛ{éÑåñk:ÜS;œ˜7J4â׆JåÀ€–ï°Åb=t&uÿÉSx_ å奈¯Fð²ylj£ª“¹‘€epMäñÕ%Ç­@ì/¸ŒO³#¤È:mï1ÓñÔÔµËî˜2ÑWýôS= +ò°=¨r¢Šz0˜ÚF=ä=ëtT¸°¼nÏ™Œ1Ã"ƒU2D b9!1Ú3³XÈ_»p’Õî•ÙyòꄤۮzæôŒÛìœÛu==}LüˆèV™;é¡gN&Þ¿èöíuÔQçS$sç †uÒ¼[ã€wm]}~iùÆeÓ=ÍÞ8Mfká ”åÇÇp¸e'xq‰˜É¸Hg0ô½Ú5%Á?/z³ˆÉb­¬ÕÆE{NúÍ‹·ýo0QÖo‚](ÒBþm·;°]ÂñØqéfÙÛÚóæ ¼)íÿ2è[vAÑG[¶êÆ8ÞùdÙ Pÿ7ÛÏ-@ci‚xw¢óT¦ùŽO¶Y¯eç>¾nP¸VÔÏw š Sߨ¬G¤¸²>*D]ÝÐÜQŸjuàC—M/¹˜U|5¯¬+Â>Ùx7OÂ#髃óJjä’5̬¢Ê/ö¦Â¿tRlØ¡ó×^ÌùÕ+%"ÁÖ£W.d øÜ±Ã£ÒrKòðžûe¯ßHÍ(‚œé»é™q‘škyåAJéýK§Ä†uÔO&=húü3Ò ÕCtÞböæjN.žÚ­ÌÞX²óXî—ofû¬-†ÄLNúõÜ9S&µ½êï”–õ‚"¯dEÐÑþàÛ£9ÅUàUxÌžxï’©ôù»ã—_ÊyïWá%êÛÃñL3¼—®ÜUÿübÿóëKŒ¡k£ËÒ)þˆ€2%»Çþ“g·:ªá–,‘néÍ©1 çï:1Þ©’-‘¶Ì+ÙëÞø þÇO>&ñ¿MTï安w-1¥:B ."¨QgıRp#Ä;ʆôécÀZ@‰®UÌÖØ°Ö™±«ž]\¶töZ!ò+d·Lñºœ.;T–ÜP¼Q!Ž µZв³Æfߨ¢2ÜÞ<HN|kõ&­u‹'7éMEÝž‘ù+°0ž;kÍÏCŸ#GЧt¶ÚáamµZ ߪRONŒâ¶‚b20áÈD¡¿±½[XVƒUPol1êˆÛR|“<¸Ù(!µE=7Üu!…®ŽÀ/ÒÉ‹4ÞÓtà°Ø÷´”×ùÞʪª¡×•ôWÁ³éùxŒ|oͼß>½&).ü`jffAY'™31é™{"4wŽ]¼^\QGgÆcré?Iä ÓAâtY:f¹•’8¤VôÕÁÁFçÁSgauÿälñG>§D|QT="ðq‹âgÍ‘|PUSýÉÖï€I`v˜‘õÿ¼(¤b•L A>ËfŽ>ŸYÜyŸÀcvOš66¾UN“Åö¿g‘Ò’i£â#4Eå·^­2ãÏëÅ”ÖÞãwÍ õM†Wò°Ô½±uÉùƒ‹=‹@"¾nq Dã§ÒòáÛó4´ŸJºÔ¥ Ï&·ÙS“ÞЊ¡G£–Ôà ÖxŽñ|fá—{ÏânA"´ûWϸr΄Ҫ†7þo÷âi£ï[: é¹%Uÿø|ÿ= S–Í·ëdÚÑ ×Á{@¬㟸{®RvëÈî¡Ô¬Ç.“úQÛøÄ˜‡WÍÆYkÏI¼sAÅ';O”Õüù¥n™ej[¹)*™DÛ¬†}l#‡¼ÞôF4’WZßr°Z¢zhå¬)£‚Uò7?ݽF𤭇/Bk GeáïÞß±vQJVwòJî¿G>üæg{Ôþ³˜ ©c†}¸íؤMSÇ$l\>“`xîZÁ{ßUZ>kÜÝ &Ÿ½šOÊ‚C¿ýÕ¡)£ãÓrJÀ£VÌwÇÌqÍX:(­n€;n°œ2Ð0}ÜðÉ#ãS¯dU®˜=~Τ$ì¯å—V“ë›ôϬ[¦Þwú*§Ë‚'sÈ™\9+D-ßwæê˜zý£§SFÅ^Î)E",Ü·$….ÅD"ZXntՆؚÄʇ§6T­ÚÝr44’ª¸j Åé´<,]÷- ƒÄ¨PÕoÞÝzüRöÄäØ”Ñ Ïe@®ÁCF~Y|d0"—®c9TË¥¡jED° ·1ôØèÚèȘáQáÁªic‡}´íø…,¬Ëà@l=u:ˆ"NuçüIž‚ŠáÑ¡­®¢*ÜÞF i8ÁxfÝ"t ´ª¾m¶ÏvŸF¯°Bˆ{aÃìJ¾çÌ™ô|¼ŒõäšùbQëWºŸ«IZš@IDATž¼7¸7ƒ˜ a|RÌ_¼ïBfaVA9D;ûZŽ)Ø{ú*6FKªê!=ÂÀx æ ³F:2„âÇoãa•–[2,:Ò Ü<ÓÇ—KaÐ.ÎO ¸ć`ÓeIbÅ”Qñ8 !\©\Ë+™Aä‹èO«ÌúOÀ‚…ÿBzF}³q¾t?Αù|8Q£Ö5”Ÿ«È¦Ì’åžy½,óK›¹ ÄhÚ=›Áf„Ò°Â‹ïæ§¾9iål6\G Vç¥þ½àü?GL%~â“v+,$¹R¿¹7~â÷‚¢¦Ÿøt^Øðåã–üýèÇS¦®ùR<•4×f^Üþ¾êb›Ã›ðxÎé?œßº¡˜qUäl“(ã%ʸøEu%Ç%ÊXuÔŒ‹ß=’¾ï9MôLMÌv¾Öž`rÊT2)¤ÔøÃadE>œëW®QÒ¦€èÈkÏÞE*úáíj=$qåì±5óü} Ú^Jˆ ¦k~â®ÙP€4ˆl„!3Ýѡ깓qTmó 8د¶œY#UÁ>ˆãÀ?>$ñ¥û‘ó€4ûËbAÇäwÞÍ•SbðÎÖ!°"<›Ú]È["æxl×B¹=g¨¬Ž 4ÐiË((#+Dh]&ýîÙ{ÒsK°5v½¨‚¬µ´JÛî}m0­ Ëg¤åÜÀ" ƒâv £Þˆ ¢íU\¢þ↥ˆ4éç3 ±)Ö6$OÈqQblØ]ó'M÷í¡ X¤ëµz| .Co"[—¸µ ]—û7öª°ÿR8fXT†^ó0³?ƒ&Bh„ýPS`¾j΄ۺêžZÈx É7"æÈ…,Jl“}sè ÷/ñ„ñÄä¸=§Ò7í;+àñpîD)¿¥pv[»íÜ…À Xu]œMipú<8l{TÔq ]]fùõ-ØAÃNYPôLX3~é?ÞTÎR{d͵.§Ýj¬åòÄŠ1HŒó@dòZCc(TCÙY³¾*$~1öÔ²Žÿ&(jè쨹¯¢`cÅ0'D|´ŽðK¦û‚å’Ø°`àãùñIý½¯„aE½Çp Õ@ÖºÇXBvœHǯ6“ð<š7)±åõŽÎÁD”åÆ-_“.+7lì²ïÔzîæ7 5n¿ÚmJ¸OW±o·Œý”«yà7•„ô`Ó7ÏØ-ç´SF%½xÝnwb‡ ""ÔuèaWʹà_ÿÚtðìÕì˜@tÞ¦=*ª*¸Q¡Œœ7*ë Ϙ5~ÄîSé­2¿ÿÍѶW¡q‹ç,VwŒ -›óî7‡ZeÃzÿËïÝ a ØÏóŽA-k?rjT2¬øwÌÛ#Ÿán_ ÆVÝ ð?×-™Š;tzvŸLÇ^ØÜII+çR숗³o$D†€BÀÜÐl¤…‚dDxVÀŠÇéô<(Í™”ˆHðK\š9~ÊBŸº§Ÿ?99¿¤útZ>H*^Í#÷!±`E¸Ï.Q½ŠœíÑ£7ÄŒ}ûh² DWÀb³MÍ%P!‚¤G9U>Ù¬¯tOÊ­[Ô¬«Àï¾2w{iÆ— “ž®/;™ªŠŸô4‡Ë¯)<š°‘ë'ç°ê£F¯o®½ÖÓim›ßîâZ§_·,– ø“F$êgx?†µ»!”‚µçç.+®lh6˜Â‚Œ ¢>÷úǵ5…döA\\w†CLî‡Ó­‡&]XntŒÈ@/WóS’]º¾yß9¨†àŒÒÁ³”£´;f´H1qÂhÿÙk ÝÇtR,Aî<‘†í˜SFAPÙd[J„ãÞHÊ.®ÄYn4í¬” ¦ŒüxÇ TE=?=í\…*žÅ†¶À{ÀÛ ­ VÛlØ*Bž§î™¿faÊÿ{o[]£;GÐÇz=¤ïŽ]†® @ב›GŸºÎH9 À{ôÎ9˜Vò€xž\ éékÏÝKw²=Z¼÷†¥Âa2°ÑöÚsk ž$¨™GÆGÀ¦)º‰âtYÏ« »øàjÎÊùSF¾üÐ2ì~¾úÞ¶á1=Ÿ´ßÀ¬;h ‰¸ÂmÅ»'Ûìóžæœ~]( ³ðuÔ¬­J«.ØHÁ…‹±‹ŸJB?VíZŒ5 Ÿòb⌟Ô,q[«ÈÙŠí6l±9–ªüÝ%×>ŸvÏ&§ÃÔM^Õªšîþétqr­ó ,³à®D-$G‡Ãk,4ЍזÛÏôt·FæcX‘?ÑuãµúF¡§L»F iS‹žµò»ÎÝ…{¯uzWNi' )eO¯] 9N/~¬˜ ›7äjL¸2†Z­žH’ƒ~8ÚšQøïÍA†°G³j.µ-Û*ì:‘†ˆ—@ƒî[2 ñ…SGáðÚ+ßDÔº±¡ƒVhAEÛ«(…߬ÂrhAÔAˆZÛlèØÅÌ¢|±/«Q¡ê¥3ÇB.…o;r ;A8ŠußM>ת‡ƒïO¬Iùx1.ìÓRXÖà%Æ‹è"F“õ¿[áž!4¬ÞÓÆ £/ èX~7ÂÆ‡g—äXŒíóùˆ6ý•=Oó„JH‰°;F×é»G rd³PÚ~'?›O"¹gßÀñÒkŸ•^ûœ'TØoæÑ×gïû7u~Â\YÇ~™}ê5¼‹€$ÝLôò“K™k™ëdñÙ,'öÌ´f[¼H$‹ˆ‘—•ú­Êü-S1ƒ@ß"`θfÉ Ä6ü„a’3»Ó8ÜȆoi[¤…Á¦C³ŽfEȽ“?½x´pðЇ¶5­rKjð”1˜¥ÁÊ(*@³­UC´P¡U:Ì áèÔàÀÝŸºyÍSPÑêê쉉$”Š v"Üv+Š1¤J›6›ôàŠ™ë—NCA¹êÑû+y ïÀ›½fþïèuýé…uù¥5¸‹"ƒÁ¹;Ì:Ð.àFB€¢B\°º v–ˆÝœ(Ä^•ïÍl<«¦)‘gâíqW»éTc03Ëëì Ä;%œ!g—˶ñJñÁ:kqJ‹lSO^r•—½ððƒª›™¾¹Ùºl%à„W]ö˜ÉÀ À Ð{Cƒþà¤sd2ùÊÕm3x—ÂQ¶°Ge‡† ˆ—â®`÷ú¦Kz“™Ï%û¦Á®[%âóù"‘Hê ašh™°Þ9â þG©Æ •¶d›«Æßu½9päÞóã«“b¨Æ<ÚñqTÈ1Œ[(y»ººô“o·QG=)´eRߘ¾00tݶoY6Ê\žü®»9¾s»È‹!=°ääKfSº>L4øÎZ¯ú¸?Úf£JäÇ…Ü‹á@VA´gär¹J¥"§Ð¥nƒÁRoM®´ÆÞµŒSŸ$8+h}¾²;Í ÄÁ‹ž¼ì™jÑ×½â™âE.Õ ¦}qÇCu%ÔÿØpš$üæ\ÁùE7’‡ÅÎV#+òߤ353ô8ëÒôÍÒ˜rýý>lU4:™ÔfÉÌöaµLU½D+.jÀ7‚˜Ç†Â>½¬sàÇØÁ åb>Ð °ÂXа"ÈŠ”J¥F£ ÆwZ¡–‡JD|ÊÎ5Ûâô^‰Êfn<½i>°6Tš¹ ‘â´á¬Ã—òÚ!TòE*EÈ8È~`†‘8ñh›B°òtB£ÇቂãHÕÃéßFBy–³ ¤”1p4óY‘og™©A 0?æ¨qÈŸ5›CŸ"ñAOø±Q¹Ì©Ó[²rñØ"«ŽêmSœoÀz ÎÆãÈ·Ãáj¥ñÓ&;“p …\Àu5·d^ÊÈ[©C)†±ƒ …Ê{»!åsÌ ÿ€R6ÐÈo®-Ì6ÇõÊZ­Ñ¦àÔŽˆäerÙ^îÁü*,7ºûì²j‡UƱ‹þŒsdßÉbC6V ®?Y‘§™©ŽA ï õ¬÷oðyëÂ1”¸ÄÈVRæóÊé _ÿxç»_ÆŸouø¯Ÿî¡Ó™HG€¡’ÀçrÄ\ÜÏÁTG™q:F±+„8×#€Î`Ñ".’ÉdjµºJ«;™‘c5×NoZ${;†ÕkJÔý1ÂãÕ/úb)¨( ¶Jiㄲ¼Š·úâÙߦïA_ŸC}û]i£x|°RP®?VäÛYfjcèk¬…æKÑ*/:Æ}Þ¼pL‹Â|…:ößQhl6À3kGWét£ÙBÇ|R"’¯ªk‚E¼Rߨ¨C—î™Ó3y’ÛŠà­4dn7»‰¤˜Enµ×ß1²üÃe¿›ÃVq­°·kÝýݯ~h£ÆØ£E#ˆ@ v}ézîs—¢y—IÿÉ¿î_¤nוÖV]±ëÐ"—ßbbÛ3%(zq3—r"h9`Q™·ŸÑ þ8yÕGpëóCÙ<Ó²<:X-  pÍì ùa¦B!Š@Ó–›E÷Þ‡%Áç(ˆ§NÔ~D‡Ô8ªX³¢mýg¯æÃ ã¬>ܧ/›5îµ¶ÃҫϬƒµß½¿ Vþ`dhç‰+gÒò ÿ‡!?xàœÒÇ+>,7‚KM% ¤†‹Yxׂ5¿Ÿüs³Þ`ž=)é¡•³<[Äa+8·Ï*¬€FÑ~쮹:ƒö$áwcŸ9aòÃRû«ÿÙ6kÂ8\ƒÑäÇÓ–Í{çüIðüõÏ÷oX>}Bbl'EÖ.žBœÚz¶€qûær°à‚ p%<–ÄbÚrð|\D0` ÀÞú©Kpë‹Q+xv™P䯂 XüÔ–×Õb½¯¨©Ýuôä0ÁÙñ¢½^×ÓeAbÅQ1E‘r{fbTË3íVJk÷ Ê9K°ftäÉç¾;þ±œÓž¹Ôûo‡‹›j|ÀÁ’ŒŽ>$ô¾ZŸÔp7OFÕg•ø~ ê³®÷wC t>™§É¤ß³ U±œ>óI­*Å‹ŽD¢åj–­¼²ÕÕòšÆOwžš”÷â†%áÁÊÇ/CûÔ0‡ˆœg®æÁ õÝ S®d߀¿­‡VÍzùÁex/AÁzöíá‹’c^=.`‘""xA¤^«[>sœX$8w5¿Us›÷§ÂíÚoŸ¾D§¨¢¦¨Ñ"¸Ô –ÀÉÜ”–V5 ¨&Ï,„MíÉÉqˆÃ¡ êÙyüJh.Õ:/’ÞªÑü"Š ñ(!v–¸\ÛÄuÚÞÞ|À`‡ýÑ%Œãå¹ìa"'@Ä-1  u æ­¡4sær:Ÿm+¤lgø/4Vœ74ÎX·U3¯û­À%ˆ®î:Ξÿ𩹔vo²šØy ¿w9zôÆ¢KïúÁ!Zªqcƒ#nTT˜B&u3ûrýÁh[wÿ湕“¼‘ã›Ïç´Ú¸•‰‰uŒ@t4Œgd®t€~ï§^LÒ%K¹*U¹½½,[¾Hûáç(­ß{XýäCžÕ€ÙÀ–ã+gb:¯å—²`; >õ² Ê!Ú{ê*ä=1aA™”NÒ';Oùô½ £Ã‚àùB£Çïš‹Û`ï©t³Í.iXâöØjh[Ãû³Í㛟îÃÒ‚â¯<¼¢º¾ Î×ÞÛr>@~öØÊ&Lnh¯æ•Áb òÌ–b7-øåwOJ™îÝá¡îÕ £Bñ.‹x00ã k Ì„‹D‘PH¨‘¢m2»Þüt<ïΜ ~˜ï}¯ K„3H‰@ƒ¹F¡@,äÁÒûV|Rq«mn†akŽ·ŠÕ÷äÀ»-&ÝaÌúäç <} `eî|»œVÚ›GÛ”VîA蜧¿\Šóÿ8ìæCJ¤w3=htj4ÊpZ"‘à˜n`ý·ÙgWVÔ+¨Å|^U3õ,fBhÒ›Ãw`¤GCÌMß|Mz¢¼ï~ÿué+ÚwDõ½éUª9p:6}ìpP"´~£²nÅT ihú#ç³@GîšO^:},¼‡žÏ(Ä'õZÁ±r£*Tb0[G¸}‚BÌFE –ÕÂ=öàÈ%¬v M 9²¥Ý§Ò‹+ëÀ‡ {OŽ G¸8…óÑØpÍî“i`]´s.D°»‡­ºñ‰1£"÷œJﲈÿôaÍŠÜF%±X"‚ov([)l ޵C©™p7>)Ö {â>ì¤Ï«Â!|œ8;p.NmEN£Šm’%>@Àâ @‹ÏõºBÜiŒbj4"ûUGh“#LÉmÙŸòºÎ. vÃÑG;utTÊj¢Ä·> 8ÃZhžeYÂaó†É#‚ƒîbPÑVäåtãiŽ ” s*´Íz“B&ö²¢¡W pAq$9BA0zølÄæ«W­9”‚­ )Y4žrNî§ÀMg¾rÍ^Uc<*3ƒ4=_85»QYIÌõ¢ ìh¼¸a)a${N§Ã ¬B*nhÒÿù»ç¥$Ý¢¯¤f–Ç7hTQY ü¥Ÿº’ ÖÙRiu*LÏ-ÁÆê|jíµBöÈêò£®Ãíë³ë.™>æµÿn—‰E¸…Ð,ü A CÉñb!¿¤ªaÚØa4aAÊ Y…v»ó·Ï¬AbwŠÐe9šˆ7l±X$“I壻‰6´×eÐiln´nÚwîË}çTr±Z.%íy8]ö *e:ƒVg”ó]V•]'亰šRf¥R€(¨M4n Ør4j‹j‹—庑s¬¼ªž'{ú¬á¡¹²á¬ËÁ¾ 5öáæåÍÎPŸ¥”©UAî^„ÄI=FV4°',çX¢5ò yÕYE•3ÆÝz ì±ù¿÷€ Ï5@I𛃳AÑ}þ¡bí*°"´ÒøÁg’YÓˆN+´8 ‚ªõ›Ÿî…WÄá5yÂ4”uð¡ESG!/W .Öàƒ]³GWÏ VËçLJ:y%÷­/@µyà´´ª:ÔâBÞûæ(<ÖÎO9yd.ÑB#«Ýþ—Oö Ýá1¡÷-Š”Âò¬ýx)Ÿ3kB"¶ð |"§ØHAèY¬v)H¡-ì.‹Ð-r«”i "‘Ëe&“Ùl±Øì”)<«•År8x¶X‚±q„æF}¹¶ÞÅ Šw¨ÂÝ:,þÉ\v¾Óß`?Ø/ƒˆH.—*0-¤WèåÔ—%ss‰Ãé²XYf#6°X²ÊO›ÂŸ:¢v¬è`<ÿ¢ÎEx‡«KA«ºÒ>*×2¯Ù.ä8c¤pt0!0"ʦe%.‚/p¸,à`dE^Þøía"ñ3 ’òO\É›:&~ïâ{‰Q{ŰŒ.€è`ÿ>ÂÚëà€Iã™Í†CÑ]6^”W¬òw¿% f ’†[s l…7 Ë–-$-‚mÀS=ö5àW*A$nçßûÕcž]úù«!1ðx´TõÁ•³VΙ  „‚[.ZI)H=@—±âYâ1áš¿üà~Ô£QÉh±Ï¯_é#n%âß­š¾cæ8|èªà8¶Ë"tæà [(Êd«U ýpJt˜Í6Ù@ ¶ËÅqš=7ªÁ™|Dm»‡;Á3‘z ädnJ¤R*!uP©”PÏÌ}‡Ä†’c„ µžR]§œP1FÂM4²9ÕVqºù®"ë´‚ÓÑü ?iyö¡_âЧ®wÄ•ÛÆVØÆØYBÇ!檥2âÿ¦›@‰`õ;ÐE€«î¡~™$ß6J(~¾ 1Lv®°15£hÖx™E÷mçû·6Ußd˜1L è CŒ¼žŽ¨ë. eûG¾b¥ŸuÔ,Fê§©þñ«ÈÐøáçÒÅsaŸŽd øtT¤C,ØÖ=ô„Ú-ÕImØ_ n­TN3­vkk7Ñ‹"íÖÓ¿‰x¹å%h_¹ Ž€>‡±ŒF#vÓ`$Üå&J„ DJDà%ĈâCà”z.ô‡ Œ)(‘F£V*ånÝ”¾ÓØå8š+³·&dhhÇL›ÃWENŠY ›Í–Œ¬ohª¬¬¬©©kµõÍúFKèeóÚ ËŠh~z8/'˜[ÌaSÓ×û`u‰ëí±¡¼|¿Ú‡l°G›]r['âè Éíbq.žÉ¥48ƒð©·Ç59# žä±RW!àIq&@Ý/ $Cà°`EøFœh” SÀ°"oîCüálŸ<¸ø,'È©šÏ‘ð8¡ó(Üž€!>cßH (=0†ÑPt¹MˆKåÄ/oiÐï“J%##ä—Kšþ÷ÝéÇïšÍ£Žp%D"®kd„ :rŒö¶ümp¾í*óÇMÔeòêlˆpÌXaå‘£oBгV>÷S´ÕðÁ§â9Ó¡…Ý7í2­t„¸±ˆÒ"§^ÕÈCI/1M”¦¼Ë9±k=°‰Eˆ@‰8ø@Áœ¢}P'¢tÌA‡d7H@‰òt„’×é8™ÕPz‚È„t5é¤ÏMI¯à‡5(v!˜PPô\xÕhÕE[E"°b]½pôz½Ðd’ÁÏ…Õj²; 6a½côoÜê_N1[/áÔË8 N£€mâ±Í|T© /û¢pfvÊ!­1¹:G((¤5ÙˆØN%>ñ\%¶IV—dœh¯o5»›¡EÖ)ŶiB¶K#¤ŽL8\ø¦NN€°BÙKà¶…1b:  ±c ”š_$9Ã%dL †µº‡oû“üÌð Ô›¨Ý :›k»\&U*äIÁæìZó{ß_=wüô± ŒŽ "x*cã R"Ç™"\ Ðá‡éœ@8Ó˜ÓéL¤-1}.("}-½cáÀ1—ÑT÷‡¿‡ÿëODíºm™”>C?,0b,KØP£ÎêCÓH É„fÂm6œ '´ý9ê³^ùª!ú€Ñáqée¢‰ZkEP¯†‚¼\‘l>iÔnÕ7–"L¨©ú2¥)}{ˆV^\òÄ{(&3ön¿~Û_XøÑgP7¤¢ÿè0X‚N§12™L‹…(…a– I²8œfÇæT]r=Öîb9)5ùvØtϹøP\„«â³D\ŽˆGöD+˜e¡ÕQkI<¨OáFð³C¸2N¤géuÎ`‡15öD£KÍc9‚¼ ؤ¹Éiè BD |ˆ$”¬ˆü `ñá”݆x¯ÿ`XQ· „¢Cu£Ö3«û^çá-R\£ûHBQqaYÓÌ‹åÎOI18t P»!˜«›¼‚QÁ]ˆR‡GoI?Q*0ʺ˜÷¤Ã.‘$»¤m¥x°""4ÂnZG܈´’„!#Nîɇ@¶P3êÇUT‚:Q³PhV£N»Ýd‡ŠosFÔ:Âv–½ù“»—[…1^‡9,‡ )”€G´~CíYº R#} ]"A ²‘&D"ä¶D2–@þ¾µ,r/û«oôÍ|>;6U祴øG—0Á¸!Å­ûvwûž È/y.±FÉM2³,:+Ëb²Öu5¤÷ú™Ô£ p¿Îà×Ée»Bø.¹€%ð±­Œ}3P¢`Z¥¤ôŠç/Øâ÷œ‘H`ïQ›C*³nÛ·d¼Šµ÷öËÀÅÓ&É×®ÒmÝͲÙk~ûFä{ordíŸ&ë—î åFÉo§ª¾yßÙ̼’ˆ‚R¼ËÁÄá@‡¦>ËëšÏ^Ãq­kIqaËfŒ†éóžÊé°5U_©/;Ù¸[h+¸ôM;LH¢æ¦A”XH$‹èiåÇ“ ļÄÄz6yBŒ7‚È–Ñr>L1© ßnr‹…àOdC%Ba‡ŽÈ¢ð§Ô-‚BmDø„oJE‰ˆnußšIî¡›­ƒÖ€ß€ÏA1߈#}@vÔ‚oä$ôŠ|“:Û­ö8ư¢.&‡L6´ù42éÁÔL8u"{·¤ç^û›óÌy%JnlÅ‚Y®¥ ôÔ €INqtÊ‚ ]43¸.“Ÿ~/øaPÄ–Öd2lœQ–EÜZEP}À¯…4~úÀ“ƒ$ägF_e"ž˜3®Y (›(Ú°ðá}¨gíÙăž{Ü|!ÍVZn+.­þÍëáýlÈ´ÊÃüÙ÷à¡ó݉«ç3‹a^|òéƒÕãÇ¡óYom>2mLüÝó'Â|h8»íõŠšcÝemÕE§òÑÔŠ$бà@ä#VÄtQa/.ƒ€^¸ŒÐ¬à@$8¾I 8‘;ÐÄÍÞ|´¢š‚EÈ ªXn@ƒ°a“¬Ò/sK¡ZÕ†œ­ÆA*Ç7U»GýDÞC˜Q¥ƒM“"­:FÊ⛾ڪ¡ÿ“aEMæ[ w@bdèÙœ"¸$\0…2׋ïU?{Í–_$Âôs8ÁÏ<š?«¹zt¸!¥LÍâFjĈü pƒé H†°eFÙŸ¥Þ1à °åçD`Ä7P…gõ™É À¥€9§¯2Oš·o#–Ž™ây¡oã‘(ìßV<óŠS§=ª{óŸ¿Ô·]`Zk@³ÁôÙîsuMàCƒÕ;,ä^Øãí U‡TìÑÕ3eh)Ø-Mú]é•ço;+C€³qÔ1IËC⃠IT ­Ñôçßx¾!àA*ƒ‡x¡,ô7IÁ7BË‹µ›Ä§+¾I xZ"Bž™øF:z RqÙ¤#zK„áš@…tµd”¤ ª"5£*Ü\‹ê! F­4„HAºüé™âOýX7Ê:Œ›ƒÜ!je¨\²åà…¸ˆà„¨sfNõÏïlÔ"ì4‡ÿî§¼‰ãpÿÁE!¢BgµY)]G÷]ˆfÚróÎÚ˜×ÈO‚àFæã´Î\à´v£ñ›j9O²‘!ÂyP¶@˜@ Ì=3 L$üÒk§É¤ß¿U;x¼ÊäÑ~i£Û•ZC‚Kž¶`û=_`ªÐóÿþ?ÊgÛ Ýî…2²YTÂìud¨jìðhb&ÛGU÷E5x¶@½úÓ]gõ¦W^T_´Úm€ àíâw¾>ôÉÎ3O¯KÛ:÷ìTUÎWJëyÏx€Šž©‰žµ7edG®\± ¿g†>ŽrƒFiêCø ÍZ!WÉ7ý€E„&1t„t+¤;xØ‚ a{Žð!ˆ£hê$®µ‘ IZ8‘û•P7Bèod@NÒÜàûîV„ͦ›:õ)¦w† Þ#£Â.–¾½ùà ± ö>rY©=i^DXØ_^$Äâ>ƒ\ ë:¶\q+’{7(áúÈ9¤X‘Q¿(ÊŠ%‚m÷À(ð„6°ÂÀ9y ßýÃÍìñ—£úƒû]F#Ú¨Liö›?–í8v9«°¯±êqäR°^ª3¸çý;~Ö^©3æWBåïË£‡EÞ½`2Œø³AŸÕ ¼ÇÓ+뛇%¢ùƒó–¿}¶c¿wÑd¬ëô%ÑÄ--Nû¸”¡)!±s‡ÍW‡µ¼KØ2±¨ Ü´„ê¿çÓ,m2Ò¶ã%ƒÁS•€Á`1•ȇÈw rÿGWN×Fú@ˆ¾Q ùF„Hé¨Ý@²×ýèV åÚ†á7wn/ˆ ±f+•Š‘Þé³®m— òÂq£Âþôk®š²WÛ9qG‚@:B¤D¸ýpó‘û¯×“5`* ]îßõëB ‰ôÀ!‡”HÈeŠ ¶@˜hð!3-À#0•¤î³Nê¶·Ø«-;¡Ïõlg½¿ÚîTZ¬“ß=Âà3?AlIœJËÿãG;çLLܰ|VO-Žg <¼]\Î)ÃÆÙ —µÂã]·dêæý©ÓFÇŶZ°ÅÊ„<ÕßµMú '‡†hdòÖ&[Õh’á´Tw:‰"î‡.¥f„@ȉ¸Z¾ZU…R$êƒ8!é­òÖ?û‡©ò¼â’)n H/ ã¤PaS=u­…åÇ>ö¨„ǧBз¸}ó‘aâÏ1ÞÞw JmN8„gÏeÖjuJ7>Xä¶Û„3Ðæ½ïFßÔ mÖ%ÆÇöM[Ö¢"szÚâÇÆ6FùQ!´£áè æw·† |h°š*…±ã†ÁÙ3ìŽî>u­¼VûÜ}‹! ë“~O‡4Úl±¾˜ª–C—¨ßûÓ÷À¨\¸¾ÿ\Öãw΃·u`©gzI¢1 +Q»ëyt“o:ÿŒ´¹‡úƒÈÐzmVУ>i°Wà—†=Z¬Ù®/?ç?JêÊNu(iœñÐÅ/]„ Z.‘I÷ÑÙ«ñû®0ì¦`‡¬¼ ^r⢠… žƒƒƒá?çvžk¾ë€ok ŒÛ8*´t8šw´èYË×ÜãÛt§6l ¿ûõ!¿yfí¼AïÖ;pö φÿ·ë,FýòCËá•¶;(õqÈpº¨AÛt£ªqã²èvw šÃ¨—L½ij“ΨVÊÐ;U¿ ÇPŸ.aú¸äØLÏÎ?­Ñt NKṵ̈5æü迼-þ+æÌ«ž01A¯Ç[Z³Åf1›jŒÆ*·0h¨H„º ^K>">â°Y° *â*„|ÂÇ–,v€Áeà€áÆm<6yDÁð&»ËfÓïÚI•ärå«ïbí9èM-Þ–Á›åæýçJ«†%¢Aù{lõÌ÷·žÀØ^5»oÞ¡:½ªšLw£“öΠN{½° ’hÂï$ç྄±¹ïݦaóà,3:#Ð?¬ ÆŒ~èL꜔IÐèò÷ {Y¿ËáhøÃk&²&ÁÎÆ]Ó¦‡45A°£r«•(²¡•vÅ’½l}Ð'‹ v»! “ *D DpxqâÀsiAo 70nã¾±g8yÂÑH©âIæÎãiúT wuqyÍ™ô|lœ z)Q«ŸÆ»jÎ8(óΘêWb„çŒnïžšÿ}ìœ<³U7Úý‚"œ+‚f[UC3dÕžf÷ÛÍ?ˆ)i½BRYß@ðxaÄEƒx®û`hýÊ0°5KüþO\¸´pÆÔ>§×Mà½ú׿4r¿š³ÙòW~âX°«ÓA· 9ì‡â] ‡Ýg͆¢bu7År‚€§x9AvÍ#"dï yºYa¿gí[]×ðôý}äL·£EÏZÑçÛg¸·w¿¬’A—¨ßaïû`Ô§Ó €À ÷/ñßö®éòåŠ?¿Þ˜›MN¶vg˜˜T[ZûåçÿýÀe2ù±qš;ïâ7µüÙ ²xÔà™Ukp"<{:É9D.AÈŒG0,g½b ‘ @ÃìOVÛ>Ï=xß»_lyó£Oï[¾dÞÔ”ÀÑ1rZ,•/ÿ€P"¶Hþ·$Ó¦“y%¬k9~~và+~‡$  ¹ïã®â9E 1"¤ôqO¼nË6Î(gdnÝvÍéz]y'u7ÕüåwÝÕI6Ÿ_ч¢Fyu£J&Æ‘uŸ×?P*ÄØpð¡ÚŠáô©ú7ÿb+)! p¤2Í£IfÎ*«¨`]LïJ8ÚDÓ§ËüC!^Kõzêtâñ2†ÌŒÑô'+ÂxäRéžxè«Ý6íÚäÜ…%³¦O™ÔïÇõa­úg?1_¼€rd²ˆ¿#›Ñ<²–ƒa¿I‡ZL̪ÝÛˆ‘tÝÀ"Cè9áãÄÔ«¡K„3H‰úŒÁ¬­¨}Ž+HÖ.¼~JtK#¬Za(S"‚- ÷^mÅVVV÷·¿Oo™5h+Þu·âÙçŒ<ž¹¦¦ûS Û|nB-÷Ý/8XsâÙ @Ë` 3®¾A ŸY‰Õå¡»WΟ6yûÁc›víû⻽•Äö¯û‚Ö­¸\öíŒÊÉB:¼M]¹¦1ý: & IpƇØ%Â!|œ8ƒzußèÑ`ßÝy7Ø77+²ÌÖ~û1öÍ8»ÑŠD$0˜à{œÚ ÁÎo7J´“nì´ÿ÷qã'ÿDzÙÈe0ÝàŸýB4z D>,®2$‘—1J:‚äzÉ`D-o©Coì̈}†@ÿ³"2¬4/>²Á`2eää—×ÔbÂjÔ/¾ÒÆ=@(‘“ù´zmcd´ÏÀf*hÀÇzÀz5L5Â.QßÂ÷Éeµw°lè±-[æyÉßqêÍÛ­Ï˨­¨i4€ -ûìþ,è¬ÿÇßìUU¤)^|I~çÝ^TEjhYÿ±}æ‹4Ôf4[IÍ슅¯;†J› |· Û‚LÈ(˜oïVDzUgúÄqÞÄ'¥êßù·öêª*'âõ76.Yê“j™J¼CÀpì¨Ó-B„\á]%Þ•¢4WZtæ˜- B¸“"–Ézª¶b-ȯûë¦ ÔŽ<¸\åúûÕÏ|ŸÛ½³f¤¿¿aqþÿÞB·N3}ÜðVÌ„/P:±óHaYÍùÌ¢•³Çc·ñ}—ñôÚa®2 ÅŠú H¶µDúòëßÊJÔ¿óÁ´Îb5·ƒÀ¡BßãAˆÞ¿û¾él8€&“î÷ †ª?x¿é«M¬›§îÅS§ÿäg‚á#º_I»9ɤø\42>1d*Ë'/瞺’;*!2eT<:€q7LJ¹¤’TY×tìâõ)É`E®žÓJP1ž^S°Óó·"¡—{‘íb¢¤fÔ½J¾ÛÏä2t…ÊZjÞúmÿþIþÐüèÅÝkº‚Ž¹Î à_`ÉÆ”zmpCBÅ3fø·±6µ“—(j´¹8H÷ÕV a ÿ~ËÑÐ@Àâ…‡k~ø#ÙÒ>²æàÝ …i”„ñ¹ÜüÒj²°çTúî“éàƒ 1­œ5etÂÕÜÒ¶{nýâÑÃ"wLÛwúÚ‹–l9x™ÿúéÞ_=y×—{Ï&ƆáýWRÓrJFD‡^º^¬–?~×\$"å“§Lë‚)£À½îY”²xÚhï:Üe)Ëeî®´«Ëʘ C†QÓ¬?°¿öO ®~êiÕƒ‰ÉgØÜ2S´z5ì‚÷}g 1Â+8(Ü¢`Ò%æ¬Ìº7^·dd´ääóÕ<¦zü Ž8Э`l׉4ÈŠ®ä”Àšù„¤Ø:­î»ãW@•fM±ëdú¦}ç&Œsb3‘2 D‰ÍÀ R,š6üéÁ3R”Òín¡šÝŽë>¼z6¨ÒñKÙ`E[\ÄÙ… Ëf\É.Fىߺß3Ã¥zíW|§+XR[\¾‘Ë/—y^eâ í"À°"–éÒÅêßü RW¤Ü°1èÙçÚEŠIdècúñôYtÐ4_uÐMÔmßFž'üÖÿø§üèqh£ª¾É`²áôZoš˜«”‰¥fbkOÑ(e8÷þ·G³‹*ÛΗÏC¤G†¨`ŽÌ3´¶^= ‰GÎg•×6V74×44?¸ræÜIÉñ‘Áþß.Ï̾W›-.JPÄ>߬?%+!ÿÆ#âð˜…Ï·HªÚ†úÍa+/¯úÉ+,»³*_}§æÇ?TÓË fÀ"`Ḭ̂£ûÂqãqq}?"¡ÄE}ßv@¶hLÚv:GM[¿ix÷]§¾å€=?6Ïéì9m3lʬ ‰÷.ž‚î}¶ûôé´<¨ 9Üÿ‰:qùÙ‡MI.!%·ÅÚbe £Añ¸“ ¢Î{(fùOJD:³"2tZ^ÇiáPä¨Hgøã•Ì^ËYí`9¥¬ÛØ[GýgÒ‡Cš9 †ª—_r65aÖÅS§…üæÕÞFj·3^¿"@mŸ¹Ⱥ_b*ï=¶«éUÿyÇ–ŸOªb‹Åê'ŸÂF<Û[ËF½ïR/kˆ Ó°Xy8˜6yTüŽc—±­6{bâáó™P-‚.„=¨?5£Ðjw\Ì*òl«¦QÔÙaÉð`UXbÛ‘KжNËm1ííYƒãxžOtrbôÑ)Ã5é¯7ëQ¹Ánÿ"¿˜ÅfEqÊKÆ%t¢BîÃÎ0U úAY!@ Á+Kõ¯i-(@xÑ1aoü•͈Udn†|7`]]¿?àú¹C|®„zýô#ûõ¯¼LS"Ù²å±Ûv¨{bàR"Ì©\JÑ­¬Ó‚Á,1ötzÞ_>ÙSPV³nÉT~Â5ʱãÀ‡>ß}&:,ˆÜP QËßýúpC³¡ó»â¾¥ÓDþîSé¡jŠ?ùû]‹Ü<µò½©ã?›?ýθ(>QÑc³Ê9ΗN\±mÓÿe¦7[-÷™¹:tº²¢†wß&F÷ÙRiÄ?ßâ*•CgÖ™‘8Æ3gœÚFtR:wsgædÁ·kÓæíó?û‚çÞG'‰IÁ?ý¹xòäÀìpç½R+¤ïýê1:Ô«Snþ‰=µUs&4ê п&Žn =ý†¥Ð@ (§†¤¼Öÿþ¹{q² ŒçÍ—7’ÄW·Äñ%õ„v5ô–ž¼gþ0÷Á4ð­È5Éàïï1jåŸBƒSÂ"9vqL<¯?N}úc¼L=E`(²"è±Ö¾öÿRš—~(3·§¨1ùü‡Lÿ)&G¡€¬È ù¯æ:­þó½©ÐÕ%M$Æ„>°|šXÈkÓa(èþòñHIzãÓ8åôƒ ‹ ÊjwOÇé$¤ƒB­ž3nTBݽ“WòöŸËzqýÂè0J*ÞÛzâ¡ÓLj¢›è~ÄVY]õ§·ìç.’"Naò˜Éüƒ<:z°R¢îƒÓÍœß[3¯´ºµ\³FžÖ»YCï³qXì÷ÅÅ+‹õº/³3vå™Ü2¿‹Õ•ø„I¤÷'^—8JðÆzSC+†œ^,ãUýèeØU²U«U<Ú æOþEÀpè`ËýyDz¨›)Ñæjuó&'>{ï¼)£âòJk@z >2>1ºIoÂño \VÓˆ-˜‰‰Ô‘õƒ©Y÷|Ý|È“Pœœoêr"ÌV*i•­í©(d#ÇÇZåìÙŸNgÓ×;*žý±él‹ïá˜äÓ÷o¼4w‡Ùï ”Ø€‹‹ž31iÌðè~¡DžMT½:sÞÑuýtÊÌØ›Nuª†¥]Xüíç??uäj]g~&>èZ²"§ÅRõÊuµ˜Wá¸q¡¿þí Ÿ`f€[§ÏV­pG‡AwJ«'&ŬšMù4Lˆ ÆFIZnéšÇ'F¸’—S\Ý“ìâj\O–ÖʆøHZ!Á¦Ì÷îž Quü›ÓÙÁi¸2ýrßyð-T¬|xå RZX^FâÓ²£Q9š†Ô Vx ©š5~ø3¼·¡lÍ/Ò>¬Á™ l©UAßL¶bqóׇY&úÐÇh(ãÇaëæ>î@ 5§7™ùÂíµ|÷¨‡rðÑÑã5îdyéÙ§*JQÜætî,ÌÃgœ&ÛjË㇠éi°ô™‡–¬¨þŸ·df`R¹¡¡áoþ~Èý3XØ*ÊÍW.£Ï¼èhÑø «ó¤·P'Bdd|Ýùä8*éQLX¨Oö J“#çFU¬ûOhMJŽ¡ùýG»ßþúhVaE|„j?tñv#²ŠóKkîš7áþ¥S›G/f#Ûþ³™Pæ½oIŠL"Ü}êâ'Ó¨ÓòYM{ôRN£ÎØnmÝIÄÁ@*—£XwgÌæä+—x±úv§¡žæásX½WO› ÌüÚf£@ô.`BçEǾ¿dåž55NÆoY ®Õ×þâôQˆŽÞºr¾Ê@ïg F`ÉŠ 'Ž7ý5—|~Äßßââye†6@ÐíÙMz._9 Eè<±þbwPî H q¢+=~DôÉ´<oʪWÏO2¬_:eÚØ„«yey%5‡Îg§fÿâÑåkC§ç•E…ªgOŽ@³2 *–N}£²ubÏntB½¡Ê½fþÄÌŠËÙ%¹7ª±+þÔ…mᘑ?|V36™†ˆ°„“ Ⳬ›Œ°-¤’KZbÿaìà…±j¤÷£S(>uÖK§î,Ìý";³ ‰:Ú`6píÊGiq.îXV;šæ½o—©!è-¹„1t§öÚÚšß½Jrj~ð²pÔ¨î”bò0ô1ú[¬h ž> s"˹Am‘[R .ª¦—MHŠ;ÙsúâãÝúÎP :tþ:!øyå¡¥øÖÌDžÔR¾½ÿŸ>’µ!ƒÙ ÝgšK¹HÖg{Î}w"›k)£âÚ«¦iüø˜_þPçŽvšé^V²üHÚÔbêlÞU?[Dì^¿ú'ÆÂUâ+b„‘Høüû“Ç|w÷úïX½$¶ÅØ#Ü¿²ìß± k¾Ûòun–ÉÞ…iïþA„iµ Yž˜5¯þÆ©ÕR7ú¬Ùªô1¦(ƒ€¿0g\³Ý¸Ú…ã'ðcbüÕŒŸë…‡Î2 *¾:x2›‹×oT7èfFôj£BTà(hÑçž@IDATí$Dj215Xÿä•üK×o,š:Çò¯å—#Q)¥.y†Ë9%Øe#)“FÆ€QL½!ƒÁàrdˆVΤ€–»”[RÕðëï­,¯ÕB± E›öi', ÞŽ$°$1pÌåp`3Hµ<—1{RqÊáÍðl¨ aìJ1ªcÄ~”§‡GáSiЕ“¹%/[k¡´¸ò´ ¿;wòï—R×&ŽÜ<:VÎX¼°÷Ðí²¢¦Ï?3¥žÃÀ¹AA!¿ûýí01 ºÝ7·ÏVÔí3åú%)â‚AY éœ]\Íë³ÆÒ(OpŸ;#ßH„Ÿ¬GVÍ€~ôöciïn9n±ÙïY0±í!üÓé»O_#èSOªñúd×Y,‡ R’PÕÒ飰7÷þÖ“8êÏÂI0I„-6tàÿ}° ­ ®ÒÝÐãâò¸|*ð‚…ŽZ­þäåœ=(ï:Qcìqj€ €Á»ª:/!•ýpòtœV[ȇÜô¡¦³Y?ɺºrÛæïÞ{ª¼ÔG;ïsÕÿ ~Y‘%;»þí$C÷{^P‹}zÿcË´À Ð`+Y`U€òò±¬%/+„@OÝ3[c /0š ÖâÙÇe3Çàã™2<:«Ín¶ØˆÉó*dHøx¦ø ë¢~(-)$Ø7A"TªÇ„Š ÄQÄæò’i£ (%‚¤êÁ›U¼ñâÚ›Ñú?ÆKI‰|‘H  å"žÔdÙrðÙ õ_d_Û]”oqP¶=S«*ðBÔ±ï1R-j}† ój™«ý‚@`±"ƒÉ”‘“_^SÛ¤ÓŒ& Ûµ^¾Ù4M‰«³ì>už…z‚›Å–JÄJ¹,*4dlò¼ö¤tò‚¹ÌÔÁéÂÅ }ïpC;+Ç …2™ÄjUÚív·~ ‹c4­Öºiß¹/÷SÉÅj¹T& t¨`¿»QgÐêLÐ ²lá<‹J*T)•Aj•Ji‘PþæÈ Íïg-øqÊÌ­ùÙ›r2Ëõ”ES^ûÇåóï¤]Z™0üÁ‘cGk†âWÿN‡w­÷ó=Dwº´²jûÁc™ùøakTJ•BâO_õ"’|ò˜ÐdBÁê„¥ã'yQS„A¼¼¦¾!·èÆ>mL¡Œ1|ÍÒ1-rrâã±}¶Â‡ÕöoU0¢íiÒتîÒ\u'½…#3hL{j u’yè\‚\:S‰V¼].jË w)ŸÇçb›Éj7˜Ö(êOôvaL<>EMZœV2ºM>¦×Vãó— âõI£×' ‘PsçE0™-W³sÓsò® -®lœ|®…Ç5qX°ajâ ­´Z8=×éV³„Nßî`ß4@¯ R%ÇÅŽ™8>)»^tf0éçñƒá¾ûÅ–•W/›75Å'ÇநöO “ôìs<† ¦¶ŸÆ¦>ZÊœ”I'þ?{çÅuî}umSYõ‚„„è½w ˜Žq7Üã8¹I|zË—ä~7Ï-ß½¹iOšsÄ).Ü1˜ÞAô.@HõÞ¶«}¿Ù#†eUPÙÕήv,gΜúžÙ9ÿyë‰SïmßU\^ñ§×ò^vÉp »v 3IÝŠ•îðAç’Aö¹Ü-NžŠ7ÅìK7_ûðÀ?>»_‹´Æ>" _®¢eŸ5-2*rº+÷Žõ¾£AY§ÅÌV›:ü®‡„’¥#ø(r;>“+€„`¬“¬ÒÂ$ýk­Ac‚]€¦Qb5”}¡“txé¬%@$ ’üV²gûP-—tÌC:˜D D Œ2'˜ýÃYó¿=uæ–×6æ\Êoü WYÌ¿;Š"K‡fÂ:’5µ{2ƒÉ´uÏÁ§Î475›U¡5êCD„1<ÄÜÖ"„´´ªl-Zks™ÕRzùò‘3çAœKçÎ\uß“Ÿ—ÄØHb×ó—–¨kÛq|X/›5—ŒŸ¹ØÔÜ‚«¡µK§ß(ªÀï"»öŸ¶úòÃóîâ’Öʪëÿû¯Ÿ£V22=ñ™U³Ø;Ão$ÁCpÕˆ¹þK$O’ð«ÞúüxIeò;ÉïQ¼(y´Þ˜ HÀT„@M²ÕGÓHk‘ì¢;,bjÞŒÖá,y­´›ÝI.šT*Àrt‰‚ä+ÉO”64 WFü-)B¬¶¯¨”ÚÜÖºýæ þFëcÁF ܾ)KËÆÑ&¡Z¡€)¬ Ú&¿|ssƒÙ\­*Œ´…Þ#޲<€Ž‰æà ƒš¿Ðò€€hk5µ$Ô[l‡Žžº”óO_~¶º®þòüë7oUT64H RâÀµÝ'*Ï¡v%³ÑQ‘YCR‡¦&g¥áO!ÍÛï鿞DEÎàýãKÏeq$j*-­ýÃkböñ?üÞ=¥„¿œŸ=£þågžüÙŸÞä~æáþZÑóÄZΞ¥çдtÕ¸; {6o*5$AÏp,€!° îн»ëî7¬œqíV^\0©ãݯ?±Šù%U‹¦*ª¨%¾ÇåüRª;5ò•Gço=x~TFÒ‹ÍÛvøâñ‹ù ¢#çoUíKÎÅÃ5e$Ç ŒåM„ëÁX0bB°‹+CL¬6 QsK3{k;¯ˆf¼Á+’"œHî¼C$($ AÏõ`Mº,2'e(b£Žýáõœz›•…뼚_´5 t¬.² }£DëØK ÞÅ5•Ñdªmk¹0TßâbÇ:ÖÐàÂ8my”jzAÍ?þäW<-mAõªPcx°)Qg ±†µ#˜»«ß DrÍ-aÍ­áM­ZkSQþ í•«­­Ö‰£†O7­ïÒôhà!@—Á™ !ÏPÕO~,ly"{\5q’ãSåOû)à* ŒÖ®\ºi뎅3§öSùÚ°ã¶›¢•+]5b2ÚB‚mQaMÁ*t¤%Æ Sl ii mm i1‡ÆFºÉd„ùt=^§jj©×„6ªCï)’ƒ‘`2; ÜÚÚ",Íz£­áÚµç/!î\=;C¹õ•'Ã"‚“´ãØåS9·¾½aI½Ñœ¯šLÌ›@è4Ǿ|/mÇ€˜FÒ§¾8Ä4I{é|eè#f'Î^:1캆†­{9suxX•±ð‡!<ÞŒæUÕ/ptܘ·ÇnmNª3þüà‘åóf¯^8ßIcI óuƒgP~‰0ÂÇâÌ%êÕbJøÁ«úéÿˆtÜw¿ç1œVË© ðè.;kÓÖí<Ì}öcdÍ͵å^gRácÆ„ed(gv.IQEHÁVfQZõÐä˜ÂòZØB„'!!óâ£ßÞ] j0 Fsu±³»`?5[󨡉‹5Ì¿yw/Qc¿üð¼­/P@È‘šãˆôT‹N\.HODaÍ[NAYYuC£ÉJú..ð@4%q„Ôª0b¡¦6:#‰`2Ñ­ú@®ùÉîý»žh h+ŒVUFF"·rC?JiÒ’—Q§M­5~èèÁ“g¿úä£c²2•2¾ÎÆáTtáj._~ˆ;Rój_ÿCsY•UÓgD<àÝ!ÇûHµ¥ðß>ùœ‡yöä }ëÙ°}›¨¨[鳌¢+ù¥ü!ÛÊHŽyxád€!î>R«µ”Ö QÄVWX^óÆÖc/¯]Ôñ.TJÐG¼¿û(#%vLF2b8§bàÔi£ÓœÏÛ•£S‡#n£¯µK¦m;|áW›wÃ(™<2ÍçQ‘`•×|q<vXkðÉpoí½0BþeÕ,<*ûÏ\‡)8<-~Å챩 z™$Sl‚¥©¬®yuÓû¥U·ô꽺Eè,+vÄ®3½§-‹R*oüå_7®[µìþ93»vžAE%•ÂU£«ÈÞ\YY¿i£ÔZHHüÿù«šõ·ã§@7@LÎcÌÃÜM™nnñ–4lß. Ô­XÑMI/½…ƒ¢N£Ó£6ûÍ'ïG©U³Ã‰ÙÍ›”5gâ0„;½+ÚáS ü¡mgïwldåÜñË猫k0é#5â›…Qn‘*¡»ä¥”ìá°±XÂÿÓƒO_-Lˆ‰\¿b–¯FüØ•}ù7ïî›>&]àìÒÇƒÅø±•–ýäOo7¶5_N‹2ª|Á‚½·ôDcéBjTf¥ñm_Dê4Ó'ŒS&0ò *BÅÞµŠWµúc›ÕÊ"E>öDX¦¹sÄ!Á[¨ü4`¡ÕÈ—®Mà"‚ÿx›÷Y@ÓÏñ47·Ì’s0u8²à;?ãF£ ÖðÐ0 c{Û…}c³·©Ry—aB§3å1æaîôÖ=3­çÏ5—–PL=}zH|Â=ËûX4¬fB’³Žw) `’!‘¨Û±ÄD9û‘òxN´rºdÓE·º¶Þ°iç©òšFðЂ©£PLv*æ—ð½î›6zÞ”‘O_}׉òê†gWÏb‹Uæþ*Z­­oøå[ïÔ´œM‹<,¢Nž·ÀÀü]hKë_>Ú:"#=*"B çT„ƒ1ºo*)nøèCËEý—_êd%\—õÇw?ºœ›çØ¢£‡e<÷è¨;æwšÆEÓ©‹W¸5nİ{úÜyø褸Øÿö×;mÍÝ™¹·~ñ—¿Ñ bàï|éi¹»ß¼¹ùfqÉý³g¬_ÓkGu]Ýñ*Má‘_…ܦ—&4*sßß(E>,>ëeüµzE ‘¤´k²lüâdƒÑú½gWe¦úx¤-ߢéc†&ǽúî®7·éá¹x.RàþÊ:²:ØØ¿ýɶ:³éâ ‡D·ëüx]\^uö¹‹÷Ï™‰o…ÛÙJù×3®Õû«ýýkÂ5pÔ“Bâ$‡oy =wþêõÿíò ‹ïÙ/.AÿüþþÊ*«ïYX9®ÜÈ'Þ…rÆ£ ‘H¦³}1íikm5îÞ)M$$D{ÿÍÈ?¯¢€€D8Fo½¢Öðò“K}ÉëÃL_^·uò-ûσ !…|K! ±:ù…E—®çåÅj}[±ºç4Ç­€-,¸¬² ¼#­ç¦¤gP‘ çfËÏoÜ&¹Ôj£ŸÁ…-wÓÌ¡?üç¿ð÷‹|÷ïÖ?Ž ©Á`ħŸS¬“pÍî”ÙÕå= ÓE§uyªˆØé³…¸ªª¶®«ŠH{.ýyïó8„ët"“ß?}¡dƒÛ¸ŽÅ^]cw’¦^ ¦cû^—c>y²¥ZBÆš9süö’^·|Ê0‚3¾µò‹Ê/æ—?±tÆàDb ˜/³>s­èfI¤Pκˆ‘ðÞ³ÙlÇÏ]l ªˆìµ¾Ò¦ãªñ·´†5µ†‡†@œNw.WuÔ·vǼêí4j^û]€lF?û|pTTo«÷³-L÷õÕ«–£ˆÀ+Eeõá!UôªºÏnkË*7ðãÌJMi”&DSÊÔ·'Àš“c$²¦ä¶1:ú©;Š/}k­ÏµÆŽ&ê–O& · ð¶biɉ•5uOžy÷óøe!Œ¡( èÁÿl7…E1Ε5µ?Ý.À *,›?ûâÈésâî_>øäÂ5ÉÇAFj xñÚ¦÷^Ã]ÌÅ17[­ãGfÌZVUËÁ÷wìæ*A¿øËÛ@"† Š¬7ÞÝöŶ}‡D›ž[~?ù[÷DϺcÏö‚O$™¡\Dc¾¯m|_”ܼuÇÞã'D¨ÔÖ×ÿåƒ-Ž-ôa0ŽÕ½4¢q´gXè ½tR0lüýèŸüíóãöU^Óð£ßzâÒMþHT×:-æK™ˆ¬VkumCquã²Yã|R½úžëŬ—Îw³ …f¹gù+ P‘Ùlæí§8ÙÞ€QáîŽB›[ÇÕǬ“‡ T¦ÜÓ»yE5¿û­ ¹þ…ƒ\ÀüîEìÑ•¬g]o7GB|€”íëO­ãkõ¯~ ŽÉ»UôÜ#|çKÏ|ï¿A£Ï=²FèwUXî˜8 ,]´|ÁÿéëoÖÔ7€BæN„lHAGßzþ©áCÓªkë¾ÿóß‚ÈÄ\üôåZ“}ó¹ $à9}¾Þ.Ãú`ÇäïZA,…h NχŽÁê*Ö)|¦‰£F >…ö÷Ó­’ÇFœ´ýÀaÓ'Œå†iŸ8ü鞯ß@)16æÐ©3Ü4zÄúV€ ¿ùƒ·Šäú0¹®÷&LÙÇ[ë%𪙿ÀƒÏ­§È#̓ŠÃ!ÇÔp†ÖÑ,®(I­/Ž_“™üØ¢)ŽIS†­±°¬–tZRÌÑóyª°Püà —EN…}æ"âÈ€/%Ãñ‹ÆßUSc·5µ@:§5rUû.o‡¹oÜ~ìòͲÙZåDF¨تS…GÖÖ¢wíBv‘*8(,(¸áîXi.'¬  knI©5'×™ùÙNŽO—Ã…]¸ª)/~e˜Ïž1>!‚ã"×=é*Šô¡ƒÑ,j KuUüÁ³É>=›3—$€‚Rv§-ß³0\¥Õ‹æS7Vììýí»àñ*Dx÷«ùGŒçsoî:’}áêuÑ>A!I€‡8ß*)ûÁÏ;:+sÜðaÿüÕçoùf‘¤Ž\ïŒ9é4j.áEôCºÓ à¥ë7]·hÖ]AZâA€* gLðøŒ¼7BÎ|”@Ùÿ¸»fñ}ŒŸÄÃKýâÏoË]ôm0ru/Mv Rñ0eËþs„*ƒM†¿éÖÌáµxêJÁÖCL¢¯sÆ–x®ÿóæb™Ý*Ã1µeæ¸Ì•sÆýôÍF‹-·°ç„“G¥‰¥7[mïí:MX4uxXJ|â³äØ(|gGhÃ÷Þþ[å5ø~ü»Çø¤e>j4 ÌfKU‘"&ë®ú9üþƒ}s‹æOùÌs]Õ¦[Ûaîx¨ÂJ‚ÀƒWˆ Ø*ÖhhB̵’ÄzKy´ô²uÉñý)ãËLŸûñŽF{€?§6×e¥G††¾žsÃ)¿Ï—Ðô{G,«<\Ö÷l*[Ë”‚š ¶€ €€äHul¤Ž8}„U„u¤Œ£"ÉôÌ~è_úJP¸'Ùd?~¤JÇ¿ûۻȒIó ¡C’%†Úñ|ϱÑw"7ÅÇèE BÇè³½·<"@ ü!`·l„1{Ò„“.#Ë£ýC'ÏðGÝ'W/–ž*ʣȟãxº·0'hÞHw>þî¶ŽµàQ‰K´šDhˆSieUU]}„Ý[ 3Ò( O4쮾 Ftä¥ç6›€} >P­Ö,¸ÏKgÑ·a:›{öZá’cˆìqìb~iU=¨è½]§fŒËŸ•òÙ¡‹Ä/#8N®aÿÔ5˜‰\¶ýè¥ ×‹ÖÌŸ@(½§®mX13+õŽéžWÁI`¦ºF3EÒcš[Z+jП›2-=0(oÈ@«±ÃRú6`%×BTÍ·j+³Í…îß°ð¿|£„z:çæú•³¹wüZ!,·d²tÌÏćBÓZi°„×kÝåJ4<8ÈzÛÒåþ”¤xu¸#*Š m°¾:ÎͱŠcZ”Wš›ì_µ¡AOȬµÚú†Š¬¡AXãGš›ø+n0— GÛÚâ“f¬ëHoEE¶¹ælI½ $9%òáG§4ÀiúçÏJ# Fƒ‡Ä_?øHŒxêÁUYéCx}¾_’1uzܳpE5;D³m•”KH‹ƒ— ZD[vï'½zá¼ùÓ&ƒ½^ù÷ŸH±•ízÙÿô•ç‹ÊÊÏç\¿’wóúÍI?iëöÿüö˼æÐ€†÷3vx¦(,ÎúH Òus<°xÁÑ3‹1S¹˜‚\––!È#aµÙ*j$=n}dî|Hð–,¯®IŽ—6³b;X$Á¡Q©û<Ñ‚7žap¶%sBíÂEAvúxã,ú0fö¬ý§¯/œ:rÉÌÑ0ðd¥ÓlÜžÓÅGOa¯%²=ºAÄô€ÙÃÆûÜÈCTèUµå°÷«ÂBÆd$É\wvAЩ#`/æŒtZ’¾¨¢–ßÀã÷O!¾Ç衉¿~g/QÀú0Z…WáµưÅb³65Ç«ûÎÓa¶'/ç'dùœñ_½x1·xò(I0‡Kñ×?ÚGäüƒÏ—¹a圎9 éímGŽœËåãllVêK,$rÙñ 7¶<[YÛÓîÙ5óF¤'uÌqè¼ïIBšTW!da$òCÒ÷û]“1°å£6GdtJ¼)¿h\q}±^]«%Î|¿›ooàŸ']’št¦ªfùä"£éûÙg$%ÌLˆ¥ƒ×ÌüÚÁlòÿuÚ„¨°Ð£é9_Swà¡eûKË'ÆDQT¦ qª~±¦>U«þÅœicõQ¦ææ?^É]½±xý½8:«ÞÖ´ùFAoÏz”E«ù£bxSK¿ò¢â«…ÅV-›7ÝY ÞÛÆ]^†–Wõ›7‰qG=¹>pÀ? @Øñ3æ­-Û.^“•3&Œ"¤[¥R8¶û¦OÅù!hæÄùKIÌOWdÞ³0¹ÀþÐJŠE¬v«´œÔáá-Y£‡3Ä»@îègz g‰ç®\Cúö½ŸÁ}·°•«©¯K²£$_©‰ ü¡%R÷úqY¶ˆvÛÉ¡è-Bü9sž¡2Â8øÐô´ä$QfD#@"RŸ#ÀëƒU|VPZÍ·Q­^Ÿ•ña~á¥Úú“ùÎJ[Ï?Ns¶ªæ¥ýÇ4¿œ; <[葌´*‹õxEUÇêTÙ0<#^þõƒÙ[n}kÂèiq1ÿqJ,|ZP¼³¨´ã Ài¸ðÄPý-]ØÛŸngwsíÜÏáQÝ+yE- í>ŠTªˆG<À( ýÓO~åH}Ø$O=¸R䌑…ë}Ù§œåäÝD†E>¿UΪ0IŸ”_î¦Ïv`¾¾bÁœn Ëícáµçè‰úFƒPNZ¹@ö‘…¢4]`cÏ*åEG0f®å|º÷Œ¢´¤D<Ü‰ŠŒÓë[¾ø×on¦|iE%œnñ"™:v´àëÈvš˜?m n„pP€k…“k$kàž 9×áQ!;ãÖ°´Tá#=ës9×)pêRŽv·~UÓéžÙj6›`AºÍÜy ­k‡'”ž¹ZÈOàímÇÆe¥Ä &PqEÝÍ’êë…刺^|hÛÈfÂpIæUUg0[›Òõð–`#™*y@ ‡ã¾'–Lü€ ¹ÅHͶ–ÞÚ”¼xCR›;•s‹ôž“W' Oi‘ã{Ž@ ¼#X#®šLÀ n£ùF‚¯sáz!š[èl½vkØ„‡MÍŸ`ýdÿ§œG.ðS]gൾt£øÉ³ÂCCY¬ÝÄi÷„$8È)ÇU#§6WY\Øfš‚Q„„H­VGDD`$oFi0Ô˜l¡ –äz _±æ°à¢Med¿X}5·¶=;"sDTD¹ÙbljÖ„ç7'ÄD'iÔWë`Y[Zà)Ž´«y-¯úêIز*-Å©:™°—›šîKN@wËyIñ¯çä’¨4[ªíîWH÷ó€[v3AÇýÝí»&ŒŽÖ©Ø{bR^É+jüø£6»žˆÖGxò•Çû<´já ‰•P:Þ`‹Q×Ѐ< ©üâÙÓ)FØd ‰n ‹ÖÒ““ÏžÕ)xôƒ=hÜJNˆ[4sv"À‘cgÏ#Þ¢$ù¯Iï+—Ìœ8Ž'°&fh>ÁúúÓkym9üÅ'&ÞHqy%\. %Ÿ{lè®û3ÕQNr*ƒÂ8R<žiŒü$½òÜzQì…ÇJÜpª€ƒ_µs­äú3¹/J‰Ä£«]¼xàyœž%u£œÎ¹õûd¦Æ­ž7n‘í+jÿ÷ƒý{O^[>k쨡‰ !XJp}-ü!ΤANpBEæás7—‘*Rl”ý¤÷žQ‡‡ò“K4˜OÇ.ä½ùÙ±(­zõü žµ›zgå¸#üïº>²/Jlï}'¯|û§»’/±âÎJªñ¨ZK˜4/À€ÀŽ9¸B ŠÖ± àø£à÷_\óÐÂ) u˾Ó?ùëgì‚N9ÜrÝð%‚ˆÃumö½%Á+EEEÅÄÄÄÅÅÅrDGÑG Ñ„†@DÄIÍ­·µúÞ‘­¥HD}ðS+)ZIh[¨ØhÚU\ö³s—kìJ®e¦v !î:UGq,A­25·PåZ}U€PNͺê²0V{ïŠärïŽüÁU÷¹ïãñà׿ûŽ˜pÔzÉì| o¿ðÔ=»Ë’òoßú@ÆItDå5r-zYº˜¨«±“ÙMá!I‰k/ŸX±¤¼º:16V¸a™O=´ê1ò«ª)É;ËÑ#í¥u®[½p]…Ñ™ ÆgOž€!=ѰNIŒï*ú,Ækøï–G.£ì~½3ù$zdÙâU ç—VV"×~iívm¢ iˆ†E^£Ñ8$1$§6{8ǽ7møb‡¼ny;gÑ{çÒÛ‘ƒZ¾óÔRŒÊH¾-Ü7ejÔÈ¿À7|cxúŸWO•ΟHË™ÿüü ‘C#ÿðÌòºFSt¤FÈàÈÿÁ‹«8³;-VôÚDIŸ<·ïÿl„öÂ>Ï7ûbæl/•ÞWÐð¯Ÿ'Í™8|Âð´='.# ƒuDLÖ¿riÇœa©ñ@(|"d¤Ä}²ï4œ$–æÇÙ:oòˆonXöÞÎgr ªjþövÇ€—«¼'€”‰Ä T„ ^<6’ZÚò«ªŒMÖ 0Ae„Ê…:FòºóLèBBâTá—këJQáa{¯–#\KÐlÌ- ˜3z’k¢ ÑÚv¥¶~ˆVs¤¼jtt䣙içªëDyЪ f ‡z}I6¢r„É›ûG_ZquïCE¦ýûÚ#Ïœ6,ËÕqY{) ñ]µ²Ñ‡ÞÅâꦰh„*@ŸŽ ¢`$¼iw¼EŽ”d_JNØ€/N™ý¹Ä꣮ZÀDŽ¿®îº|0]uäÙüVƒÁxè cÀé¨zfwÎÄ=;N÷õ›F‚Sû죪˜>Z Ñ ÊÚN rI¾oC¢ŽSîNAi¢.<"N›)Z;z>7çfi½Á4ʈSWn¾õ™d5HB™Å/§œÑ™É•u†ö 9`KŒZ»|ó³'dí;™óÙÁsÝ\8%>&Ò)ÇU¨ÿÓwG  a…NãlùŕՇrò,­-Ä/R¹Ð}‘Óà”VLˆÑÿfÞô »ÿèäùGe}¶j1hæßO] ð˜S᎗¨(}oâ˜÷–- ÜZP|¾¦Vm‚ ¦¿^ÍëX¥o9Zks`[›^§E¼¨{4ïCEu²žõ†{³mú¶TþZ~ ¸ƒ’A¾]­JwÿRô0ÝÑ…¿ÍAB˜"ÌÔµ¬Bпöà ø­§îˆËÿýÁéÁM8Š×G:å`mõôª9ë–ÍEÅFégú‘ÅÓZ4µ¦ÞÐMŽcýJK\³v²ô«—V ±ßÃ+*(­Ø²ÿh­:ôjRTsH™"?:y?Fú§/ò'† ‰M¹›s àëp‰Æ4˜•U £g‡D³>jçXwUýz}#Æk1áa˜ÏÈöü/:jv7L&Ñu¯ÎéÕ&•* Ë xE¨WuÝWØË^ÍÖÜ\ËÉ#$5¿À¿e?\N;â³í2 —wáoÐO7QeBÌ]îŽ9tú|\´¤6 »gŽ\Ø'`5le6nÛY­ ½’é>‘#õ€/èÉ9Å·] Ë9÷LÔtP¬&”ë=kõ¼@B½9Æh›02SR³k„õ¼®[Kö±ºup¯ß¼QdF­[/ˆŽü9~ (“NšŽclÁqqª©Ó”9Hÿ¨K³­ÙÒÔ*¼ž*vþu¤û=Ѿnki¾–10¨ã0”–Ûh^nHŒÔ ‰oý!«½z|¨ÞÄ+jil0lû ’bïQÏ_6ÿ¼Ž’øÌºR»d©Ð{ÝòyvÀø'Ø{±ÄÒ܆÷&ÏŽÄß{o)€u¡Ð0¾1……®¯·Õ}²|J)£Ê2*%mt"¥£–îGE}Ykãž=mv«B»Aþ]LÚ¾4ç¯ã§ÀRÀ¸ó Ñ›né]ìßß•·R?º ¯ˆ ŸòÖi Êq#;™Íæ˜ÆÚ‚ ýàFP «¼1Î`Ó‡‡ OŠÅ:O§Óá¼`¤4žSo‚®wbj®~`PþÄü“öV Hâ3{€I|6y²·NÃ?î§îÄ6î8QUo¤gÜ%ÅviÈé¡aQ1®\ØfǦð´DØ&Øñ–Ï䀊ŸÁ+JÓà QֈɕÏÌ®wikK¬3Oϯ%kB3ãql)8s)ÇIy­¥¶Æ|"›'&ª&ù÷•Þ=þÒž¥€_|æYú{oï›vÏ-’¢”‡©BÜÎ+"°îŸ·¸Y"yí  ybÙŒSFõœz8ú`÷Iü‰ËrÏZyE•?ëó¿{|ñ”ÑCïYØK €ŠÐ+‚]01=ùìÍ’±Åõ×#l¡ƒˆçŒo°¦ÕUM­ÚÀäHuT„<„KKœ[FFF‚Š¢ š×ðŠ »váVŒß†nÙråÏK«þa0¼Z|†^ÿžøƒ&\]cÆ5gdb°ëBŠvº 0‡þôñþ²š…ÓF½øÈ}D.ûÛ¶£ø+’ ÃC"ôŠ|)°|d~ üb P%“«p·¶Áèn”ܯBìVˆ‡â"uY±zsÓ´›5©5¦`Ÿf’ âk-ÍC+ Ä}^ÞÝÒ:D’¡×ÅDKž¾ãããqö­×ë‘ )J|ÆÈ½†Wä`Õ<èœ+äçíFß(à¥â3^åÌ—shhÉbíÛÜ}¦€2M`^‡Î^ûüðyÑÑò#µAMîîôZA|qÒ¸aåú54éã½§ìÝ„ƒ}ý£ý "(0wÒð§V͹QXñÛwvM›?k„B«æMX>gÂÏÞúœŠ;Ž^ÄíxQEíõ[å8¼$ýèïÙvè¾PªðÐgVÏ•Eº{Ržmr E8r$Fl|TDpkKi£]ãôjcY”º"2Ü¨ê£ SÏN­óÞÛÚ›Z#-M‘æ&½Ñ&Å3‘ ×„hUa „È 00âŒ^™Šb1`ï@EÍ•–3§¥á¦¦ªÆŽë|=ü¹~ (’wÄg8oôBwêв‹"I;pƒª7X’"ì¿F£yã¶£¢»UsÆJÕ•¸»÷²ªzº9T §È¥Ó<ÿà|‘~og6N¨¿±vINAéžìËÓÆd°ÙÃñ<½z.qa·¹°xÆØ§VÎþÉÛfMÈš::€U^]O %³ÆUÕ5M–Z ª­ÏmÚ~̇¥f‚bâ *bËgãgû7™L(IÒ´ @@vµ9¨ÞœRg¶…UéÂkµaª~©¶¶éM¶Xƒ5ÚÔ”› «Õ¹ëqE"6¾°NÝÔBÐ[H0zRøv jkS5µ„5· –*lMMP€&<0",V P«Õ‰ìÚD’:— „D,œw "ÃÎ"Önù ÇgΟöS@ù0îÚ)‰ðWù£u!ïtŽ(møÕ’ºƒ9R'Eš„s'RÛ¨äHAwS ¹¥õVY4-˜2bÁäá•Õîî”öÑá,G•{Óq[2}âÈ´±Y)O_=y9æø, ¬ž? ¬ƒ6Â>8I)ñz2£ujùQù—¯< ßè‹£’fZÀõ­©å÷ìÍÉ/õíX‚z<0¡=#Ü7 ÖQ˜Ñ¨¶Zcm6cS‹±¹5Ü€ÖРu(6ü–Ð`Khçð…k+¼snk‚ð§¶µè¬Í–&¢gHè$ª¶‘e9ÉõÚ°;å]”RÛš‡U"-ÍAm-M­-¶æ6hïÝ)bÞ†¨B‚ÂxŒ5xè$‚K4 A ÒäpKQù2…¼É15WøÅgòÚù^@»œ7z•õ™@qâzùåüÒÙ†yÅÝ0DæÎ0t€îFµ5XmÂPi∴'–L7Mn˜S'M¼ŒÜË7ŠgŽ“0ô槇çOI$.1¡âÌ Zˆqj?ÐÈæ_È".ÎÜa€[ìÝ©H`;»QšSa_½±ýƒм=Ë6Z,•ÍÕÜ `²¶´ZZÚ`%é[$ qû€Ð°_ÀFü‘ þ H[[pëB?<°-<8PFɶ€J[ÛøâúZMhi´ºNÖÖÅÝîäÞÿª­ÍÑ#ʆh ô“ ™ß5å3 &Ðáq0S4©$‚âà’|À"Åîݱ'Jx*j*-±^äë¡á#Fz‚Jþ>ýè#¼Z|&ÞqjUhŒ6ôÀ™ë3Æeˆ-­´ðÎjìßÌ @ÞãÐÄ}ó°XmÏçHDäù—]ØEtsÇ­($(Ð`rˆstFrr\ôÉË7Õª°ô¤ØÝÇ/UÕÆ #‚VøÄ˜Ãç®§'Çåä—X›š§Íèœöݦ>–¤·7÷©c2`&m=pvÞ仳/¡Z4vXJ~±déæªÃ`¶„B‡}ÚU-÷³†ðü€ bEÛb5b£bÀÏ|M ~;ìÖÖ¦Ö¶¦Öð$\ÃVyöèöùIÌ#l¶ð|ÄŒA¾°?™âá¤zxhSƒ­9åm4ªCÏ™ÂCÌ¡ÁìøëŠÿØ á¯¥5´¥UekEL Jgi’äd˜%D…E©$€5‚àÜ¢k†A>xH°ˆ€D!,i¦Ï]ŠÉeuöTdøâ¶û»åŠcÿåC¬+—ŠZ`… Fþ …ŒÍ­ÃpŸ-skG.o\~Ç……†HÔË«=~1îDIn2¨f]]oœ=LÄNeÜAöÄ-û4š%|üòº¥È˜à"ܳ/dµ.à'1¯¯­½ÿÏïß{â ‚]ˆö*˜=Ý? mk éÉâ€ir +:L:>+ü¯¿ËËnbLä²Ùãw»È­à õ+g³‘w¬ÞŸœº“^í–uéϨD]Þx<9œAÀð\ ÊFøxÉØ`$À »‰8;n+òû“õ‹ƒöétEk*«5ÊjÅhÐÔܦ¶Ú¢-MòÖÄ£©žŸ <äÈ#ç~(\¨ @uH &48ÌÎþA"ƾ¡;@I†D‚K†! w9ä’¤e %ªˆö•yöTdܳKÐN9JE ñì6Œ®åâͽÁ`4™-ŽÏ®2×Ûƒ£â—£Q«"uÚ”„ø #‡Gè´üŠÄ/܃£rk×Ĩ¹ûlò·öåòÆY^z¼yÆGëâ5õ[ž’ OOŠqy_Šmýf¯ ‚Ðj@(ãòóêxgû®â ‰}BëÉñZU÷½ˆßgUh`y½ Mt„¦û*÷¼ |ùþ‹¢G»‡X°²+í±ÃRúõ•µ:u¸N# ÆÒk?|A4¸bÎþDúï×/k’"ÛRWVÖæÖãK¦?0Rm£1.ZjçŒHO”[uû|fîàÂt}´ IŸÛq_Ebp–4Pg;.’˜Fà`;ÛèßÈ Ñ툃Öx‹ "`-¶äž­ŠFø§ÙÎBÄ&® WûŒy¢¥¿`É,(°\(l.îáÌA/ ]0~@u¤Éa‚æœE1Œ¸åxˆ[”q…]زÒQQKc£õòe&šž–™é™÷¹)/žÚü¢Ïö¾vóWL¤–÷‘ø¨ês³ƒ¡bm]u~aáÞã§ø%ÌH`Ѽ̴!âeá“Ó7î½ûÌ;­Ïx—………ªÕ*­V3:9âô­ú¿|røKÍ$ÀHÄ|UÁm£“#¡t€ÐÄéYuÉ›þÃ/ö^¸vƒ–]š°Ðð°{¼™ÅVÄ–Ä×:<’[õ­ç¯ÝºoÚh§±õí]iY]ZnŽzèY[€¹¢œ€ù”-_º0ÁÜY…¤h¼G»°q6%  ˆT| $$"팠aÓa ™ÛëÞ®¾#C mЇ€$BBÇ! ßDƒ4Bk4B‚3‡ãÔd(G>Ó ]pÐ>ü ‰ù¢$íP]žµäCd:v¤üô=~{Ÿ€do Ô3gy|0¼(]Ñ„ÅbîQê`u8Àƒ!KW%’/€B`#¡N$øC2—Hd_Ä! gû/U:шŒcïkâmÉo·ÐÐÞ‘‘¨?ÀæÙÒœ_cÛ²ÿÜ¡3×ï›:rlfrG¦‚\ݰ8;púZuƒ)2¤%3&,6:2** @¨!obv@üøèØþÌôô¥œ÷··ûn¸ÆäXMxOPÃàå"mZŒ*.¼¥ ÎÀjÑô1ýŒ7ÖeÖ•u†I)R,-YœÖH™“â]'p ï=6—ÛøGú׸pÉ]y·±G;oFzÚ'-Mœ’TGu v6ð’ˆÎbŒ(€·d¼eoU:‰·.g©~°eÀ$`Ä0â !=svF‘<*_J(ÙcŸAqõô¤;支¿ûqmcã÷ž]•™ïÁñø@×¼¼xƒcôûê»»¡êËO¯Å›…/#oŸÉ/YMxhQmƒ“z +Âw!œ›-uàÖ¶Ö ÀúêFs•¡ñã}g?Úw6J«ŠÒ©5jw¹Œ°GÝd¶â”¨Þhá WÔ’®iPëõQ±±ú}4€PÃq<’:Kƒ1N%öÇ[=Lç¾þÞGbË[4cÊäQY……=cÙ÷T¶'>ÚùŠP…hÍV|-òƒT¯¦üâJf­o‹‹„!í×@„>/GW͵ÅÄ8"š•ÁŠHIB>ËSµÏò¨xJ9€/`#›—8@E95+7%0–@lœEkòYäË£•;õ¥Ä]?r¥MŒˆ°¶Ü\F6rTp´[Ò=œ2Oˆû½í»J«ªý¨‡DëI1Þà/¯[‚m ´}êÁUüŒùùõ¤¢òËwß¶X¦\ë3ñ*ˆq‘ÚëeÕNê)Üb§A«F¼L!{P Ä@ŠÀºÀfm´XͶjSc»=ÒZå¯Ãíœ~cÁmñ¡majiÊÈÍ€Dq±úè(I¯:8m’:K`àð´TAF‡{”,¯ª~õíwÁš”ž>~ÌŠù³++¥@°]N‰Ûºð(ÀâF•d¶Yš»y'ú΃‰˜oX`ë°8T^Ô’XÇ®ïD@'º9ÝUÚeßž%ÇYÐoQà Áy’‘ ÀPGþÕE¿âÌsN‚3íˆgq×±/ŸL+z2Ÿh—µ«gx’QÄ32÷æ­S—¯!8$oœ{Ü¡çKglÞq|æ„q£²2Å/pÀzwSG­ØÜ?FãÁz½JÙÖg¼éÄ‹/íˆðÐÇ.9©§p¥ñeÉêð¾å²Q"§9‚ ͸Vá¦t¸‰˜ÐìíÍ@ÚN@?l±RÀ¦¸D@"´Š˜2tp‰¤ÎrüRJ¬…#ª;ÞêI«Õ_¾±Éh6S˃gyÀlêĺ^´,†‡Y -¹qÆÃ¶‡¸N§¤ZÌÖ¸¦ÆJKÛÏÞܶvÙLß–ïC|gp‰BZ2£‚¢$ÿ€xÄ‘¬¢œ–iàØÉ äÙÄíGZ²z?OùÌÀįUþÍʰãó&Zoyv:Ù»ÂQQ¶ …g•ŠxïÃ(ÚqèX‚>’wÍ@.Ï é ªîɾ…‡¦&Û·%×;ª`J°{šÑ.^Â7×÷Þóîì(Gú¬„ßÎŽ’©¹ZY爛Âk‘EaË!AI¤ð&t-ÀM£¦fa÷ÛþÊíy×Ê))Þþ| K8#DòDg‘ºDœI“œ¶I¥¶ñ‘ûfS‹»P²ç3Âû7on®®­£JrBÜ7ž^ÇܳzXH 2>¹=´ƒEÂ(íJ_ÒZŒÕÍa|cì:~iùìñ¾g "l5P¯F—HhMÒÛµ¾ ¤€ N 1ÀìäÕQTB<ኒ’£hTdʶ£¢à`õÔiž""Œ"ä²Õµµù%eVÌ‚™è©‘øp¿Pu鬱›v‡ÎlϽÝcHY|¦]²DÓ‡$€4G´NUoxoç 'õŠÙ‘@Exy¾½㌹Ɇ¢‚àÌÓ¬üõ)w¡ð„À:‚v™æ6(d¨$©”djÓn/ŠÉs‘ÔYvš›’éFNä’¼Rþ°ùÂ’RnEEè¾ùܶs^2KŠZ‡&,¸¸ÞâŽE¡w‚JÙãFHL;ª™Ì&„›u6‚°nÜ~,í¨­p2ÔU^‘ÿnÜÕ5šáË…4%…X£àG/—@IDATµáD•øyÑp‹ÚãH8Îe€Ø9víO{/”‹ŠšJK›‹ ¡løØqAZ­§HÌëñÙ¥ë7xÝóáå©aø|¿Ð–78tÆ n„ÓŸwM¿Õb19̘ƒ""ÔÓ¦+|ðíì{øF ‰ÖæÕ:ª§°1 ´Šþ*jœv­‰WÄL½ ^sĺÆngƒŽ Wμæ(©³¼³S6{ü(¬”y\¡aÏWùoŸ|ŽëWÊcÌóÍçÖÇFKÑǺ:c‹Õ†æVZÒñ3±ËûÐúBŠ)¹´¡$ì®P“IÝd¶5-­!¶Si}-ŽŒ»ˆÒUÏJÊ·Ë' zÐت nU…aI´Q-Ý/dö…pVüÈvJ¢—,ý¢€rQ‘Y¶>ó¨M¾@EeU5z»«Æ~Û_¹k `®…¡3š#Í麬Òï‰Ú,RÜíÂEŠŸ;º]=EŠB€ ÂâT[SQ£ågo~¾vÙ 'õ»ÔF lÆ0T—¨SåM¥/ÒÝã» >„˜vO0"Ó±àmu–š°Yc²ÿ}@G]Ç*NéÏößX‚´€¶Rƒõ”ÝÇ//›=ÎQ=ElÀ,“8ÄxH»p`Ö”ü¤‰©‰³Sï·ÕY.UÖ5ÆiU“†¥%ÄÅâºõœµyôÌù-»÷‹–Q¯7"Ë©—Ž—I04”¨h¡ÉÚÀkµFÇ€tŒ–»j•dC$I…/rƒ]x¢H`”ûoW‡+ |؇:¼¤cÒÙ â ¼ƒâ×€°ë¸jþ¯¦€‚QÑõ«‚²á£=æ—Œ`P‘¤mmµé£Ô½Ziê–U×#Ö]ˆºìÄ’¶óµ3BÐUÅ _9f‹°A¼ö(FE³µIú™;|ú˜­6ªkTí^a¸äMj¯†„Àƒ7‹c³T§)rä€G½jðž…é‘a>êž%)@È”ºz3Ô†æÁé˯'-(¡ žeM¨ZóáaÖl% éžc`GA†=îÄgɤ-6(¨ÑlÅ;×mõ>Bãê)÷¤†S»:‹e ® “¨OMŒçˆ•PtƒzNU:½¼r#ÿͶŠ[/Y8wê¤N‹9e¶c{4:ž¢4a‘&§€t”‘€‘}É`¤P ÄÏï}bA,¢eVÖ©}å_2;)HÞ 0Š$M*•]÷Kr¸ $_“§3`ìäý Ÿ¡@~Ï?[<*Ø®_§ßàÄÄàH RxêÀìÒ«TÔ4üáÃ}Eå5¢Ö˜Ìä—]¤Q…ýçëŸðfúïo®#Ÿ7ÔÿýÝÑ‘š\ðÜ‚üèß¾ö(À¨¨¢ö¿þôéskæÍ4BîôÇù¬¼ºþ;Ϭ54™L oä•õËä]%òŠ*²/寞7Äÿç×ïN‘öÂC  çgïŸ9vÝ2·¸ÿõ¦XÆþëWq챫4/?è, QWe”ŸoÊ>Þj00NÍüAáÞáÛPfÉÄ'‡í'4Ô¨³Ù`8›ZlfS…ÉTFHIå¯+F(mÅ81 Ä$>Yª×ª##t !ðP\\*¾=g••ÿïÆ÷@'49oÚäßõ´÷Ôå‰-¿DdzH„%¯¾Å) F¬ßRð®p€™Œ¢>1nóŠèÀ«Q‘¤\…,Qrç"A!é v¢øÕÓv^ËîòYñßpŠŠšŠ …fFøÈ‘®˜fÛà%Â0âÜó&(ÿç-1ËfŸ42íðÙëGÏç¾³ãø‹Ü7}L>NŠ+jSô¥Õ¦FÔàÔjIлB%&&HDA·{Ò¡¶¾á×ol†ßLÉñ#²žyhuÇ*tD¦½Cç€tvTÔΠ“ŒðSl Åæ€Žé–(ð.`l`\‰Kä '(#‰ÐnkYA‘éHLæÛÃvF³5:J+ÈîØ‚?í§€BQ‘íÚ5±6a#<ìˆ}Bl=V€;7KªfŽöøÉþhxZ"L£ìKyVΞ66TtéF¨èbnwÉ@@ʶýÈGþSqѺ’ÊÚ#g¯ÏŸr)æ–¿þÑ~‰½8wÒð§VÍá­ñ½_l‚'„¥LVZ©Ë7iç§o~þ×"QU×ø/¯~P]oÀwâ+ë— lJûÁoÞ«i0Ž–úÕÇ!˜ÛvèÜgÏñŠÙ<³zîô±™ç¯Âýúƺ%c‡¥l=xvûá ÿó­u$Î^½5|H©+7ãô_zhÍï^{D’ 'D_xNéþ²ï¾Á¼‹šqÿ~z d7 »îg_¼–—˜BZpM¦Í@R†WÜ‚’ª'´yPx|Ɍ߰7kH—ß[kL:~áÆ¶ÃçDË0~`É<¶dúÄ‘ic³Rp¶{òr>¨ˆ»£3’¿ýô ¨{sš%ë‘é‰3ÇKOŽ¡ú-Ú™3q8 †ô·ö·s×nÕ5JÓigkjaH9ù¥¢dÇ3ïñg×Ì¥q0Yq¥.äKGkáÈ9I9¬j+ÞéöFÌÑpÛúÌ»Ägò±šlÀÀ\Îv§DZ«dÇ$¹%*G^½:ò4”á,xEPv‘]±WRg$ºg#Ðê¯}z5ï&%ñùÍç7`BÞU-ú’»ëŽa ë.4Ĥkj²6ÙŒÍAMfk•©¡M¨Þ¼¬ºO¯òmÒ×u zL7oöªb Ûùtˆ0ûÀŽ·ôè¬ hÞß1øëûŠŠl×% Z Jš–æu4OŽ“¸#0„dqØå¼~|I±QäÃéùâèÅvŸ ?†³|À’ÁóÉ3rŽSðþ mk<·Qé:çÀ€@~ç|Ì‹*èn;Õ—†(Ø.Õ;ùB µÒ£‰f¶C' `K.g¼÷qGHpÀ[`>Ñ‚Ád×Gp—Wy„Au»¬ÿ ={öH“ ÑÜwŸ÷Ζ…{?Ê40H$õIAeðê Ž‘8ÛÑK¶Ò¿Ø“}î¢ô8¿ü̺äø{ðQ£—NÒIù]D£C¯Ú®øÅ(ÿàåxñ íõ¹kPb9xùUŽ= `·ëøåÌ!Éõ»kXþv½™JDE- ÍeeP5løp>ּ޼pb0:;›Sð×ON™~øÜõÒª:dÂ@==)Ð ©ã¤%vô„-ØÿûÓ§ÝLyÁ”‘ªJ*¥J¨" IŒ¡ýô东ü Áˆu>*jAQ]5{þzá$pUuh8!죵-ûNo=pvÞ仳/¡Z„.VuT?~1ÏÖÜGª«¦ÈŸ<*ýÀé«'.åán€q šõÂù–*IV‚‡­àˆ.©Ý é”sëöfsWhI1<ì»O™‹ H!Î=ÆþìS;å_\ûðˆŒôîë HdßÚ%vTÇ€t €»ÊŒFd2ñ=ÆPCï~š®½+HdìU»ŠÚ†‡ï_H-ÆìÚ!ù[ó (ɪÖáŸõguŸpÁ_>9xì þhÍëGO“„]ôùáóŽêAò-PÎüÉ#žiW6—óå?cÓ/7~!r»ÚÖ?ës.§JJE)rI‘@Ñö»wwÿçË;Ý’/ab½±õhxZ„ih[c=GüEð¬ õ+gó±K™ñY©à!tP<ª7ËÕ §cýé㨇MŽÒC§2>yiؽKÌKwÿRŸ™`oÑ€ÏL¼?9—smã§ÛE ëV/›>¾sMAÇ. 3›4Ì9ô‡8: Hw‰’ ŠF\R€Ÿ+ FJr¢ã¤Üš†´/è&¹¯ íi»÷w7lhzJ¸uœþƽŽùòûÍ[›¡Ô+Ï®ï”^u›6Vÿì'ÜŠûçïG­{²Ó2“‰FÆÉ›¶íŠ‹Ñ½üdïv;áÿ-.J§Vu©bÙÿ‰ Û@KI§ïÆÃ¦ø`n^ÈGp€j¶<@±A6¨QÈ™˜³:¹—”o9&h eƘ(Ã9Þê&ýê;»ªj V/MHH@Ï—/æn +ä–ã“\ðàêæ’ÞÓC¿ØãÖ:öëÖŽü÷–ùE%?ÿÓ›hSqÉœO> ©÷õäàmc0*++KJJJKK+«ªnT7¬ûûõËø¶qlßš^¨{¡¯„htØzœø"UèW×Ìr§[Ó2*¼"i¼©zÀn—&<ìé–''%á»o/ÿ¯Ï­+ëñÆïlxŠ<€¦["MÎôÆ0¥¤âªñ‰&4–ºiPݵ勨 Ê‘‰à,9Ü™Žûénz‘oÑsòå`HXsr$HݦLu7$ ôôÒ9VÖÔþö­ÍM;jíªå=ŸˆàaãÆ'ðë¿T«­Ó€tü¾n+~)"Ð>äœ?ÐV]`#õàÙÏû1Âå½]'¢4êUsgà\¶…{¾:þ’ƒ‡JDEÍÅíҙД;ÆíƒgIü3õF ÜŸy•óFo$µbÇl0™~õÆÆF£‰f¥yií£°1z>Zu@E¸Dºg@: À#‘T¬í‡è‹dÏ;uIIC;œ Læ’{؈èWt-"çÈ-8°K‰Z8m"ñZ 2°‚ËÅü ?d (5Ù¿¹1ä NèÒF]ž€?á§€(p'"ìâû•0ÿ˜Ø¥ÿö­w*ª%ç ±1aÂßÛ1qP*‚W„hLxC x0 H'||pþÇû{;e÷•w `©›9<}Xúø¸8È ‘!µûºö·ìÕPâ“Ñ\*I"B’’½ÑÍ«ŸÿàûF[~^ÓMÉ./|üøÄS8íÛhýµ\N¼¼þîÇy…“;B«ùöóúfõ ÷É޳eW"'$Ä`÷K¤Ð€tÍv×l8²¿žWärÚöªÁ;¬9{»°à!‘ê$}”>:Šèu½Š^׫®ý…}†ŠCEÍ55"Zhª_|vç1C]º´²nhJœáß¹Ñã¶`èS£”íì¬Çµý»£€,>ÓúõYwöß»›ïlûâìÉñ,Võ8°Ž‹Ñß}¿W(»ÀÉR0äA‚{DjF" €‰æ^LÖͮטšÛÚˆ~’©¿c®ÑMy·ÞB4Èé„Ã-˜C}ˆ^çÖAúW2”‡ŠJÚ•ŠBRœÌ•LG÷ÍbkúÃ{¯Þ,ÃÖŒ°ó&x|éŒ>tGX´_¼½ý+-rrÙ‡¦üUœ(`ܽ[äèî_âtËéóÀ/ÑÞc’SVö㯮,#µ_/.„D@:Á(’õ¯…Ÿq™¤`XgÆ#nd‰›ëÙå†zÐ T„æîÈ14ã£×AXÐe<;NïJ¦€Q‘$>ãð«Z :=—{%¿ô+.LŠ‹þdÿi‚ËŽÎL—%Å5䵈,¿£]XÇöÆ›âß=¾8#厃]lïÃÚwÕš†ÿÜÔõu6{€š°#½Ñ{Wóòç÷„'Î_ú`G;&~ú¡UGèI­î˰g³s³³»³¯“ú× "ÌòF)”5µ´DR<ú)+°gF"HõÀ”¡$inù!Q÷Ÿÿ®òPQe…X•àø»\t Ú¥"Ô*®‰H¬áë…Ñ;aêÿ¶í¨(&RûÒ£ ‡ IøÞ/6M‘OwŽåÕõ¸[üñ·Öá¦è·›w˜ö¹5óñ¬øåGî#Èõ[eþø@m£ ïØ-š ÷¨ckƒ–à½xRn{Ì>­ßú¬·´óòò×ò þòÁ'b«Í¿oÆTWMH#¶vˆàv€‡ä½#e¡¢l"368Ò< IFE‚WÄx HHРI¾«ÈߎS@q¨¨¥V2âà‰h×¢_¥gÏ:såæýéSbµŽÊHš7ydblïÄ÷wfGvÉ̱ì>ùÚ{üÊZ4‡ŽžÏ%’¡aSô«7KIçÜ,Åß4ó"§UöñÞÓÍ­­Ï?8ÿðÙkxz4"­ckþ7HŸ„¤ëí¨H·Ä/>ë!Í|¡XiEÕïþö¿)&3{ò„G–.rù¬*b;‡9Ã3†iüöÅáòîúÜ xWpÆÿjŸqUE°‘8 ÀH"ÇU]øÛñm (ÕÔŠÇøQ‘D‰‰#Óþß+k +vùF1ñCŽœËýî3+‰á §‡ð g®Þ"Ý`0UHh ôí§%_ºD9G:F„ZÒØ†Ìš…¼Œ4GƒÑœWTñIJ™s&‡·„?üœc;·F¼6QÞî†áFCt™$ó :4lXV7%ý·|‰u¿zs“ÉbaR£‡e<÷è7ÍNlçöøÛp¨Ý—nê´·Í2HªpF¯¹·u][^ŒD FNÎqmGþÖ|˜ FE±VÜSȪ㒕×ߊ9ø«m0þðÕ÷ž¹JÐY†©S‘ƒ¿)£‡FhTä ÃÆùýÔÑCR(%ÇEq®Hw9ˆÚÁÛ©œ¸ÄÑ\u½‘tÇÖDÿ¹ $Þ¸.ô6µ~7EÝÉ·nY¬¶ß¼¹¹¦®ži¥&Æý©µ0%Ü=E±Ç»»—¾µ`GEȪúÖ‚¿–ŸÊ¡€âä¬-uí´`½IÏÉ¢ŠMÛ}zà ¢Æ¢Q Ð‚rxK‚o&ŽL‡Ísµ L„q4­˜5>«ºÞ})oÖ„aŽ ͤ}'¯€“þ²åÀOߨ–žÓik޵üéN)xãšÈ×.º¿ÓþL£Rè×6½_XZμ¢##¾ùüµªGap|Œþéø)à«P(¯((* 3 _%z¯æõÄÒ@–§¯~vðìœSF®^0 êg˜»ãèÅýï‡HÐÐÂæìÔ숡IÑd3ÇÝ…Š(¶æ¾IovC}j¡]”¯¿gkNû/¡@«Á[X@"8.ÿ~š  ¼½å³Ë¹yÌTþÍç6è##ìýsôS`ðP@qȣŮW5xÖ û™Â¸ D«ª3 ,!‰òs'௪®‘¨®ýúŸžqlŠxõ?þæ:9-ì×~ø‚¸œ=aøÔÑp’`‰º[“+ú]QÀxøPP«äÖW»p¡¼.]ö½|£Ù|ñjnqEe}£Áh2·(EÍ¥¤F¤Õ¨£"t© ñãG תÕ6òÉîý‡Oco€é÷ןzbH’ç•‹;§?ÓO?úLe¡¢6,,Ìf&.iÉø™lºñúN4…•¾\¬ç <\#Js*ßçÖœÚ$—ƽ{ÄL›ø¬°´ìãû.åÞ@ž¥‹Ö©;:Íò®g@W^Qw5Ï\So@ÓnÜð¬G–-JKNrœÅáSg·î=(r^xôÁÑY™Žwýi?üð ( µšM‚¬~Q½o<_¾;‹¶¦&Ó‘Ã̯),L=£/ÞÆ½‘6Ø6nÞºãàÉ3xºZ¿|jmHi½q"]Y„Xß}ù?^}}Áô)֬ľ›Â—®ßxkËg¢Ö£ËÏš<¡«üù~ ø)àÕP*Œ"¤òóмú¹òýÁ›Od·%۽ʌ¬ÑƒÃô¦Ñh|õíwo•”­_1kÁÔQ}ɧä‡wß´Ñó¦ŒÄöóý]'‹Ë+^~f]m}ãk›>€1ÆÈqÕ¸já<%OÁ?6?üè”…ŠZM’øŒÃ/AtðŸKãÞ½blåY#;HÌÖÔôê[ïTÔÔ|ïÙ•™©ñ.lYMøM349îÕwwÿê¯AE8–fœF êÁ•J°Ç¼4áü›·60qz¨6À£òwçÕP*’yEá~cW=WHIPcêÊ –ɶææð°P´¹=6DOwŒò»ñÀ>FÑT™álâçéѹ¾æ»ùÓí·JË$’Éø{yÝ’Ÿ½ù9¥ñQ˜9$å«O>.ü8Ëe<›@Áë£{/_ÏÃg½¼Æd&·Ó¡Erk9GOÂr<þ¾|€ÊBEzE~ Ú€>]r‹ˆ›öÄÒéôúíŸn\4}ôÚe3;Á¶ÃçððÓo¯ÐÞ5²^¸ÐRU}ªÓ†6û:‚å:}Á™Ïs‰œžyæ»vÙŒÍ;ŽGh5/?ó$N©¶¦fìÈ:æ‹8:ø›uÔü `S£Ñ‚Ú„“ 9ùp}Žœ»~«´š×+Áe[Z[Ù hf>²å.ˆB‘›%UDdÌ‚÷µ‹ÏÊøŒð[[víKˆ‰D—H~O‚Yï:~9*"R«VÊg@}c#!Øð$éà ^½}À:U‹Ð¶ûúïmkþòƒ™wö<%P¡ÍjÄ6hv%P+îªÁ(/šºbî„O÷ŸÁOã?é´Ä˜÷vfSà_¿úÈŸ4YlxqÄ3ur|41Ñ„WkA·ý§r‹ÛÆ—ó=ûÀ¼É£ÒÁ7ï~‘ îÍšyyÝÒŒ”¸]Ç/Ñ>Qc½6*#ùÜÕ[ ¨wwfÇGëhêܵ[þø‰çšlÜj¿ñé¡òšXå·Êjˆ¶&º´gã¾v›üЬ¾M³×Õ7\Í+`vÙ¾=kÇÙ1ëå³ÇÁ.‚1úhÏJÐàÛY¬VtÞ«jk•4ÓqEºI;*„¡÷Ý/?68,!º¡‰ÿVo)ÔÛ î-o÷‰GƒÌ­¸¢öÍOM5ô•õK“⢶ì?m²Xc£u0rÀIõ¦ý§®.ž>&R«Ê-¬Àûâ´13Æ+©¬ãÏqE \·l¯äKKw½HzrÅ,¼AMÖ­Πб65Ÿ»^øÀ‚É §Ž0-9nÅìñù%’`ÇEÏ®™G&h ÖÑŸ·Ä>™ê³•f›ő¤mùùM’Këð ¬Z DúðJÍÙ+Wy0Â÷áiv?5æ ƒ¤`ä¹HÄ6mÝ^TVÂÓ ÿv³Pú ‡2D릤ÿ–Ÿ) ,^Q€ŒŠ|]WÃi%`ðà¤ñ©Õs`¡âÖA\…Ì‚b `N÷¨¸²®©¹åñ%Ó1F–}1òÿú¿òAOÉï=» ñÖˆôÄ +çPë­Ï‡…×L;]QÁ¢Ì‡{N‚lH€~¦öý×ÐÎYEG¦EEhhNÌ*ƒIRœd ÇÎßÀƒöÿýÊé úŠš†Â² pkвøL»h±o窩©©¸¬"&Rëc~‰zµpÌ @¨Aìz°‹$ʽyëØÙ‹ðíü¨û„>hIÂá›?cJæT'µîëúïr (‹W$ãúA¥Wwçr^qfj‡Ç± ´ü©MŒ‘ž”VÖ8}íÑÅÓÐ"ºYRIŽ`”UÃ.ŽŽÔ OK šÇ˜a©µˆ»·Êª)™‘9¯„ö³†H¡ 7ðŠÆÒ`0£À4eôPñ² Yº$¥haÂð!fœ1NξxC£ cH\RK”$=hY|æó.­Ñ(²Ùlµ Àew/7¬¤²–ÇA­è‹·¤ÅÖ$wͧ9¢YZ*ñ·dæ8\¥ýñÃ} .´‹2Ý*>s'Ådûy6îß'Zðyñ(D "wj³åÿyËòêúe³ÇÿÃs«æL~%¿ôÇyΧɨm4¡uÍa^6š,3Æf úöÄb@¤à̃)SCfi@¿¢ Ô‰`˹F׃YÁ«·WˆBXo‡í/ïY (”WÔf•<ÉzüX„sXHˆPÇqÓ–Î7oò\nÀûq4Ëÿþ‹:öˆ š¸äùw?xÞñéÕó'ñ'2¿¾v‰H`Z[JÓ€$1#Ô†ø“«cªöäòYÜ¢k”¯EþËO. $n?ûîz›­ÙÝb( ešËÃSTbðˆÏ+àöãÖ6IkÍ}pŽá$ sô2<-¦Qö¥¼ +gO›¹óø¥K7Šà^Ì-â.9ÈÑHðÁ°ýÈ·ò‡œ¦  42ÁÁRp´9è}&“ÉTRQ ‡Œ•é×zVWó2¶)(ŒWt[‚ÖÖ¤T$? êð°º)è•ûº ˆ½#$ra_¼LÁ[ptÚ,0«›®›»!£‚ÂtÔéð’Ùj0˜Oœ`0ÁqñáãÇ+dTî†FîfÀ%e ㆷóG¥´WJ>lK üÅ’høâ"ä¹\Šùâh“Ì÷ž—pÌ3h2}9v!xT ¢úFƒ9R¦'i+„õdþ2Š¢€²PQ`Xû¦¨^K’àˆÒjàäÃËQÔâùÒ` -†Î‚àÊœšñð¡»m¶váBÆ©ÌAºjT€@gWµÙi;ÂÈ@ò;zûÀÐ’$úvœ§Í@ß®²¶± ¤júmñùs_2ãØ…äß®çÞej¸› NÓ¨Èl6›,Ú½ ^N]ûÀ% a(ÆyPMÞh8ئ 0Tt[±ÍÖîÎѳë!vh qÓÐæ 8í–gÇãýC[( ¡¶b‘qï±>o}&¦Éö?)Ý †èK4Q’ݪ$0û`·Ä¢ÃM—( ÎQ¢3÷‰Ýÿ–c¾ûÒCÇñÓ£@Eèµ:GÇ2þt7ð¬BX7óßR,†Šn»)R¯ˆMµ*F=1A@;!Ýð}’‚ƒlR ã…µë*D¯ˆ‡A "µZ­ÕjÇ IR…ývóNá×g=*n™.”„žPÚBaè,P‘[:ëG£¼RöI „„hæµ»ÍìG{þªP€8ÇBv‚D”dYb;—Î~XÔ×¥4ìkm½ÁE¥¡¢PAþ6›R,ó*Òh4QQ‘£“cáÆânqßÉ+~£þüV 4„’ЪB[( •‰Š¬.´TI¶Nê3ƒ´ÚþLÜ_×O?üðS@±P–-èvrå "T‹pðÏn‰Ë,AF67çWÖáH~÷ñËËfÃ}jG%PÅ®·†>gĬ­¬kŒ ÎŒŽÕ룀E‘‘Ðj+PÛÚ¸_Ÿ-V ýc$8Dv/‘$³vñ4=¤æâYø›( ( EDˆ‰+G‚ÆxÐÿEßNf ¹-¶ã5 †JƒqÓöc·é#4ðÿjÕ¼µ¡8*Â\V”©Š‰ÔéõúØØØ˜˜( ¡¶§gÜ¿_ŒJ{ßBÏ?$?üðSÀO—P@Y;PFƒ"‡Z•ñCÖb´^„!m"Gg0X¬¶k“Õb®0™Êìš|ö“K–æ®FšËËÚ̲BRRd_—w•èßEkcCKµ›3X%ùqí âÀ-_HP`‚*82.{‡j ‡÷Ø®JN¢ÖoÕ»EoF˜•AI¨úA±H„à .ì"Á(ân‡¡y>CŽ«Y¸Èó£ñªl?|á³CçòϾ³fêµ›¥¿Üø—/¯[2aDZÇ©ì?•c²ØVÍ›Øñ–cN£ÑòO¿zçÁû&Ëaûïö°Ç*þ´Ÿ~ ø) ( 8T *j©©VÔ $ÄFNB0Š„þµÁ`°Z­Än”ÙHnFºqãëŽ&áµ5à—§¥²B?f  ÏåíC7A:pšCááá0Š8Ð%‘²3ʸ¼ëþ7h:x[|¶Ð/>ë 9 ow£¨bÒÈtΤÍœ`˜I¸®iúܵ[XeTdýÑF„c¹W~_¶¦fùR$œŠÝ³§êþK?üðS@¦ÀלåÙ¼"ÐR[‹i΂<;ÇÞÙ°arŒ@Elê$„þ5¨ˆC8ÈÀ¨5.þ Ì¨ÆÆ„”Çõ?Ýj6ðZšŸžÞÿZX‡3HP+¡!R3ÒÜR&$jil0Ÿ>ÍŒB’’ÂGŽrššÿ²' ÈëÂvT”™G@jñ3y{Û‘#çrIÍJ}é‘…»²/]½YFþ¯7}ñõµ÷ÿñÃý®þöξ­ãÈÿì$@{»X%J¤¨Þ-ɶdËM¶eÙŽ;qú]âK¹Ëý/É•\.¹ÄÉ¥ÙqlÇE¶ä.K²zï”(Š;E±÷NôÂòÿ=,õ ÅÀ¼ /öíîÛý.€ÎÎθº¹&F‡þãŽM®W7½µï,öàrn…þ€PeRìÐ…’ñ™H‡©  NK@xR‘¿?7ðFÚ×ç"¨‰a‚ÖuÈQ á€<Äâòº"Cßmb\ä;ØÞæÒ×i]u޶ªÒÕ‹ó‰à›–ka_cš³ÀKE@éèAb!k"š°]uÕùó0tCûb²³ž*åĘЊÚVÕ6wmX’ɤ"¸ª:_tãÑ{HÞøìôžS…÷-Ÿ‡[0ÉG²›­H?½ey€¯èÏ?[X½~qÆg' Ä>^Û6ä]º~“õżØÊsÇodªƒ zD€8ÁIEîL*‚º¨§[hRûD0©k9$!h;ð »"üË.Û}jFÒÒUÝ]h?P¯÷²ªºHv9аI”•f+kbÈFì:&á•åØÚô[V¹µ}FRÑTi&F‡¾PÒØÖ384„¸f¬™«õÜÓ¯À?6/»ÙòÔ}KEÞžZ>"DŠßÙ¾¡¼¶å"”I..}²î~y[÷À“›/ÏN T¿×Žü©sLŠú£±1™ê¨ ÎB@xRQH(c?Ô#,Ó"ãO[Ë!a¿%ê‡ðÖ¸¤Óú´4׋\tRïÎIV–[Ö¶·ù6+ƒ22á­ÒŠ-óMK3t†XoG9]"Âb¿oE„åçsr‰ØX¹ž¸\‘ˆ?zÖ3 psu –JðÉØ´,‹7-bMŸ+ªÞùÅ(~6.ͪýø2•†(°¨‘–JDccù¹k\Ò9Ó*?b8èœÃ§Q‹(îš \ [ì´p2gruÏ«0È#õõØ{²"„‘º:OC˾©©VmÙŠœ•¦`Q4¬ãÑâeË!"¬ {¸»ÅE”×ݳ8ƒ¬…Šª±$Ãêhï©ÂĦ¤tÕhõrK¨õØ=‹ RÂßø[#.*z l¥…úŸ¹¤Ù¼˜¡ý1áŸN øÉ?üï=}ïiÐá¾ ‚¢bvM@pö¡£R‘uE³2å^‰Iì¹:ƒï+öAWË9ãq“H<Âíج4uûômŸMo:!Á®è–èõ;¡U9©Ÿ¸úßoìëêWäf$ s^r ýå£ãËæ'{yz¼ôÛ÷¯VÔù‰}ºúd¸»uÍÂæÎÞß½w˜«o¸,¿‘[Uéÿ˜Ÿïƒ>‰/‡Ã}WÊ¸ß váè¬Ýo½»ýóF éÞ¾m0´Wªo7Ënád¢ííÆO§4Ÿ€uE£v-°+¿ëÎv×3&† y¨³ÓŠcG Æ,1ùÃ}£2–{`àäk;r >ö™xÕG'¸¸¤'F­ËËÀNåé«U-½™IÑðœ‘_Z EÎü”ØÕ9©bîz¸Ï¤“Õ6wnÛ¸J>4ÿœu­Ð=©bƒ®U5B †ÔÜÙ‘L©þý{‡Ó¢¾úÈZÿ[¦ô4-D`VNWßÖ£ÚA3ûD0uìGôwìÓ›œD¯y‚È5‰jNP”ôá»f­ —†èÔØI4^ieÛ¿<ÿ ¹`ëóù©Â_¿õ…±ÑNBˆQ©uÐîŒ.5À3iÆCÐBÃjž‘å1 (‘†Ü"•,L‹ƒ5=ŒÆ]Ÿ‹­´¨ÐÀ+¥µÿù·Ï›Ú…{úØ©?"N3xÁIE 'Âx%kkó!¯Îê·šºˆ×<ñ›?× s{{µ¥%¸çœ9^ññNH€†ìÌ ×ù¯×÷¿Ô?<µšž…ÚVÅôp\n²‚YE†H±=zª ¢º¡ýùß·̉‚àiiþÜ9PU5´#:Þ+Ÿ8w­zû½K^x˜SÊv÷+,¶F™D`fQ*òŒæÌЇŠa %ž.žoúÃ+xø[SN ñ;hdWdQuî 3·"—ÖFT¬ŸÄÉ#N£`3/_Öï±s´ã­¥ó’NT¾ôÛ]¥7›a2#S8Ü÷Àêl왽¼ó ÷/ƒBèÙ-Ëáóg¯|z¶°*/#ûhksÓ!„ýäO¿öé)l´eÏuÆ4Jœ]8yD†Ò75z§Ì(¹Ùè–[@{ì°Œ;¨l•kHÆùãÁå~«qöÖÉ_y£"Ú>³Ý'òÐ~ÿÁÐðð?<µ)#Ñ ÑýȡΤ&ë÷?|š•ùŸv°T;ü Á‡×åâÔXï€b‚'-6²t^rNZ<™¦K½&5*L¬N@¸º" U__oõÛuƒ®·â…YóoëááQ&··kDVéü°V«¾tM¹ùûûd/°J›Ôˆ9ÂÊzˆD0a1ö‚ƒbðldâá™pƒ` æ{Ç1q¨c^˜r&ES3©‚‡ ƒ­4&ñx!‰Ï‹N‰x”˜EBÌy]‘®¡~ÑûÑÆ6ŽÓëé­XÓkÅ¡j«¯\1ìÞŠW¬r5ÄBq¨á f0—Kk±:"ȵʆ÷/õ4ãƒÃë·÷ÃÎÚÚEé07yd}îú¼ô.\@@´‘‘Œ¤è¯>¼§–̽ã8WÌ;ÔòI0£¤Ž"`O„©+ºµƒVß`O,mßWkªˆÌ{K¶·˜ÜŽ»†Îäß‚bíÿ÷Ê”7;rÒâ˜cÆë7šÙ>=Q€ÃJÛï]ÚÖÕk_ø³©ké:_tâÑW][QÛºçT!Jš{Ç1v¨cíÎR{D€8 AêŠn9YÖßÒ)ÕêÒªš–ή¹B©R¸à܃³\®.®¾bQ€Ÿ$:,tÎ-ÿúÖÔïÜjK£Ó••ä¬Ôdå™3ÜÇËÃáÏœås6ããÄ1l<339qͰS½Ã;Ü+?½yÙª…©Ñõ?ß2ð†ÌêîWàLOÊnr®5q™xÇ-0ïP‡ W"@ˆÀd Q*r÷ós“J‡ûû±ƒÖÔÖ¾ç該š›ÃÃ#AÄÊÆÏâdi×å!vtöWÕªaö¸¾òºíâ¶ÿýãϫâœru­úЀ"RÞÿ¥..¦Š(wbÃÙõ‡Ç:·Þn­qòKoâíïvŽ5+­iæ£ñVn°.º•†­®›«¼*£#›–eÁ"—µcâǸq›¤mÃÁ&]¥F‰˜*!JE "¡j ¯Ž(•/¿ü'IdØöMKàßÂÄÓÿT‡l¯õpxµá×].uœsý#çó·,Ìõ0œé˜Îxtz}qeÕC«Ì}~Ûf‚\ÿcHËC‡¦y:$„º^ ³XžÓìXKg_kWNz|DEæÆ%™‹2ðïþóñ l¢!óñ‹!}q®;kx‹üõ‹3Vå¤BTúï7öuõ+r3Ìd9¼C± L9Àg2åv¨" ' Ä¿~ À8×Ù—k —)‚¿xŠn3?µC}ì¡;žØôç3%~÷ƒ—^xÖËstCaâýÕ¶F«ýóλûúžXŸçRyu‡úú'Þ‚£–ììÖÝàÖfϤø¿þØ_><Jÿô³ìx”£Žzüqù‹E•­PUZQh~ôžEøÇ?—spü“çðÖÕ*­Ž…R‡9ÑùâðûçáîþôýË”t@¡â}çXôŽƒˆëk¥AÏÄ·l•ÆGÌsc¬àQÉ*ý¡Fˆ°Á X°wï;tcdt_ߨl»ÁÛ]ËC½£‚K\zâ·Ÿ¸§±­¬&{0 åwí?ÔÜÞ‰FÂF=É’T„σê|>ûTˆW,Nˆý ·¶ïÞ?jûbw˜ivª\Ñ!ˆÒàrýÖ~Ö4Û¿º»»[qUãoÞ=ø¯íùÛ§§¢B¥‰1¡¬ Þ˜øÎ±Ø ·}¼'ýw‚ŦøLŒ¢‚>ŸD€8a銰`×5·œ+,~êµ.N7IEÆŸ9³ÌÕËËM,J‹¶mX´ûpþʼ… 1Ñø½6.9Vš‰D5õ—ŠJx ¿V1ªâE®±ê:C¾êܨTä»r Æ > òšÅ9s¢"?F&¸¹¹‰|¼ƒ|ÅGóËV,œ;ŠÛ^ÝÔÑ‹~â”9³îÙ¾0vÐ Áˆÿ„P‚8$a銆††>?v*,ÈåÊî!Á ®o=…ëô';(&¸¸IYE[€ˆÛ›BIF³ÿäYTDuÔâ[êµZÄÙ vFhņÕu!·™èè•>j†AÆAH¡õvú!ÀÝp%†#šWÍÌCã"Cà|(3)fÖE"Œ£ÆØA€¡“€@ D`¶èŽƒ¸}ýUµ gàOR¯DÎàz°­cDoÁÍÿlñšÅçŽèô#J:ÀË1 V nü1æqzˆ2Z­¶«»§¦©…AæZ •±HW¤.(B\ 0-[ÄëÞäòšÚiÚ¶3/¼ÅD"//¯ I°Øû££WpXL˜½µQ¯0^Œcp ÈF$Ùˆ65K„@@@RŒ]Š+« ‡ðÆÓ Ái‰®®Q¤f½¼.Ç=0ï X¸Ÿ9Ve ]¯ªÆ>ƒŒ’®n~œW²+R¿ÌЉ—/6f8 ¹‚ó‰à<äBH| WbX ·»ËŸvuÁ#Åx1jŒA 0á%fçù0Ø÷H'd\`ßC¤Þ[‘€P¤"¨1t:]s{G¿/;êÂtEª¶â†l¿Mé›[YçÝ#FíOñ¬@ Ü@o|u#¬T*[;ºˆÑø<‘G8×àPWϰ!ø—ý"šfÏÕ ¸<.p˜y©ˆé¥ðŠËÛÃÃꎣì}¾îÚ ”‚J²×»V¡NN@@R´*µÚÏw4 ‡g|,>ÅXÇÒ].«}ÿàE=˜B y`Õ‚Í+³‘þ¯×÷¢Ö/ÿá ¤±~ÿÛ_>‘ú‹ÿõ…­ûϼR¨8F’ñüC«$·àË/ÿÜu’µ6?%öÙ-+Ì…'ÜÕ}r¼`^r4¬AñÖä¹â×>=õÃç6'Å„™Üšæ[Np1\^I£..Ù[tR¡TƒÖlϱ}á.G—Z 7üß<, ÁËD*BP* §¹ƒp¥'D~õ‘µb¯± W5´}p8n‹Q"Ôcò@ÒP•{2dÕ…+¬Ÿ¢åy|‡ù„Xä|ãæ ;LB6>QüýýñÁ>„ïh™ZÓ/—ï:téýC— á°û›Ä®®Pià”RÖOw—HW‘D"ÁØð  1ζ¾ØÛ³¹Of]ÇQ¶îóì¶Oަf—¿>]Rd¬70y‚çèæãã1ØÒvvð–¿…D[wÿ;ûÎIý|·mÈ“ˆ|ž/Þ{ú¾-H³(='iRa'Úzä* þ¨í“)œ-^›º&7­²®íƒ#ù{N>÷àJã6‘~|CžŸ¯èZU" ÈÇ~ôå-¬€Z«ƒò‰ý©NB»î'öæ¥"ã»|ƒÜX\]ŒuNæÅ !ÁGŒÚ05ŒEiLWÛÀI9æS@IDAT7ÑáGÏ2ÐBu‹ñ„QÌ|º"Ö2¯ŽboQòÍÏÏtô l\š•=7qË/^¯Ðóüë-BF­}§‹ tþËW~õ£¨þòKO™,$cA†¸ 9–ùKœ2dãFØ(&Y}qT*[’Š8ÈÃÃø”ŽE˜=Ëñ^a_ “hJ RÃ.}xðá½~P54¢W*;”ʶ»º¾­..A.bwW|%¡®Y```pppPP€hÌü8A›]A>‡PeÁ×ÿÌwßHަìqÖf½Ï³ð%73~jñƒ‹%ÇdÁöNOTÄ\W×úÌÏ0®ˆgoÌË6˜fG‡Iÿí/Ÿž¾Z © •Ýl†T„“¨…œÚ–.lŸúù"Œ@dˆr½<ñhze&EcßgqV⟾R^‡í9t ÊÍ…‚dÝ¢ô×,„‹9T;|±² B&wY‹'.—_¿Ñ42â²uÍ‚MËæ5µ÷˜{÷‹óðÞ‹¥%9&ü;Û7à‡oß;p;€Øâcî}ùþAlÔ×qR$Eóýp=¶ha׃¯Å'xœT4r[ôd¼’X‚WG±·)ë[»g&>fpCœ¥TtOÝ·Ô"dÌ_msgòœpìµAyðݧ6BÕ„iòºs3Ñrh ßß>=]r£ÉÕÍA¯þqǦ)@ÆöœI#°&–æL%é9'Æ3z ¿Dï— i‘0ÚÁh™|À=™ jž:·AN‚¤ˆ g¯vG2úÌ$ŒŽ»ƒHýD¢©TŠ·à`"ßÏÀHù^áSçíé)ñöÀîÞÌ8ŽšÑÙôø9"GS6%ì¨[Ùöeʘð{Êý!îrÇŸœÞYé¬A­ÙñŸöî~¨aRãG°Ð@ø½…ù Ê#„$„žÒ›œ££Ò›Ípć·©q01þìäÕúÝ®?}pL©Ñ&ņÓÛ¸¨Ü…0„ •øÉüÁ—î_2/éà…ë\¿ã¾¥¸…·9iñæwY›Â^— }4XÞ¼XÙÍvؤ{é™û”jh§@à㣗!f}oÇ&È[¯~rÒXF„©5Næ£q^¯ÃÄ^Á£gX™ŒóÓŒ0Ú4/æäÀí©™èŠÏÌd왌^hB¾EȘ”Õ í?üýÿó÷ýÅÕޱ&ìV3£ÿç!—Ýl-¯m}zËòon[_ÓÔy¶°z Í™8dì¬ ô,nŸ±î‚›9:“9ä[¬ÇL]•IXXXxx8^™¬¹{Lpaƒ‰%ìî•ï9Û/ƒ „Ññ#åE³% CãÙ!ñÁÓÌ8޲÷39š²÷œ­þ BWÄÖóÛ'“s3ˆË\*Â/–wc¹bŒ¬YùÜŒøcùei`[½m#wʪ‹ÿÆ#X§‹«›*êZ¡Cªoízê¾e¬¼ù+4WÈ„¤µý¾¥EU —Jnb‰…DÑ3 ˆ çÜKJ%0ÙßeMAÜÉHŒŠ øÃ®£µ­æÅ yBIh2Ræ„o]³paZ´20kˆ ºVÕˆà2…º¹³oN÷,\¼DzTd@Á0²ò&¯iñ.ר"™À¼~Ñ~òƒøkÂÖßnœBÌpHh’VˆT‚Xãý §êØ5Åe,ᕚlÒöÖ"@ã’¬÷jI,òNKT„òšÂɦµ¬" #­“Q†F+CDpÒæa¯ŠM̹qOnâÂeÀ¶ö[%5ÍØÖÄ[ó‹‡|®¨zçàÎLµŸ0)iN0lÞÈ!#,œúr!ÚtõûÌÏ4yúí·±èö[gJñ‚“Š !A\À‘4HEÌÌŸWUrŸ/»º˜¬ƒW¦ŒÌ„ä‡ iäàÖ¬ˆDÉ:ÆzÅ:îëÕ$בÒw¶o$ÁÈâgMYÄB™$ ©Èbwñ'*„mI¬‹†d wÎÙ »Ö䦞ºZ±ûÐ%X®HÄÞG/–"ÓÒ,v*˜ªÀb¦0ìä°Z£Ûw¦ú˜µ‹Ò!AQ™Ã\$*¨¨G>ä§Êú6,ÒøIjlï:gí¢4£qî7ßð#N7`kÌÂ]C “yzºC_åáî†Í»·÷3iäìµ*ôçk¬ÁFÛÏ_ý j¢eó“ñ8‘ÃigȺʆƒWÍUƒ@†È— FÇÈß²JÂ'oÁÀ®OÑ”úÊ5^*šŒCgE• oí= û-0‡‘;Px{qÑ7Í!ãN™Á"ûþÙГVÖ£(áÕø²¹­ `½Ë*žd 4id‚5Å¥#*5z(^’ãêaYJ6î¿s¦ñá„Æª ¼z{{Cƒ5Iã-™\L0bJ#¦Bæ¬O(“Š A?ǼFA?1¬îÖ9°ã¨ÉbÇŸ©‚r45ÙþSyáp•_ù?¾»¾ûìvqÊåòÎÎÎ]Ž…I¾ýäòÔùÎÿ÷K¼ üÚ³ÒçžäóY¦E=ý ü.ÃÚz,ã“*øò`kl`“òx ý ”LAÂßÅßÈÐAp±xÅpßÕÅêVÅb18[ÂŸÛÆn“P¸»_Ž^$BŽâÈÉ®ÿø-; úÖWXƒüëŸ?8ÖÝ«xjó؇âw‹‹Oð„›ššŽÇD†CfÅÚþñ_™F*zç+^pet1W.!ðÀ™CÝõâÎlkt“òU¡Ö˜ ˜,d‹ÜrÓS/6µb¯bÎÞîÜæ ùÈ õÐן|Ä„°É'Ù¼¢rfë¹ÆÃÁO±|¼5.`/i^ôá$£[— :1òPOOO{{{GGx ]Z?$rÕ s›âã8j²äMMI=GMáü NDDD 38Èñ“my¬òBøöÕ7ÊŸ>AèŠÆ†wÖ¨gsƒkÔ‚906ãT7¿5©¥šU·è=ˆ¹ÕA‹w‘/ò¾ãKh±d&^lâ» o>Íê‚Q{&ŸEœ›J]¢¼…L*Â&š‰T[õI¹éãܽ@ó4™ B­‰H„Ú“…l±‘ñ!ãp'¹¸xgÌK$šÌ8œ¨,“œhÀ³7TläÁ¨9Ä#c¯Qnnœs„¡áµ9Žš,fÁ:ššì@¨¼@LnéšùNã<”{XÈPg·¶¼jæŸ.„'â :ÿ*×ü2Þé´ÉºÝå-è{•kRQÀã[­Û¸`[ãOŸ‰ÆÝ>lÿ©cÎ@[{PuÀ®ˆíWbÈl+Ç™œäeð¸Átuvª±›Âm2YS€tﲬºÖnxkD¸YÈ~Ðbbœ¸RFàÜùrY-|jÏaÞt⢙".Ûù¢Mí½&Uð8ÕéóÃr‰Ü 1íù>Ø.á‘ê…öµ×Ëõmüƒ,B–J0Lp«N_­Z·(î.ßüüt|TÈ|óÑØˆ`±rg[W¿\9/9&)&¬­{²ÿ,$Ì'Ë2 hÏ‚‡"Œ9dó*Æ#ëÙÅËéô™1{JÛˆüv‘ØF4IÎp±ñâú!@à…${š?ê«ð؇®È+1Þ=HŠÃùÚë#z=öÔŒI"|9îØ¼ _ Ä"…Ȃ͚ð ÿò›-ˆñ~ðÜuè{ Ì(»ÙŒZoï;hö/>¶Ñé!ŽÀ‘4ì‡sqÇ š`9¿RV õÿü6€”jD4“)U¿yç‹EúOÏÞßÑ3€Â¯~t1@~üå͈Mñéñ¨=ð\Ö+¥F[oXãòüƒÐQ!Ó¤3žînx4¤ë7šáÐÌ« Sþù!Ö ßÖ{Ybf^¡.êã=Ș/ˆ\¯Ë¹où|.®j4žñKUŒç¥åÎÁ:.Ñòñ}°2ôJˆ Î@À>tE˜ ŸEœºhD«Õ”VO ¬RÊk[¢Ã !¿¡­+¨Ôß7,(ëî‰Ëå=±uÍBÜÚ¸$ Š"(“.—Öþâõ½Ðñ\.½‰è(»0—A ˆx u„v­¶¹+*TŠ=¸äØ0„?KOŒ†B ž{—̓¼õŹâ7öœéì“7upâ‹u§Ëç'×µvAêbB7éLae´Üˆ‹*سC<פЮ¡IS\†ºÑ‘>9ܪ?c¤"íÀãdŸÀ91$Æ‚ÄÅÇ€ÖçLa5â£Á@ ¡W~ôÜæ…©s ÷¼¹ç öÔÆ‚ŒŠ5^k[:ñ°ˆàcÈ&“ÕÔÑc3…ê ÓâñŠËòøó2ØÓ§«â´€žñ±ž‘á†è…"@œ€ÝHE"ƒT„ékâ 7ÅbŒ5¸¦©–(؈ytý"È7L"Á‰§Í«²a°Ò; ø—?~„ hßܶ–._„B]lå NûÉ+•¢Â¡ƒÅêÛ= (®n„I ÚDÙ@É—X‰Oß¿ %ñú¾ì¹±?øÒf˜ #0Øèζ‹ëü”XØÕ¶tAÌ‚‰ä¤[}Ä‚ÍiŒøÎ0M/¬š’cñ„[ˆ)kRÅXQ4ÚaϨñÊ%èÕp_¿ÂpP,È ññh~iLx ¨¢Êÿ½dïékÛ6ä=»…3sȘ/&Ð@f*ªjDßœôø´ø(²¥Éb˜oCÆöäHµañ \¦Yîó2jgÍɧí³QRô?"@ˆ°4ÌÓmÓ¢‚¢À¯>Ã϶”¡ ‚©õoÞ9û¤3“¢q7<˜[,!­ÏKG"(@)ÑÚñÑÏ=°6C÷,άnèøÛ§§`Ý‚½é„¨¦öØP'F‡¾úñID¼_“›–“Ç? è3àÓ6ÂxnRlØãópêíþóO_­üÙ«Ÿ!„Ù—·®„”+ÆR‘Ig`… Éû‡.Ackyv vߌ«à´|ÿî¹îî~›7w`fÒÛV½„g |°ÇoËÆ± £„èçžØ¸˜‰n©ñ{NþøÂÔzÓ²¬ùsc!™@F-¯0á:SXuèBI\d0´eÆã2Ÿ¬9‘Á&±Ë ÑjÈ’Lªϋљ|Ú>3fOi"@ˆ€Sp• &1‡ùˆî» ’˜‡sgSÔüô7ô Íðgwð7ñç•°=„,–cÍ'NhC áåáó¾ *"Þ"N3ñ9,|,ó°2ÉÇ[ˆ/h†Æ|ìRdâ ¾L© ò÷¸^G¦PÃFÐü$?Zëßùqß«o!!¹ÿžÐý>ã\çÞÝ«xjó†°°0“ˆî|-žpSSÓ±‚â˜È± óUZ^øÛcŠxù?E‹¹-ȉ@F1¹R£ÒêÂýxcA†ŒŒ§ƒºÅÉšd‹U`šÖ°yLjZíæ'™³ÿ=¸ãŸ;Vꡯ?ùˆ a“OòXÕ­ž?[ϵú@¨A"`wèÛgwS6©ÛÍF… îÜØ††5Å¥&ƒ„çh€Æ‰PÛj›LÖ`T4‰PùE"Ü ‚ŒE"dâ-|ñrrîz¡'E"˜òÈ>úœUØñØ]Û±Q€í°–>øŒ%&%±­’1б ãØˆÉtðc±8YS€l± ¶ áYˆp2‘ˆï%ˆ DÀ± Ø•TtË´ˆèés#?t|4ìŠÅ^ sfkŒ»æÆ™F©ó uµ ³Õ [<ž»Y³¢ewìÜÙâYÔ& D€Ø{’Š|ræaû pUgógeãoæ!Mv}Ê$}zÛ ³ Pj„"à@ìM*Z½ŒÁgçÆh"¸¡È÷lmGÂ';S¼T›;þ?ä(E—TçòMügr=¶Ã‹?“/&—Öv8}Ôe"@ˆ€M Ø™T7Ä^É ¢¯kÔ·pþæÖhúßAø/ d\›øåQ­Uß_ßH¯¦ÜaøF/,Au7i€Wjò”Û¡ŠD€"àìL*Âð›h*ÇÚD“}´oôèÙò<ŸyœçI\~Ýçaˆ‰¡¹V¢Ê¿*^M­šÂë#:êbûÌØwÀÔZ£ZD€"à`<ìn<âÕËúÿ¾ ÝVžÍ÷ßþ£±‹ i»:<< ëÙù\b©–~íYŒB k6Ç^…¾²£ó/£Ÿ]¯¼¹pž@ú6©‰f}–»<¶Ø£¤Óg“ÂG…‰ ÎAÀþ¤"ï”D÷ðP}{§âzYwCkE á2à'þ”íT$Â'MW×8<7ÛG¯“F„$É´Yr?_‘»»;Ör¶œÏ𧑆FF† |U—‡ßX´L®Õk=½¼ßÚïz§cñîÛ”aHÓ©öž·H¢Ó¦Š²å ?_ñlžò(¨" D€ØŽ€ýIEá¹bIíáÓÒç7½wª!©ŸñÎ(tV«ÌwbœKbœJ­íPjòåósÊœ°-+æÇG‡zxxÀ´Už2ÁF@xppP¯¬oíLodë1Â:^¡Tî9UTXÝ"•<¼&Û!ÿúƒKç%n¿w‰·—KÕ¶þ`PûD€'!`7Rl(0º{eoî=ÛÖ#hí‚%Y î3«D™™Ï„<¬Óy™ñù¥u_œ+iëx~ëª (lª c„5mgwß{G®tô)Z“í,»ú¿¾m] ÿä"ÙÍÌçžBˆ 3F`F·f¦<*¶`ËÊ7>?Û+S}ýÑÕËç'9¤HÄ#Âè0FŒãŨ1v…àÀ°n‚V«5½};]Ph¾áL»ýø¤Z£µaëεFˆ ¶ `R¬~¡ÃøøXA{¯ìË.Ÿd l#Åx1jŒÀÁFd„årÅÞ3ÅýJ§ƒüÀò–®þŽäÛŽ°&Žš%D€+°©Æ¿°t©il‡™Ë–•óœG$bÓŒñbÔ;€hXqúYSŒ°R©ºÑÐVÖÐùÀ*'…Œ-˺æN[¶ú”QƒD€"` v açH£ÑÉ/ ð…™‹-(¼MŒcp «÷–Æ&ÝÙëµÁÎ Y*Ù¶Ø„­>eÔ  D€Ø‚€Ð¥"üáŽ#â=}òúöÞU SÛ–h¬ ƨ1vа®2ƒÆÉ¾®ž¾Ö^Åjg†¼ ùFSç€\e]ÂcM+å"@ˆ€Ð]*‚‡N§+«m1· ߌõcp ëÚ¾0Â*µºº±“ ýxÉ&ëž± =ˆ"@¦IÀ.¤"}G¯L*9˜_¢IÍÆàÓ"ë®Ù©HÓgÝʉ³Cöµv÷[—ð¤&š "@ˆÀ,´T„cÒCCÃ:½^¦ÔøK|f“ à`b­ä–(îuFhý!< káÈrBv*‡úøg¤À` "h§Rkf„=„;ે/àYôÆZ*çÑõiFV(í=²êÆŽî~›b<û)ZÝmAZ^­Õá.6³X‚•Ä[ãb,Óú¯6X°a¼Z¿·–Z4‡ŒR@Î|°GhØÆžȆO¥¾ (Oêï×/“ ¨CÔ"à4ðÕÃÐi†ëtµ›è°¶žHB;æ#+{PJlØŽû‹¼=ÿo×qÈ ?ùÊýÈDzý«wŽHDÿ¸}ýŸ><ÙÕ§xñ‘UI1¡¸õ‡Ý'^þù­+lÝO»nß"d˜òÍ/?[Tó­Ç×ÄEc€ûÎ\¿TZ÷£/múû¾ ÙâŒG……öôЯ³E8”IlG_:|õ¢Ã¸Ÿ}º’€ uELy€W[ë1 ÀØ}äJWŸ|uNÊ7[½(=Þü>?]ŒõóSbêöNZjîìSªµ RbøÂ¾³×qŒkëžÄ3±Ê³øÖf2Èx­ªï`ƒªj舓HØÛ† Æx®í?tlpS|—šìææZ\Y=ÅúT)À—_½¬Ôä)Õ¦Jv@@ÐRÑŒñƒ¸ÓÔÑ—•½eż„¨Ç7äF…U7©µúù)Ñè[°+ Ë6[‘ä/†¡‚òúë§]?hȈõè'†0„iŸ\•}Kô$È'ÝW$ÊLN:vál­Ïâ)“|Ýð¥ÃW_@¢á¨H*âf¶³3ÑH‹ç§95ŽKC{è/®lhÇÛª†ö9†·¬XP€ïòyI‡/–ÃØˆ¯H‰±ŒU }BlR¨µL½-ä1€>¼qmGwï™+WǸOÙD€X™¾nøÒá«gåv©9! ©ˆ› 7Wîœó ÑŸÝ, M)òç'ÇÔ·õô (›;ú²çÞÞ>í KÒ°ƒvüJÒtOà. ʡꆠqAR?1ßAæQ'b##Vçå|tèXmS‹q>¥‰°|Ñðu×_=[´Om „IEÜD„q Ø›œDƒ@ÈåC‚áÑó%HÏOæ6ÔøKäíµiiƹ¢Øñ™”°H`|È1aAþ¾Å7šëZ»MDO‚l‘'2ŸÜ²).*òÏ;? Áh,D”O¬B_1|Ñðu×Î* R#‚%@R75Ña8tVv³õƒ£%5-8úÔÑ+_6?ÑË“;£ Ë_ßÒ›­ñQÁæa–d&„J4F§÷;Ù³Û±ñ!£oÙ)Ñ•õí#Ã#óî=q‹ [œ;Oo=ýxXpÐoÞxçä¥+dcd‘eéÀ× _.|ÅðEÃ× _ºé´Fu…O€¤¢Ñ9zbCnbtHae#Îçcm^07öþåYüü13ÞØ…ÏG»l®šoœC鱌™™±'D‡ ™I ÙÿÖÏ×÷¥çŸY¾0{×þÃ?ÿã«§/_%?F<JéÀW _(|­ðåÂW _4|Ý¦Ó Õµ $öŽN”@_{d6Âp?˲·§ñüÝ»,ÿŒs~ðÌm=jrlد¾û¨ñ]J[$0>ä¨P© F‚l£I&þx}æ¡Íkçì9zj×þCïí=, €—9r¿kŠÞ €÷jæ—áÄÙ‹O>J¶DDçÅH*ºc! áßYôÆÚ²µ‰ríáWû»_Ú®T«K«jZ:»ð›Ž_vøB·Å³¨M"àÀã,4(0%~\5Â/Âw๶84’Š,b¡L"`—ð ¾dÁ<»ì:uš" dW$€I ."@ˆ  @R‘&º@ˆ D€€€ãHEð¦ˆ@ë:ýh|{xÂ[cÇŒ¡FЂIh³¡a.x;œH “-ƒx[h|²ýœìS¬[¾g@ÑÙ;Ý€í³É¨‰³u§‰Z#D€Ép©¨³WöïÛÿÛG±Ü‚B[ÏÞU5M„ÈÅ’ÚœêÚæ®{uoU=߃¿ÊkÛ~ö×}½|ŽˆqÆO]µïØ7›»^~ïØ¯ß9òÛ÷Žþï»G*êÚ&…‚çŒZ¿xóÀ§' «gc”&D€™'à8RcׯPŸ)¼a‘£L¡f“ñ]x肊¢¼¶µ¸úùiphh¬ÂÆÕ¡æ™¸Wk“¡%â5[Æm 9}4¿GU¿ûĺon[ƒÐ¹»\áUhAaÎÔZÉÙ¤g"§_¡2Ñá0N;gãáPš"@fŒ€£A ‘JN^­Êˈ3&ˆ »]PªîlQzü#ëÔ4u½óÅÅÙIù¥õˆ/ÈȈËŸŸ[“3 «ß=p kðºÜTÞMÑù⚊ºv¬â›–d¬É‹í¹÷]¾ÑÔ‰ò‘!Ïn^ ÿ×-€ÈŸ~ídþ~×q_Ÿç·®Ø{¦^³ã¢‚Kn´  ì“!D|y] k´zs?ÎÆ=TâcC[/|#\.N׿ðЊî~…aÛkh"(bÃyÎ/<´Cë‘)õÎá>™ @žßºœ Ö„32_©<~¹bhxÄÇËãÑu9˜/Çæ,¨I§Î"@œŠ€£éŠ–ÏOô{¼Xf<‹ûÏ–xzº?÷À²󓯔×Öæµœ/¾‰PíK³âŒ+ýƒ«²Y-¬Ór"C¤' ª a™m½÷-Ë„4pøR¹V7ˆvjš:·®Î†”ƒò' *QlpxX+Ä,4Ðpp™œžCبÛîÉ‹ÈK%µÈælê$ DÀ! 8Ú›$ˆA‹3ø Û¼" bÊ«ŸžÝ.¬›±¬ò·X"->[Boï¿d’oüª¿í9 ["ìyyyz`K.:LúÁÑ‚·÷_„…ÐÚ\ÎLûA~bŸONâÈ:$ãê&éK3à;àµÏΚä ù-ľ/mYŠqí9Uô—Nkõƒ¬] oâ(¦À[“r¥¼O¬oíÙ²r>ºáØœ…ü ¾"@›·õ3ó#üã»»ñÐï>»=Z¯×ËåòÎÎÎ]Ž…I¾ýä£|EgWÏîc…R©ÿW=¦4…c#F0l+ø+6ÚôúA¨CÆiú$ìÁ—°cê ±+væ xb§ OásÆjP«ÓCCù± ˜äÿ}ß…þ~Ùö 9a¡Á~~OÏÑ®þùƒcÝ½Š§6o óóóãó«ó„›ššŽÇD†0Ȇ|ŽpSsëñↈðЉ@†C˜[™ô|‚(¦Æ¬`±UÔul\6â ÈZ­ök¯6&Œ'²B=ôõ'1!lòI6fNi"@ˆ°;޹ƒf> ØöÂ^Œy>ŸScq‡Ï7Nø˜ÉLf2 „!X×+ÇÝõ‰cÕÝ|¨ÊðϤD15Ît‡Àê œMÓ["@ˆ°5ÇÜA³55jŸ"@ˆp<$9ޜ҈ˆ D€© ©h*Ô¨ D€"àxL­Co„0ÔE°Œ –(8,6åâ(8\éx{9>±) "ÈS€FUˆ D@hå“3mÝŒ»·§Ç†Åé«&[< ÖÐÞSTÕ¼>/§ëÍçéÃc8þ¯Ïo6¿E9™>D€"àœb .Ÿ¾oñÃk²œõ‹ó%çŠkØÌát7æ 9½ò ×oª5£¡'Lîò“mèÞb14‹|¾ UÁ¼6g:Rڦʩyôy&³ãH„i,D€"`kޝ+A¸ŸžŸƒÄ⬄Ÿ¾º÷riýª)XqðB)ÖQxz|Ã"oO÷}g®£Ì+ŸœþÇíëËjÛŒï"„na þl*ëÛq ýÙÍKbÃLA1„‘ÿøxa}k7Š(i—dÀOÒ»ó[»úák^¡×/JESŽwÙ2Ðí<˜ß3 Ä(‚¦mZša2è''®TÔ#rÈܸð§î]<ÝRÇ› D€L„€SèŠxðZ"Åú aèØåЬ¤¨ï>±ÎÓÃãðÅ2x3B¸V”|tÝBxÇ1¹ËZ€¾«/|[ËUšUæ Ø©«U]ýò}éÞÕ9s¯ßhAIèŸäJ ¼#.JÃsžïC&¬ ˆÎqº½o<¶xO^­ê“«Ì!7vô^)¯¿yÖŽûßhìg‡dKƒ"D€›p ]‘1ÁÁááàÎEòwžX[ZÓzúZuw¿ÜÇÛ n ýÅ(ä5ƒÉ]Ö¼8>¿uÒ2¥¦¨º óbP™(TZÄIŽ Ã %Ä CƒÐ0!DªT7´3ÍkÓ!_­¢çÃk”Õ¶"ä\uCv!ûd*sȧ¯V#‚,náŸÈÇ ‘é, Š"@lJÀ¹¤"Äíì•AåЯPý~׉ð ¿%Y ˆ`ßÖ#3¦<öݬÊX}¡1‚fh€ÌÙ¼|vÖ®U5AlB˜Øï<¾ab£C­زb¢§?ËñÒÖ… Îo¸ØÜÙ‡Hs¹éqˆdb&±ã JB®uuq]›3×Ës40ˆãá¥"@ˆ€í8Åv¾ ÁØßøüüÐÐpVRtO¿f¹0RIŽ ÃŠkÌ·g@ÑÕ«°xW£DðvD~Å?¨‚°›{ç‹‹—ËêZ“ Ó4 ‘(*TÚ+S¦Ì óy•×µaƒÉøq“¶dww·–®þ„¨LÖ­³„#&wNx¬¾pÆ0)6´¦¹S©Ñ9 X D€Ì§ÐaÃëӓ׿52Dúä¦E©qáPù°ˆ÷þ¾>ZÚzÒuND6×ÞÚñ‡Ïn2¿‹)‘ˆ¼«;`Q„(³krSbÂ̋-›Ÿ´ÿìõÿyëB»§'Df$DHœ/ù¿ÝÇq(mÁÜXóüô¯ûÎ\»qך½²Ÿýuß•²ú‰W¹k›NR`âÄxÈà Úp7å$ˆh˜D€"`uN$ (ÔLmsWˆõ@|fSG/Zˆ „ÛÀ±4(Œ[MíœÈ؈ ‰T¹k¯ì¢€Z«ƒç§‰t•çÉVkõpÎÞN„˜ä>m ‘J žãÛ¤ D€‰pŠ“ùW+öŸ+ú®‰î]–±67õå÷޹¸Œ¼ôôF¸ºþí{G±”>~Oî¯Þ9œžÑØWɚř ¬]×Dæ™Mœ¬ƒ6ßÞ~qì,#ó.È.ˆ+,¯ky{E…ÀËNdpbšâîXUnUµïÿ·÷È>:v•9 OŠ ýòËànñOž\“3QîëÛzþöÙÙ‡Ö,¸~£Ò·—GmsWx°ÿ‹¬‚ó§êÆŽ#—ÊéWÍ_˜;Ȩâçëý—N#RlX ß×]å´Þìû“D½'D€ÌÇ×!LV묤¨ç·. ô;|©‚‹ÔOÑØ *z”÷-Ë„rº‡~™¡Êà°äF3ÉIEp¦ ׂ¨‚eÞxúN\©ªiêDƒÙ)15M]Q!RĬ`küXUŒ«ÛiC{ÿÐehz -J»ÙÜu½¦®ƒ 2STGˆ”ß9©s õɔ󓣤ÆBÂþtK»_îs)*=@Ÿ˜9d<îš´„ˆ¸È Î>9¤[;…IÝ&D€Ù"àøRÑó¥A¾¬[˜‘‚-„3ƒrHƒöCC' *ó2ãôË0œUiËÒù)1Š˜[jË™í½±àêzËÊyAþb…êö†‘\¥¹XR»ja2ÔQ‹3ã1©±xm»ÊlM¼uŸ 5ä›G×/DÄpE㈹ …<^ª5ºúÖˆPuöÉ}Ã’ô¥óÚ Åàw&Ò[†Hº{Nå¦ÍÙvOòÇ!f2tTØ{{lýÂ{òÒ2„ûÍ‹uK­"@ˆ€£pp©k0vgzŒ…õhîìöˆB¥,¢çŠjÔý½K30»€°¡ÃüV#)Œ,fB±„%Rî"¬G¿BÌ8Ên¶â‰l½‡òcÃƯÂ×µëDéÍVD½ ãDÀ–.N¾œ†tH€D¥Õ½\i ñq™yq rŒ»›+HB‡ô­mk2“¢³ëȦ½›,dÖ2TMh¹¡½ˆ‹¼=/Ȥ‹"@ˆÀ] 8¸]ì~ õié쇺âFSvUžßºR°T4ǯTmXœëlý`Y—…ÌnH:Z=6t,f²Õ’V|dð©«Õ^+²“Úz.•Ö­\ÌÂo•Ô´`C:*´†v,V¹ëÄØW¨…`^^\ÝŒðpÀ²(#."8C@ Ö¢êæ¡¡!˜pá-Pøxy “¥!Áîêõ=çF\FžÜ¸¨19úÝù0-²H ",vÜ I²¹ôf Ú¼ZÙàß;/9â/rè"D€"0q.aÑ…©ïçJ^ùä4”›–d z+è„Jýðê'ö†Lƒ$!ìã@¯ƒô­ÓOA3Ëk[!fA®zí³³"oÏ'6æâµ¢®­°²qãâtì¬.¬†„TQß.•ˆp [u—JjÍ«àAŽtÁ¤º¦±óý×1(¾oytE8†-Ep@ØÆ„B¬AfGlé¼dÂ4ûÐŲ_¼yòÚéúþ³×͉/¾ ž(o2ŽûA#Ô' ôPYm^9u€^‰ D€Lœ€ƒKE±za L|°Ÿà +†&Ð_ü«ï>Êc‚6ÿvaêüc·Ì3£CX5wq< «8T#HCàå€<³©_®’ú‹Ùžy־übëê__ØÜÓ¯”ˆ½#¹®É‹ü0¿¿cK{yzüò;°ôºE©0í‚™&ˆé,ûñs÷²òÀnù'Ïß»0Sj´~Έ—ñ¡W"@ˆ˜2Ç—Š€nl|‚85ƒ/fdÞ ä$w›ç#g¬* Û]&TqaAœn —DäæÇ"6dä“HdŽ‘rˆ D`‚ìÃÚšSÈÐåâbS6mÜ®fHØÕtQg‰ Ö# h©ˆí§àÕÓÓM¥¹}Þz÷§–@x&Vé:ßn®ª‰¹¥¶Ê£…Ù\}Â}cd„ÙIê D€؈€ ¥"~Ì"O8èãß:gÀÁvc÷òpeÞl÷á·,S¨%"/á÷“zHˆ ¶ t©²ã ðõÆ‚Ëì¢MŒÀ±bŸYƒx{¹(5NnBn}[25Eˆ vA@ÐR[°aBì[òº6»`j‹Nbì  Á.«<…'ìîîìëI]]]’¢ƒ­HØ*ÓD"@ˆÀÌ´TnnnX°E>žA¾žg®Ý€«À™á"¨§`Ô;€h€‰»Ç£UØÓH¼\Î:5ä¨ ‰¯ÈǺ„­8YÔ D€Ø”€5×W«w”-Ø^ž^)áÄ`Ï/­³úS„ß F±ƒ8€†#cžž‘¾®=2§†œ›e]ÂÂÿtQ‰ D€' h©Ðaxyzúøx#rY¨Ø ^-:Æ‹Qcì  & ÃOátÆ„Ñx€ØËßcÈi!Ç…øÂ¥u Ogv¨. D€Ì0IEfk=ô"^^ž"‘¯¯8-ÒÏÇ}äï{Ï;`„‘b¼5Æà`bú)1ãfZ`ì÷Æ„ÅbQŒŸ›§ËB–x»/N“°E=?t‡"@fŽ€ ¤"¦ùÀ+ö‡šÕxô†5Û â€ŸÄ7Àßonˆ÷ÈîÕON_¸~Ó±mŒ0:Œ#Åx1jŒÀÁËËËD*1pãÓãÓü]$ UC6&ì'‘À¬&J48¤×:dw—¡ÅI¡R?‹„Ä §C‚‘äÁR‚"@‰€ ýßL“ÈÛ«£¯ß¸" _<==Äb?•Z­Ñhç Öõê>?]|îÚÕ†`¢þ‘q{OãlŒi`U34„m%îd¾›+§@òSªÔ:­\ç¢UëzTòNVa„¯gW Ãj s!wבPÏ?/7d?ì›A$ ”øƒ8˜(ŠÀ +5¸!Ÿa´8lž0öËBü}o´÷C‹°§‡J¢×kõ:å ›^­íVɆŠÍž »º¸º¹Žx¸º„{»ú¸CûvWÂài€ìš=a‹Ø)“"@ì‹€ ¤" Ãzƒ¦Áߣùe+Î…[%wËÇ{d„[±„ãp:ÞÊE •Jí§Óã&wñUì.qKpq…æÒ Ÿaåƒ3è0 ÁªCãqAH+7ÜB ÆwMÒŒ0èÁ`ÛÏÛóè¥; CX§Óû9a|~&J ä¨à@XuOØ8½%D€»# ©È èpnx¼½½S¢Â.VÕ-¬Z»(§‰Õ+α¥Ç°Š•(|•J,ôƒzƒ‰ÉEv)a\¸ îø ‚ͶÌ`é‚W¤‘(Ã3A”ºúäËRÀ Q$ ðicžžž‘þ⪮cÈh™›æ!?¼zéø„yÔ” D€û% ©K2‰°º‡„ù‰?:z%.2$!:”'{kÙæJB•"‚¥‘Ÿ„3¿ÖhuzöÖ 1]‘ÝiŒ˜¬Ã ü3yA‚ÉæÕ¸0^ŒšãÔµtX¸á2/Ãf³bx•JÄ È(cŒˆ0Í…ƒ|ìJ\XpTXÈø„oס D€Ø-Ù‘Š`Þ1r§}ŠAÖñ‹Å8ž~µ¶éO»~gûFsÁÈ öà´J¾¾"-$"ìŸÁ›39âtE˜û•Š˜®¢‰§‡‡·74bp(ˆw4@X­ÁÇÇà ¬@ ÜÌOì›|& Ëd²©om¯Â2Œˆ0CÇAþà¨ÄÛkiVê8„ña6AMo‰ DÀN ÌŽT$ùtõö#ÚöÅ8cÿ´ÈàŠÖîß¼sðñy«rRmŒ°f£$dNc$ò/CÎ#IDATaZ"Xas2‘#Øqqß 12¼rú!\Æ  ýaç jow×ô¨°1pT1.i’Æ]ž°ŸŸ”lÑ:}³\cÙÉ ƒYìå±$=)Àß,Â*µ&4(Ð5½%D€;%0;RÜåݨo4F†•ÚXÐøûû«T°Ò̬ëêß}8ÿx~ùÆ¥™óçÎáë3YÁ Òð`Â/ñ ãÆ…ŸæE64öjÒmœÇa(XIwõ˼ÜB¥A°Ã†Xäãi¾  &UŒßšÖét±.#m 9dç$ \ÆC|}²q¼/xÂý2yJücΔ&D€û%0;RQTXhOÿVˆG<;¨`CƒH¯ç¬§!ß@EÔ3 ïR(wºôþ¡KŠýı_ÅI •N‰°`Ck$öp‹÷÷ ð æÖls?Fɘ†ôÈrµ¶O.'ÈÀøg¥&Ox>©  D€šÀìHE¾"QfrÒ± ù+s††'Û¨=Ø.–p¦ÛËåØSóÓé ~‰ìÞ~ˆìÆ›YØ&ƒÙ/¬‚°q„ 4)Ò 6¾E‘ñƒÆ!Œ 5ƒ_""G8m×,׬q&YK2ï0Ÿ0áãÄÌ«aK„³'·l"-‘ù§ˆrˆ öN`ö¥"F°©­}ÏÑSe57á¤:XÁþ¯í.õßÀ{5óK„Cø8q†SdKäÓJC D€X$ ©ˆuN©V—VÕ´tvaÂjd+Íâ(“ØŽbœA:‡ŒWðKD‡ðm‡šZ&D€€°¤"!¡>"@ˆ ÎI`Z‡˜œš"@ˆpH$9ä´Ò ˆ D€I ©hÒȨ D€"àH*rÈi¥A"@ˆ “&@RѤ‘Q"@ˆ DÀ! TäÓJƒ"D€"@&M€¤¢I#£ D€"@ˆ€C ©È!§•Eˆ D€LšIE“FFˆ D€‡$@R‘CN+ Š"@ˆ˜4’Š&Œ*"@ˆ I€¤"‡œV D€"0i$MU D€"@’IE9­4("@ˆ D`ÒH*š42ª@ˆ D€8$’ŠrZiPD€"@ˆÀ¤ T4idT"@ˆpH$9ä´Ò ˆ D€I ©hÒȨ D€"àH*rÈi¥A"@ˆ “&@RѤ‘Q"@ˆ DÀ! TäÓJƒ"D€"@&M€¤¢I#£ D€"@ˆ€C ©È!§•Eˆ D€LšIE“FFˆ D€‡$`gRш^?$—èt94¨Y!0¨Sêµ#ÃC³òtz¨íŒŒŒ(õz…N‡„ížB-"àH<„?È@Š“'”'Oh®ut°»ygfŠW­–lºÏÝÏOø£  Š€z ¡¥|gWÝaygñ N޾¹ºº‹S‚bWG¦=·^P½¥ÎLŠÀÍþ¾}µ7.µ5ßèïÓ ¢®§›[b€tqDôæ„äù!a“j "àT\…üWú&ÿìÓÞ¿¾2ÔÝ=Ö¬¸úúJŸ}.ð¹/»zyU†ò‰O@«ì¨<ýÏ-eﺌ ó™& ¿°ìŒu/Ç­3ɧ·'Ð$—ýºà≦úqú™ùÏyË2‚CÇ)C·ˆpZÂ•Š†d²Žù±úÒʼnÌWrJÄoç3‘ÂTÆi tÕ-Ú·C¯î™„¼—ÒÖþÚÕÕÎv™'24‡,óE]ÍÏ.žVrÊ¡ñ/wW×ïç,ùJföøÅè. NH@ RÑP__Ë‹_Õ×Þœø”`O-êµ×½'^…J:¶ªOŠö=52|÷U“Ç‘úøÂ­»H0â6±³¢ä—W.Lª{_JŸ÷ã¼å“ªB…‰pxBü;xdp°í¥ïMJ$Â< õö¶}çÛC?g4À)èo»\¼ÿéI‰DxJ{ÕG'0…ÇQ•™$p¢±~²"º÷NEÉ»%3ÙOz Â' D©¨ï׵׋§Àn°½­ë—¿˜BEªâ؆5Ø8šÊÑÅú‚ßw×wl>v=º>úÿ]85µ!üö꥚þ¾©Õ¥ZD€8$ÁIEƒÝÝýo½9eÖÊ£GpTÍbõááaV§×›n è‘?44¦í­ÅÖ(s²€~¶ 7\ý£ª¿v²æËWœøþD%tõö•×Ôöô[M[É}XíØ_ÀÌÌø_Нè´üdM*¡þÍÕ1-1éøeàÿ Yg.Ð~‡Ìû©Ñjµvès„Q2å;%àþóŸÿ\P]ïç-õ•ËÓéÒ°B!ٸɼ…’ªšÿÓk…åë–æßýõkoïÚ(:"4*ÌAŽ¥(Õê‚’òȰP7·iI½×Ê«Ü\Ý$b‘1®)§õÚ[èO\Tä”[˜ZÅ‘‘ák{·ŽßO­ª30f…¯4i¬êøhat‡Î\(®¬Æë©ü«Qá¡áÁAc•Ÿ`þÞÙÝÓߟš?ÁòÓ,f­ ߘq¹Nû£s'†¦áލQ.»?>)ÐLJ¼»¯ÿ‡¿úýÁ3çÙ¿£ç/5wtúùŠƒ¥ü§8™_ðþ¾ƒk—,2©ûögûZÛ2’Mò­þ¶±µ½¥£34(Ð*-÷öüà~wÏòÅžvàçÅ*C¦F›€à>NJǦI\uö̰Zí&²ð3çêêŠ?è›Û;c"F}–ôôõ7´¶º»OKz˜f‡­^½·_öÖ§ûd¤z¸»O§ñO¿wåÒðé®îÓéÃôëö5ŸÓ*Z§ÙN[Å¡ñ-6ôõ>[·$o˺•±X®Tí;~úOïî~~ÛÃK²³,Vf¦µ>639ºSÍ Úi«pÕ×~+;w¬nsǶ9Q~@®8qñòßÝý_ßÿv€Ÿd¬òwÍÏJIš¾Ä|×§ŒS °¬¢®¹uįqú@·ˆ€` KìéÑ76N¼>jK-Qº¹¹â·  ¤ŒDAiÅÜø8/O>G¡Ra;}ù*„§áaÎ%.ö1®–U`/û#Ç.\®¬­ç #ÑÖÕ?þ JËûåœ3@va犖üâR¹RÙÖÙÝÝ;j» ÖhQòä¥+Mm£î(Q¾ª®¡£»·³§½ÙØ 4Ò§ÐfI¹ñ~Êœ¹R˜_T‚_gö»QߨÒhð ”Ç[äã7ë(®¨ÆXXIöŠ¡'í]=èðåëeØàïš7^vã&ú ­í5 M>šEa(ÿ‘æ‡S]×€ÖX#è9Ô$'.] ÉrP¬ª¶Û(¯]‡¬À? ‰†–¶¢Šj†×8ß鞦ÓÓo¶wŒF°'øñáãyó2ŸÜ² "]ÂŽ­÷g§¥î9zwK«kšÛoÏ5>T•†ÙAIsæã3ÿXvt÷@5Å „15 ÍçŒÿ¹5yºñÇ:’»Î8> ø|î?yH|ÚÙCÇé¿füJ{?Þ)' :Æšý%’`iþ%ÆF?±y6¿ RàYøêᇠS|¼Ù£M¾*µ•*5ß±’êÌš»»»÷-çjh “x¡°¾Kàw¿6øòxÍŸÈJŽ?Ñ&í´vvá;+S(¯WÝÀOÚg`îÐ[¶Kˆß:¤ÙïC߀ ¿KNŸÇ—ïáX=Áo†Sß2O¾JaÖš¶¢\¾oïôIyϛiÒ–Ë×K·¬]uêrÁúe‹Ùݾ8²lá|ü4,HOÅä‰ÿúóë ~ö?õ,'3 Ò/_ý;ä˜ëU5-íP§K|Å 1Qha×¾Cïí=àåéYßܺ{ßáèðÐÈÐ,0¿yý]üA†_ÌOŸ@›ø]KKŒÇoè¯ÿövYM-,œÐ¸Z£IOJ@#o~ü9~JŸ½8 P"kj¡g ®5µw,ÊÊ@üòþá]ømíìÆr‹§‡J/— üù«Åø»TTz:ÿjnVúÐÿoïLÀ«¨Ò4LÂ ÙØB D–˜…@HB-‚² "ЈҀ4Ê(ÎØm?==íŒ=vÏ´ýˆãØj£¢²ˆ ²/*J«„¥e‹ì;È[‰ 0ïMAy¹I…À¥ú»ž[÷lõžªSßùÿÿT.]Z”·‚Jh%±a}ÒÆ™ Ñ:¹ƒ‚a¿«¥ó•-^+ÿbùÊÝûÐsžïÏù„óŠ­³kß¿¼7•"ÍR’˜ÿûõ )w6dá;ûó/§/XD[ðÁÂÔ n\èZ«7lžû·%_³&óV¬&k6ni__ÇMžغub̾U\b_þøÓ…ÊYÿ…Ⓣ9¿ v·½íønïË¿ñ`7ãAõª‘ÌJo’·:Ÿg^û¬F^›2ÒÜ+s7b8æjÕ¨ŽÍëeÉõüò;S²3š.Ndñ‚/óîmŸƒAÔhËÏuëÙzHHˆyÙ$5¬?nòt?#^£jU®ð-»vG„…­\¿‰*»y3î·þß„Ÿ°ñÛCg¯,,1A…ü½»ˆÛmñò•í[e2 Få¬Xôíš>qæ¼{ö1Ÿ?Ÿ“™îy ÄÆD}{bxXXbƒ;(Žîá~ÉÉÌ€Ûü/—àA£þ—&LbàHÌ\ô%AEáÚ`µ0~ÚŒ¹_|⨼"ýÊu›Z4m ^·Íóõ5Ð^ëÙ¹÷ 3Šàud{ãýîmßcùÜÅK>\øyZrb­š5¶íÙûö‡³ztj¿sï¾ßšxñâEæú|üÔéô”$ÚuíIóÔdnîïª\yÒìùy«×vÎijvO ¸½–íFí«¿äc>Af“”‰³œþ{Ï%Þ´‚MS§/øÌ6 <]ÛçÿÏ£)x¸ðدGMnÔ€PÍÿxå Ö…[·p_×?Œ{sÄ€ûyh‘ÇõCœÊ³OŒä8Bð•÷¦JºÐkåƒ{u#Onv«­[nض“ÕaëŒ4¦Ë¨Õ·ìr® M`OMhĹ,ø*ï7£†ÝYß9ûÓ½‰³æ¿ðÌSFž»ßÓ¹mkÓ—Õw>ž3jPÿôÆÎéõ&|Î;Mhåþ\æo¥…F¸Gžq Qsèh·úck;;qªmfúK&³à†V4dåÐ~½°öyeN¨ºã ×Ë’«Žk’5}NíȳzÃ&¤§)‰8b|<¯[_­»^6èu?#ÎÂàûâsÏ=ùXHˆƒçå }gÞâ¯õìF‹žý¯Ð?õƒÅ8ë«xœÿ÷_ "þ`Á²;ŽiìfDÎæ]»ÿõ±áܪ¾nìô4VG]Ûµ¡8eš¬gvíÛïlµR¥Ï—ýõÌ ÏŒÁeL•’9¾lí·˜™ÐÖàeI†8ëÓ¥?™-:Ë_ûñh_õp:ÌK?ï×Ëî¦?(o É\¢ü‹&Æœ‰8ò>y΂»Z5xŸÓwÌÈþç«ã[§7%_Íž¶aÖHïÍœËâð™G‡º­®í¦¾‰@ ,ZЊ×ó'Ä"¦YrÆFÃtàjMy¨w÷¾÷äbDÁ/°"3޳]+§Y…†ô£%N"f f®¸˜hæÈM“Dô'8øÆH¯õàaÅ4ã—v¾d“Qhh•”„†Œ×šM[è f•Fñu “¤¯u#F)>¾.KLk6l&¦Á=µÉL/É~Í?ž×mùG•œ–”ÈÐÓ1¶v|wåIïÖÿŠqÞR}ÍÙZúâöW w7ö!þC¸d5kòÄÃÍF¸›Œ[Õ×-Ó"ƒÉäDQEV®Ûh¬jÌâè-l®H"ŽéhîöøvóÖFñõpÍs³#£YJ™…Ù¢Y‰™ðh?õ¥pä1{pkc¬Â’M¡á&£9lB·p±e¦:Wt|X¿1Û`-3¾ºõä½s1@ŽØW’Èà£o_7æq£Î¿rÌq©8j; 0¾>Ùixyúwë²rýF·ýh˜g0#£˜¤b®Ý£aÚ„±ô°02*gÅÏdwGÝXf4¬ßÆÁ¢3gPË[ag)©äUá.–jò˜3sº%°çG8 ˆ>?•›e›5Nš4{§Àÿ耾8ŶíùŽ%ÇɃ2£×ùjœ”‡„«Å´ñó/!8(È.m[³‰ŠK„F:%f9?ÁŽ0G¨—Gx¹ŽgÓ™k,¬ùZ':ŠE» ¿»´ÍF‹·ÏÊ䏿nÄŒ:}]–‡ˆìA ;}UõêrE¹öÁH{^·~Z7‹_wÄã¯îW #~îªÍÆ­ÿ=âµ#"¶,¯-0&ü'/³IÀL<Ôû>Ãl1WŒ´¯[€›0-£~ö>l›Íâ'‹ÎÔ¯k~5Ê'N>[\Œ?Ôü)îêÈš-š?™ oí³³ë“o·loP/.¡~|zãäŸ-ƹ†)qŠ®ÍĈMÚ(ëÖúŒâŸóÅ×®ªÑlE ¸–*rþ½ÌEWƒ7-s¬’re}ãµnþwgÌÁt°à({3’bƧ‹;fµèywÕñùÒ¿›¿z&*$üù¹1£ŒEŽ!cÊ@±pïØúJ‰GYfqem§ì–æz‘%{\‰ŸÅ­æ«Šë§Ã°Z=6¸¿aæaa‡´ªYýÊŒüS>K©ÒTU½:§€ÁS¦µ&I ›wìæ7¸wwÚd~\ž¿"²Œ¯ØÌø—œ{öpëQ¿®¹²³þøÚ[ïÏýäñÁºýZA_ùS¯å¯¹ZLº§sŠj1 ˆ^Òç>×V¯X…r…Ûff,ü*UøÁ£G âHi˜›µù¹,â\¨¢œ^ Ef%® _­Ÿ=Wlfó?ânW8ñg¦SɬÁHTôˆ7ŽŠ^zp¿[£eýšåî-k ¾nêaôWoÜrî‡óXÔÜì(„+9o§{Íùá¦ÆNL‚ÚÈ9fèÏJWÂ`Cè’‘.Ó¿¥©›qŠ›vÔNMhÈ V‹Œ$À±~\¬á¼£9Âäñ¦‘À¾¿  ·M+¯}3tZî¿^ o„Bzͦƒ"ø˃Ævú°æÎÅty>œª$:m¾>¡UB2RSˆòáénZkÈLœ2[Bˆ@$‡ˆð —|ÿeõââpJ# {¾ S Ûë¶lcûóȯ–^- 'fgñ¶$^øZÇW']3+aëf¶¢{¢ˆï&°Ã ®ÙÌ´ñâ×Í/æOž ?•ìyæêÖ”„…¦ÞÙpœ-[»Ž9éÃW¢¦ßþ,o9{a8#â‹[¦¥†‡y ·$¤bHñ'VƳ3q¤v£{Ë_mL‚÷Jð{Þß5ë‹lÃQB˜¯EàigD§Ñ4Ö‚„;ây &ŽøaîÙUÿ—eNf³¯V®Ùwè0Pϲ^øjÝí²ñ3â8ˆ¹Â‘õÔED»’°}Ïæ*zÄÛÕsƸ”óÓ.¾¼•ø¹pk³œ·j­¹2{›•Þ”¥ÓXQ,Y¹Æ0'ó+|ø° ¼,½ØfÁôb–*}ÂW= ´99 ¬kT«¶lM>±ÔŒ”ÿÛŠUŠH#Ñ`ehD±i€½ºÄMzí€ÃáÀî…AtêÜOÍʽæÔApŽ@ë_µÞ}ŠW¯*O¯ªõìt½·3ˆ;oŽkCøzútéˆsýYç/ü8¨GW<#h—ÞïšÍL§&&¤%ÝùìËã"ÂÑìL¡,»î﹫ »Ï^™8•W 6MJ`ºáÙ@)LñýàãßýÏ8„máŠ2T…Y¡¯æ™_ ê÷ö‡³Ñ%¬`ëöÕ%£ÃX³ÿý_ûõÈŸû2þ›mù©<+=ÐiB ˆÿeM cêLjØà⥋flSôí‰3a„*—4¬o³~Ï]êÕâý9 '44T‚gžx$²VrÍzmOtj\«Ÿ ;Ò†ú*Ë%tòìðaX†ØÕþíñ¦C„‚ìs„áAF%~˜{¶âë²|zø23FÓ.ÊhœÂ*ß³¬×#¾Zw»lüŒ8?vG²³Ó©¢Ó÷´kƒNòÚ–q°âF<;®^ÝȪåÙ†îptmà´…”çãç@[$7j¸ÿPAóÆ)nMðæC|UÌ0\?ÑQ5L]K™ ^F ÃpÇìVs²Üʖ櫯zš&'²kì·c_%,’zÐ@KWç7*ÙT˜ƪkºQÿ°þ}^Ÿòá³/¿†í {Õ¨Ÿ=àÿ†íݹAå\Ãû÷)M•GÀõ#Qnr§/_¸°·ß¸;_JÙ °°³æ:j_äQʲF6¢1çÄÅÄ`ŒÁØÃ¬äf÷v«( à šÕœD¸äY‡±¸ q„`:r8*³—÷û5/ Zä¦#tN#¼Ú­6?_é .?êw añU„W%®äÕïãYÄWå„q †Á³”ë^%pèèQãå.®Ç!}d×ÂUõ´Ü“º©ƒZô™ê¿8#Ë’úDÑiÜ©‘¥{¸/æ^òuY"Âx³ð/öÃAãµ ¯ƒ¾Z/ýeSS_„Ø0—új¨¢OÛ¶éùK,·2"­ù¯ZåX.îZÐÚ-À‹‚°±‚ÑíVe2áå ,œüÏ?®ðšöZ— ë+ÿǨmltå_b›è^›ÐA°€SEÀ=»4ïðSOZ£ýË_Õòˆµ²7ª{1ðp=1d`LTû¥‰SþÃÓ£ Ùt£šP=e%°zfÿ‚í3ËZŠüŽÐF¬¯æ|é@ }Pä¼ׯïŸzÌí™h]­¸þ°òÉìõ…Îðö²~°3Íî3 2¤JY *¿ˆ€] ¢*‚õ±q¯žœðvY¡GÞÝ9öÅ—nùã(ɳ糳ƒàë˜Z5÷êN NYÏEùo, Å'–MÊ9{b{Ùª nÕoVlR¯²•ºY¹Ÿ{å œ,tël¾áfµXí8szð‚™ÇŠÏ•©[a•ïvëão¿j™*TfPUÙÂÿ|êƒë¸-\ ¢C‡Ø? vÙ©îúë-Iãõ(«›ì–ôó¤ÑsE{¿™ÖµôÂ((¨rFwãÓœá;ú8­'ŽZ4¿°ÔÂ(ÂáøKn·¶õÑà¨Õ=°7ÀUEp/šñQáKc/ÿ´aØû`×6¼Öãÿ$··w@:z…ÀùsÇ×2²`û¬ë «V?³×äZõ;\7§2b®³dñê#‡®ÛŸÄQc;vI)÷†üë6¤ " ·€VEÐü± àÄ[ãOÏŸwùêÛîA™{wÔ¨ÇC“¯lš¸æW}oŽìœ¿cÙOòþ2ª*µµ“Ðú—•Cü½ÜÏ[Å:v‹ ó>o÷Ž7ׯÝyê„×®H4¬iÆ ÆMC<þ¤×ü:("ðF ÐU‘1—Ξý~ÙÒâuë.ØùûïƒÂBquCÓšE´kï¨åü{úˆ@Y œ9¶µpϧEùÅg]¾ôcHXTd­ÆÑõ;Õjëõ{”µ~å¿…6+\~h?¯½>vîÜåJ—£BÃkFeÇÅ·¨{Ëão!5-"p]·‡*ºîi(ƒˆ€ˆ€ˆ€”“@`½Ûºœ'£â" " " "`™€T‘et*(" " "`+RE¶NŒˆ€ˆ€ˆ€eRE–Ñ© ˆ€ˆ€ˆ€­HÙj8u2" " " – HYF§‚" " " ¶" Ud«áÔɈ€ˆ€ˆ€X& Ud Š€ˆ€ˆ€ØŠ€T‘­†S'#" " "`™€T‘et*(" " "`+RE¶NŒˆ€ˆ€ˆ€eRE–Ñ© ˆ€ˆ€ˆ€­HÙj8u2" " " – HYF§‚" " " ¶" Ud«áÔɈ€ˆ€ˆ€X& Ud Š€ˆ€ˆ€ØŠ€T‘­†S'#" " "`™€T‘et*(" " "`+RE¶NŒˆ€ˆ€ˆ€eRE–Ñ© ˆ€ˆ€ˆ€­HÙj8u2" " " – HYF§‚" " " ¶" Ud«áÔɈ€ˆ€ˆ€X& Ud Š€ˆ€ˆ€ØŠ€T‘­†S'#" " "`™€T‘et*(" " "`+RE¶NŒˆ€ˆ€ˆ€eRE–Ñ© ˆ€ˆ€ˆ€­HÙj8u2" " " – HYF§‚" " " ¶" Ud«áÔɈ€ˆ€ˆ€X& Ud Š€ˆ€ˆ€ØŠ€T‘­†S'#" " "`™€T‘et*(" " "`+RE¶NŒˆ€ˆ€ˆ€eRE–Ñ© ˆ€ˆ€ˆ€­HÙj8u2" " " – HYF§‚" " " ¶" Ud«áÔɈ€ˆ€ˆ€X& Ud Š€ˆ€ˆ€ØŠ€T‘­†S'#" " "`™Àÿð1ü»‹otIEND®B`‚networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-architecture1.svg0000666000175100017510000010230313245511164027030 0ustar zuulzuul00000000000000 Produced by OmniGraffle 7.4.1 2017-08-04 09:04:47 +0000 Canvas 1 Layer 1 Compute Nodes OVS Database ovsdb-server Database Node Networking service with OVN integration (DVR) Overlay network OVS Data Plane ovs-vswitchd Provider network OVS Database ovsdb-server Controller Node Management network Networking Management neutron-server OVS Local Database conf.db OpenFlow Access Control Switching Routing OVN Metadata Agent Instances OVN Northbound Database ovnnb.db ML2 Plug-in Internet OVS Database ovsdb-server Geneve T unnels OVN Northbound Service ovn-northd OVN Southbound Database ovnsb.db OVN Controller Service ovn-controller networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-architecture-centralized-routing1.svg0000666000175100017510000013424713245511164033033 0ustar zuulzuul00000000000000 Produced by OmniGraffle 7.4.1 2017-08-04 09:13:24 +0000 Canvas 1 Layer 1 Compute Nodes OVS Database ovsdb-server Database Node Networking service with OVN integration (centralized routing) Overlay network OVS Data Plane ovs-vswitchd Provider network OVS Database ovsdb-server Controller Node Management network Networking Management neutron-server OVS Local Database conf.db OpenFlow Access Control Switching Routing OVN Metadata Agent Instances OVN Northbound Database ovnnb.db ML2 Plug-in OVS Database ovsdb-server Geneve T unnels OVN Northbound Service ovn-northd OVN Southbound Database ovnsb.db OVN Controller Service ovn-controller Gateway Nodes OVS Database ovsdb-server OVS Data Plane ovs-vswitchd OVS Local Database conf.db OpenFlow Access Control Switching Routing Internet Geneve T unnels OVN Controller Service ovn-controller networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-architecture-centralized-routing1.png0000666000175100017510000045105513245511164033017 0ustar zuulzuul00000000000000‰PNG  IHDRYÕ¾ÕŸsRGB®Îé pHYsˆˆÈ¥†ÕiTXtXML:com.adobe.xmp 5 2 1 °ã2Ý@IDATxì|TÇñÇQEB @Ñ{ïÝôŽ)ÆwǽÄIœ8NOœüØiNì¸÷nÀ¦›Þ{ïM @T‘ê]ðÿž–Ç»Óé:º9ô9ööm{¿m³3³3W®\©%A@A@ÜO·G@A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°! T‘ŒA@A@lU$ã@A@BÉ8A@AÀ†€PE2A@A@°!à]ãa(((ÌÊÉæ5ýýü|¼¼oNnn^~~mßÚµ}}n4T›ëøù{{{9lsNn^^~ÞÍõR_¤ÄÈ+W®¤gfzxx”˜¸’”Ø é™YYÙ9Auêøù•ºm……—c/$FÅÄRTó&›7 ­h*Θ©UË£n`±(ede]¾|Ù×Çǯví²ÏšêÜ/&pJ÷³ª^ðbJꩳçûwïêåusŸiÓ22ô€,q¦”®ŠË•”|©ðò•ÐF ŠKPùñ„@VNÎá§ztl_Ç¿ô+Lå£Qµ5z½üòËUÛ‚Š®ýhÄ™WÞùhÕ–‰É—úuëb_Ý쥫>œ»ÀÏ×·Cë–öO‹‹aMä›qq *4þÛV~4wa›ÍB5tXÑüUkßûv^ ¿»–Í&¨1‘¬&?ÿÛëwí›kJÝ/&ˆô+TyÀÔ°R¿`_䯿‹»tKŸže,§ÙM”¢c–çÿòuÛwO=œHû™bLYîá—ßü`ù¦­·Qî%[/Ðf!àíåÅ Ÿx±WçŽÖÛææ)oîÓ†K·ûÐÑc§Î¸”ÅIâæÌêÝ{专4òH0"ÀfƘyùï#KNIOÿçGŸï9|¬ðraÇ6­Æ 8c˜A½ºCÖ¤ed2>¿_±Þ*¿MófÚNÆ{ŽsXãÁã'á9ÁLí×½«1AùÎcÉÃå ‘Ã*JYMÕ{2òì”QÃJ÷eÉUM(Ë+TŸ¼•&çö©£GlÞ³þbõyýjÞ¥jÞâ²4ïëÅË_~þ)ŸšÿÖ“F5¨?’™²ÀuSä­íëûÊ ÏVÓÎ9D× ™YÙ¯¾÷é¥Ô´FÁõ¿{zÛ×9‚7‹×m\¾q+œ¡¼ü‚ûn»U5ò–Þ=­Ý5}Ü(ûf+j©W—Žþ~µMOK7kªs¿˜^°t?+ÿ‘ô¿| pûV-J׿j›«âfJµ}eSÃ*¾Ý:7iÔð›%ËÿìãžžnÄ1!lý§»`Ô,´1ge„hK7l¶ŽNe¦,(,,Çê‚êð¾•/KVlár|‘‹òôôàMa8L™_Pà0¾r"+®ÖnßIDÿþö™G$ï…ºÉãGO?šðæ=û’SRÕËÂF"pábòù¸x£¿Ÿ)6êàÞ=t$²Ìçýb¬å& ;A³Êý½6íÚ›š‘1zP‡%Ä vø¨úGVÜL©¸w¿YVlNŒ#õ‹Ž¿°G$ÖFCÍçš( Ìš2áߟ|µrËö½º7m|]åÂ!Pq‰Iœ¶ÏÆÆ¥¦g´mѬKÛ6£o€Œ–ÄÑñ ß-_à #üÃúÍ›vïã8¾ãÀá3çcFìa® dûáMø¾Û&iµ¾È蘅«7tlÝRIÓyŠ ¥6*Ô£QêÝ¥ìq4^U!|ÿçÓ¯QùÑ·¯Ø¼ }‘VMÞ½¦~ª¨Ê~µhYvN. ìmÛö<~rHß^JœLÆ}GÑ££‡»dݦ3磳²s[7 =x@Níu!¾ðR°[¡oºuh; g·üü°{vëÖ¡1¥1Ìr¼i÷^ ¸pñ»rXHHÿ]‘éY8—/_Y·cªçbãëtlÝjh¿^­›]×§Ù¾ÿ0N~Kpݺßþ°<™qUs¾yö¾™&/ ;uôðVÍœûøø<>sºnŠœôoj*ŠÌœ­o;ètNºØ˜L…Uo¢ÿìýwë§‹×n<}.í.#÷å›%+’.>4}JÃàúÆ^ønùj“÷â¥TúÔ”‹—ݲgÿñ3Q¹¹ôKÓqCumßVWd  Ñ²nÇn"yýâØã‡ bMNM[½uç=S&¸Qƒ`pØE-šËTâ3v¦níoèbWg±Lô»ýŸã¯çB¼>pûd¿ÿh8ªN(?¡Ìwçıꢃˆ¬tV‰C×~€½ôÄÃ-›Ú `®Ü²#"ê\zF&ëEØŒj;l˜éÕ»ÃÛ·rmÄÙs ˜pž6v¤Q·ÏúL4‚I˜éÃÚÅ`†ŸgzÄ{1õ˜VÈRáïßwÌàÆùâ½²tMq«§f.ã?!)™Õ¬C«í[·`-*îôbœ)¼Ý{ß~ÏRfzMõSÍ,.qIQɶî=Àbx..!´aƒÎm[¹e€Ã’‘Žiœ¯Ø „×0\Ÿ¾÷.c+6mCíoò¨aÚ´r8œLX'%{š1¸W¹ËV­Ý¶“eÜØ* ;DÀ]¨"^¾SÛÖCûöÚºï ê/ȸa› Ùuèè ~`Õc‰áïø©È#'O:ñô}w±Ò!•`ÛËͳMÚÔôô윜ü‚BV+}`Mñ“?Ò;}FSEl?DöîÚIÕˆÒôú»!#Z4i¸Èó1b{{ñ±‡ê× RiHß°~½Öo‚X!¦yhcoüæ*ÓëŸ|“paXÿ>Š%é¶ÿ؉ö-¯rÚi0å4 Ù¾ÿp~A>kSaaÖ‘ˆÓDþøÁ{4¹ÃÂúÖ—sPXA‰¼Qpðšm;Q‡ìÓµÓîÃǨW'3V­ÂŸ/XAãëãÍ€ú¹˜8f~TtÌ÷ÌP 2³³?˜=ŸêX"iFrJÓ~ë¾̘¦'*T X¶>ù~SŒ ê×…úäŠÙÑS§Ù¢t½ð?Ø(Š]‚Œ7Eœ¡Ÿr‹· "¡hùIßÑ ¿yêÑa¡*™ó.ÖEéÃàlL<ô›„\ÕpGÅ›¶µ¦ŠØ``Ïp7°Aýzä5öÂ¥´tH4" é4¬uá'ÎD!÷YÌ…Hú%<2ê'Ý :1pêì9®›yyzŽè˜m@b6EhbÆ ”½Î‹ÍF9~Ç„1:’€ŸÑöך¬Ïc„Mý'ƒÎ…¿©„rLƒzu¡õ±ü±g¿ðÈ}оÅAd¥³¬ ]ûvùйÂYå_}ÉF=Ô¢IhÌ…Ä5Ûvñ‡ ZÉ6Ìô‚”s>.á­¯æðŽœ=˜,F¼rø™È;ƹuÄ…Å™¨¿£bbÒÌ/u6ÓX©¶ì=ÀONz9yyK Ë 8Ÿ¸û•¦DôÊÒ5¼ ýêÄŠ^æƪÈØÃ'Oí;¾tý–_=ùˆž>úg ?OFžã˜gL Ã,¿*leIlš³lk,Yê)ðiLAa ,d‡ïE!%®Øù…äµ?«pK”øáýûPˆÃádBÀâ8±2ì©1 Ž?|ßÈèØ”´t½³/‡¸UÄûßuë86H6N¹x)å³ù‹Ùrž{ànÅh‹Ã}®£§¿\°ˆyþÚ/òþìyîŸ6¹_wÛ½6¶“ïV¬9uN—‰R¤ ÐLïã§£ˆäž$ß_˜®ŒÑ_<úÛ!1pÝßùf.Øì¥+G øUË6n™4b ãnJ>¬•D̺QûÝ{Û­N¨=ú^;<8}*ëÅÇß-€ÜSä1¼8$›Á´±#˜ØŸÍ[BUQqßD4ìÿ~ö ” É Òþýñ—dd÷…ž fÁªõ, àù£;§)¹L£w¿ý XÍÙtáKÖo ~莩Lãzƒzu£Ù@m¤Š+äýjûšÎ”¬È_,X &#ô5u"[Y°j;ëã‹=HE%v±nŒ°gwíÐ.KDÔYµ¬CA‘€ò_q¡WØ,»wlgß OÞ3Ë?½ù^ãF þü“§uÉØ?wOÏëÐþ/.¥µüG±»“ «8» ªpNÆTbƒ&5Ë:1PxðØT¤A|æøZ“•Y£Š*ñ;>é"]óøÝw(R˜eúßQ“òË!DV:Ë¥¡k`´yÑšìÜŒX,ê˜ìo|þ-ìOÅÇrØ0ÓËÒqœ ‰`v’K=eÌÿï‹Ù ׬‡íjäÏ9Ÿ‰¦’ÕO–&^ËþcáŒÿÚµ_zòaæ mÉ·¿š‹Ž<ã¶„ôTù¥è•Ñ~u‚ÊdH£ËòË'Rö2˜¯úmÛ¾ÿ ‰"W…˜¾ÿþÒOjÙîø^ý@íq±€7 G×°ÆT¬•%rŸ5–6<5kúXd ?õþ·ó ¨®•]ìÿöïe}Å.¶Ð¢V†“*Áù8qiØwhÝŠ&{h¿ÞΛ'O=Ý Hæ{&Û¤ ߯\«¶4ûן¿z=ÔÉíãF+’ˆ° Q&0+ŠËÒ‰ ‰¢*“ã"*&¤&Ø¿ÏÆÆ²§*BaþÊu¤¼òµ«¼69*‚°`Máܩۆ{HßÞ¬&,¬¦S¢"èvå±· ¸oÚ$ûÍXB€%õé{gª‹Ùp&ÆLdt‚MÈgÏá£LÄ7@¤jᨇ&¯J¯Ò8üŽ.j*GFE‘Y z-H*“.¥ð“æÁN‡þ{læt­ê„äîÖá¶4ª±XX)?}øÞž:P €¨È\;7*j°è“eˆ£ËÉbœºX‘ID2tuæ\4Ò@bJ×ÅÝ‹ˆ'®¾'"£(Švò}âÌšËOEõ°ø¡µð$ ‰HO`£×câ¯S9‰E¨š6HS~†4 æ›1© _É_Œ7Ñ”øŒ¬é$SQVf)‹“ŸwO¯¹ƒØ#èÔ¦5‰á—ÅJg¹4tMŒz±ä]>¢_ÝÎ ¹ì:FšR?u€pçàÞ2¬‰&‰HÖ¥]›I#‡Rbkc.ç3јR‡¡ «‹„:rþ*ÛÂP‡$"ÀàaïÖß¶íAkòm=’©«]£rÙ¯NIÉ)´A•6!Æ¡QÉ­ ½®Õæì[˜ê™ûå ‰€?<ã6•ÍÊ’¸|ãÒ3¹ID˜“ÆŒ 6•»?öïe}Å.±p‹ œ—†}Ó"jÒxn·Ø7Læ^TÌVÍ’‡½‡ý}ää)[²ž]OÑ{@Ï ðòåc§lB1‡ŸE»¦vð?Ñ‹äøÛ¥]kÅK G%ÖGµrbFpñaÚA!,””Ç´O .R•5ÕˆçŸrx©{¦L4=µÿ sÅ("iÖØ¶’"T)a¡€™dLC{8ŸÙeŒQ+5Ù]ë]¤w—Ž(ú(n ±Bñâjã×yá©>|2BÇ@ßH‰ŸT$„ +~vn."0݉TT¯tÆì€ÌO:Ë(h€0âþ'W•²t]ܽˆÃ§Ê§:Êu%[øUo€H'*Aª¦oöNck›4²†ÙLÉ®ÿ,²•U¢ÑQ9YPq0®ô©z—ŠB2éY_¯«(Tâ¬1¥wòM5ãSeT)³¨©Æx¶ÒY. ]Ó£¢_?õ£?þøIv_U)ô7ŒL¸>üdÜê–8(ÊxH_3¿M)Å$$³;Ÿ‰Æ”:ŒÂáÆ†[¬!híÙ¥}Œ:‹¿yúGªC­ §óºÚ5:£iub´p-–³Nª‘ŸÖñÔy­ÙpàøI„’0ïõ¹ËÊ’Ÿ ØYcµVƒ*ó–>½,š™3¾—«+¶nYÎljKÃ^t‘ •¥=n’×½$hªSaϼüæ{}`6tnwÂ+HIdÞü|¶i\J³­’NFU÷Ní7ìÚË9¬_oµMB BF®ÏŠÉÑ“-“ÔæªXJè!š1UÄqµ\è*™Â6¯ê*ÉŠƒÂô@×Dsèd:tãE}¥©—ý Er͸ҹ ?tØaêÑ*´ð{ù#}×vmzvîÁ§ÞîBÑ»Ài‹*:ïêB”VGJZ†Ž!`êb`az‡UU”ˆ±ˆ¤ï2ÆYÖÌ ’S…+ áRw1<3N«dÆEs™.nÕ´)1ˆUwÖGi·}«–š¦ª.ñÛH’Xõ $xqC®ŠÆl|8'¥Ædb)q$@û Þ¬%>c“PêhNJs2kœä2=‚í À©~·YZì,—†®ý£=T´eÏ4Nàs¤f¤ëIalªó°šÑMB®Êwtb¥Í Ó”©ª /ç3Qç5Ò‹ôlT׫x5ÔÑtDcƘ/橈±ˆžÊëj×è®Nt(|S.ÀŠæz:š:½K],Û¸ܸbbTˆ±²¤¨QÒ ¾i¡ ´zAANVrÕBÓ{¹´b»ôŽN;'. {8¾TÄpRrövj¨‰Ûä²×[‚¿å°4c$<ü¿üôTJá±ã"/C¹áo¼§HÉ´¢©Èþg‚Ï œÑX”§Ý›²aóúz”$h÷a›ø¬8Û¾ðÏxŠôÍX¦1\–.V÷õàqF§¢Îm[Q2´¯bž8s–ÝÔÄü3V]\XŠ{ä0þšÂP²9Lƽ6âMT|£=lÜØï|+ñ™=ûľ@'³Æ>qq1&’¨¸d:Þbg¹4tí<ΗþñÆ‚Õë9Š@O4à™ûfr8ÑͰ€—@2hSb%´¥R˜ˆú‘󙨓xðá'*D:²ÈcF¹ uW»F7Ã~uB„ýÇ7Þå,IŠEh‹}Q¥·®sY  °øù‚Hùð·):Oç²²¤d­±Æ…Tg·ò²¦÷riÅÖé€M&Xœ—†=û‘.VθáÜæ^¿s7 pã`Ý0‹š#Äþ0ÿÃJ èK±öñÇKÁ·ç„Çéjî²ÕOͺSéQÁR⦽•¢Lil7zÚ·åfªmÀˆ©*Ó”’Ÿ\[Cøˆ6±V Si ¡€Ö¯_–.†ÏÇ­.¨"ÅÈABJáÈ¿¸ Œ25?/PUZAßHYÖ¹ïF½êV‹}Eô£"ÑÐ7=ܧÆ ö GZÉ5Š#1M‹›5¦dåøÓbg•eè",ƒÁ ÿ •>8²úø±´èʧõwi\oؤž¥hSØö\aë…“’;«qsÅ®ñhØ@x)þ¢*‘’–ÆV‹è¹Ô +‰¹DZ‘9câM&"³’W§aádŜ喉=#ÓÊ’¢˜Üð§MøP…:­éº¬ʸbyÿVª³’Æ¥aW˜2µ¦—•òÝ6›Ò¬PÞ1…3F@ˆŧ=|âeÆ÷]ñ@¤ø¨Å —EŠ2¨#GgçVÉØ59g¬ß±‡Ÿ=Š”s 4.Ò„eES>] c7>Ѷ¹6¿v‡\?²(æ<{$,¸,FJ fŸÒJL£6I™²©£ÓsyD-ë:Æ>°tÃÑz0È*.‘‘’S#ßJ*©d”@„‘‘ìöešb)V³…_Õ³îÛË”@ÿDk˜°RòБ^ÿòÖ‡ÈìÊÒŰåØ>aBæÂ`oWdø s»ÖTDLøéHˆ6ã}:Ý€ò  &¥tW!=MWðtEKÖmf0И¾]¯ÐÒ”÷ÈD,7bò™–IñB§4Š›5¦dåøÓbg•zèÒT® ò ]Õ®I"‹˜ Å*€;|A¥]wâL¤é©:äpáßïêOU¾b¨¼Èßi0üÂÓEç(]à’uÿðßw7ïÞo=±\˘×Í­I"~b§ÃzùÐ1X·‚ËÙFÙ#5åµ²¤({(öø oPŠuÒâŠíç[fwvnޱ †“ºwbz‹2þtiØ+-C–Ž2VêÙÝ”*¢kÑ;d sF‰·tgs¿0W™ t$JgÂÑÕ, ŘE X§! øØ&¬ Ô®Éy´(M¿˜Â#X;T±xí&£¾7ìÜ }ƒº®ºTb|ä$|û¸‘è vÍ~'Éœ?Ô«‹ì¦]û4}Cz£uqÙ¹Âsàø‰(9FˆÄ)/º~ˆ”f•JÆ)pÎÒUd´2Qá‡!|$1üÖú¾Ž¼üªb¹ ¯ËO ¡Û£|_tjÛJikZìb]lšÔÚ¡Ï}h ¥ï òb\œ†¼v.>ƒ¢(,™¨C]¾õÂJÔ¨VþõÝ”AQ‰'¹6ìÜCoºÓ;½N@ÃE|“ŒØ«K'F£ñ©“pq³ÆI—ÙCd¥³J=ti›w‘ž×Çt;»]ÆéÏ#û†éô*0¬_ Æš‘+À™GqãÔ}OS—~bú•ô‰—.é\ôšº† ËPGrÀ?ÕZd=×y DTvHg>„n”±I%†™±hö` K•.œæq½êíÕ÷>¡š¾$â*,5êdö‘úbÔY9¹(ÙðîØöÅT+)GîÏ7Ìÿ™“Æ}:o1,q˜0·8nÛw€Â‘Y œn_¦)j¦÷®¬nYÙØÛPäˆ)úIЃ°ÿùá\Q†cL7qx¥„»&ŽSi,v±Ãò¡{(æ’ ^e)ιø ‘Ó3ö¦±Ò„Ö3VVa%’r~öÈý”Ã`ûë;q“‹NGÔÚ”À³(Å݆TäM©c+ ÉJ¥*ÃYc=»ó”öYé¬R]7/@L= b|!áb2LM@à‰DŒQÊxkÞ$Ô¾a¦ÁŽ$¢É×ÞÿŒANPæyèn„˜z50å²þšžÄ¬K¬6:×´q#¹ÄU1Dã£a¦Â™€õÅE2+èéÒœJD@egú÷êÔ›ŸÍ_ÂÅÆ[={žeÆ6×r18BkKl™_ÜP¡4¬ã¾ùÅ·¦Vaé€9hqI¹sÂz“Ñ1âv-«7µC`D”~1•\âO‹+6«$ŒÖòšÈ11ŒAØÌLL]…E0uz‡—†½bù«k¿K“H€[SEp¸Yƒ™5 ¨“_=õÇhyêê#‘XnäØm¼ Þ»sG¶^öøFãŠÌصÛwãÏA_"…ªÊiÉÄH@ìý»gÇ<üÁðþTØÔñn¡®+b!ås‰æóù?üú©GJLï0ÁÝ“'° Ò‚þàˆ†ªÐcwÝŽu¨"'Á\ÈzrÖŠ?‹B•̲…oukŒxÈø`¹©àƒýû‰ÃnÑ„¦Ã&éHJ Uüth¦H'#@OÕ œ¿r-¤â$a¹@£j±‹eê0æøàÃÏÓ¼@aªˆý€€Ni€Õ4r`_Ä^p"¹3XªˆÂÙöþðÜã8áR›6pE3à+`ÙŤgml,OèQ¶XnG9ÓÃg1AYÂöYì¬Ò ]šÊnýÀíSfÿ°‹Ûü°¿}æ1TO>œ³€¹ Ëùkß0û×ädR¯nÐâµ´u"†Êä‘CÕÝûô.Å´lÆ -"ê¼1¾q~÷ìã˜%„U©Ì<Òû¸‹ÑÆÖ-¢g,³¸°TÞûoŸÄ9F—2§„†8¬MpÀò-äÔ’sªHݶ£(Ôì5”…žZYRXš°hi\„ÅÊÓÇ׈…¨T‘Ås¬ðySå–º þpT‘u0‹ëoqØ#A’Ë©‰‘ì¼@y šµ(p˜€‡t66ñ”ŠÃ£CK§Þ^ž¨R:dóš tøÎ*æ||‚·WÑ‘Ô|Ôa®ŠŽ´9”ðòR,ä\ÈŸº÷N¸ÖNêå„zŽ[ëÜ@.,DU¼e3Û"nJº@tBZÉÐLø4ur¡Ï”±?a•sÖç® &^B`áú ]Z‰]¬S–c%3Nö¨Ê–hIÁb¥°ëã“’°¥‰P’½â­ÑbÞê™Ì!D;«Cè›™Ä+µ˜éê3‘¨È@kbÓ‚^å°a& ‹ 4$ÐZÖ ftyõ2µ ÚÈ¡SøFŠ\ÕŽ"#€i…±+¾MMâ§Eôì3šb¬ @¶D`pe0Ø´qc=ûÎóHÛ§6^ŠŸV–&Èùøø„D.†Ò›º1¥¨Ž,Wl4D<œý¨±8Þ¶E0­´Óù°ç„IÊqQ{ž±R¦Û¦ªÈm»ÞüâØégò ŠxÅøì•w>Bë³l¬òÆx Õ7º\ËúýÞæÆ÷:« òÒŒ*DÀú°Ç-:Dó«/>_Ž4z¾xEWíÖ´Š÷æ* ûH9Öà¨KYáT’‰ŒD7WoºUkÝdèr»sêèØUB®$óÑ­F¸Ã—µ8ìa‚bÓ]!‰Âh)¼"{LÜ7Ý ´¿yÖ_LBqɾŸ?ú€ñ’­û$o^]p“¡‹0è•w>DNŠ—åêÚÒ®ÊCÀʰëË9è9ýú©GË(:¬¼·ªêš„*ªê¨Nõ#2Ç+÷àPuºRd¼[”\1ÓšÕ©±ÒAà:î3t¹P¹cÿ¡Ç¢I}ýý%ä–”8ìÑ‹]°j=ä8ܺ%B¥yi¡ŠJƒšäA@š‡€ÍÜ–|A@A@„*’1 ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ تHÆ ‚€ 6„*’q ‚€ ‚€ ¡Šd‚€ ‚€ `C@¨"‚€ ‚€ Øð®V0dfg9q*æBbJZzVvΕZWªUó¤1G-:þ~õë5kÒ½SûÿjŽ€Ì jÞAÒ¼êÀM7ë«?¤7W =®\©”Çù¸ø…«7=uúòå+ ë×cªãçWËãæSZ[ã¸R++'ýbJª§§G·í¦Õ"¬I5|O™AÕ°S¤I7%7Ϭ¿)á­ö®zª¨  pöÒ•›vï mÔ`ì-{wéITíq“ºFŽŸX»}WBRòˆ}gM™èííUM TM:BšQèγ¾†A]­^§Š©¢ô̬w¾ž{66î®[ÇÐÏËKôœªÕðÆÜ€@aáå»÷~¿|M«faÏÞwP@WÅ™AUºÔéFTÃYïFèWÅ«V%UÄ÷ߟ|yábòsÜÓ¶E³ªx}©Sp3çcÞþjNㆠ~ñèƒUË1’ärçIA TTŸY_ªæK&¨JÞ ‚³³1qB¹Ð]’´ Ï eè2€«¶92ƒª©Ý}¨>³Þ}0¯ª7­2ªåPt‰îš4N¸DUÕ÷Ro©`Ð2tÀ ãRRÆŒ2ƒÊ d\B :Ìz—,‰K‡@•QE ×l@½]¢Òµ[r U‹C!øªš!3¨ª—zÝ*Ÿõn‹|e¾xÕPEXU9qšg¢^]™-u•# ÝqC2ŒÌåX¬Å¢dYJ’ åˆ@ÕÎúr|)Ê UCaª»D\ÂwÒ2y$TsÀ cså·SfPåc.5  P…³^𯪆*Âzµ2ÕX9/)µ†µÆ æŠ(Üy™2ƒœã#O B  g}½‘kB j<~¤¦gܼ¦1sw!±^P`XHHÕ^Ì6õe^~¾’æ×­Ë£´ŒŒÂË—ýk×ö«]Û”ÒÕŸùYYäÂàxm__==3³ °›!r?—/_¾Z¦¿¿·Wu±ˆèâKÔb3˜]ÍUöô7õ *ûë;,¡ÚNL‡­u5RÍ5úA×ÜæçddÛ&&‘ú³yKâ“.ª{yzöèÔáGwNó÷++ÙA‘Ñ1ñ‰!¶º¶o[:@ö ÿøûE8¦xï/¿£„¿ðybò¥ÛÇš2jXé Ô¹NŸ=ÿú§_ó³sÛÖ?ôÿ¿/æDÅÄŽ<`ÖÔ‰:Òb€=ìw¯¿Mâ_>þP‡Ö--æªnÉ ̕ߪ›tUP:1˱͜.ö>F¸Ž©àRÉï~óý©³çÉbœ/á‘Qÿûb6‘ÿùÝ/JAÙÌþa厇M“Ú¥V¹g⪚õî‰vå¿uÕHÐnF·¯ág¢þþÁgŠ$bEãd'/¯¾ÿI¹p ¶ï?üé¼Å+6m«üA`½F@à•­§¯ù)=ð`\ž«¤ÒêÙ›=1Ëñ­sró˜ãü%\;Y•¢ð9KWUç•¥h| ÉRE³¾† Wí_£j¨¢j‹¹þzñ2éÖ©óÒÿë×/üý¥ŸL;’t0x6ïÙoÌÀÚ‡8ÞÆHS87/?'7×éä'â0ãS+UÓë0Öë1&ã]ǘ´Š¶™"?¿_±4Œ1¦°ó¶Ñ€Kii¦,úg‰ÍÓ)% €@¹OLF/­M*]¹yyß-_ÃzÊZ‰ì¯mËæÏœ\ϦôÒ?Þ`½{ô®Ûw:r(<‚Úi˜ÃbhÆ…‹—ÖíØ=aØ`ûÎÛÆ6wÙª-û ÞãGKpÒ˜=¿gçNŸâê°g™^¸zC¿î]QñS¥¿×lÛµhÍŠ%’i~Kß^³¦LðõñQi5²@1…y4¸OO^͘—Ö~µh)O‰„~ëˆ!ㆠR w¹pé±SgøYÛ×§]Ëã‡cv 5áYêGÔ«Uº½º3uÙgC%(#+›-ÿ=ø×Ÿ?÷çŸ> m7{þªuÆÄAÿxég¯¾ø<‡-â9ù%§¤wÞ¨ÇÓ»KG8CÜEï×½‹*-éqäÇfNïÛ­sÓÐ[¬BküÆá3?YQBˆvüT$#b†ŸÔÉp[­c›VίٳšCùÁvŠI¸ ópÞ6|©ª%xêè6SUAAP`:»•æéÄ4å81¹OŠI‹æMs¡üö­šs€qËŒà'”®n ‚o¦p÷Ží‡ôéI<' cÒy tþu³¦·ôé ÏuÞÇ-8X6¦T­Zï ìàrá<@]×/¢ŠŸñ”×áÍÜ»‡ñîgT´m‰hÔ xï‘ã°¾[‹«ûnuÕ9ÛÿùôkĈ(üêÉGD‚æ¼åéMŠ€Mj#ŸÀãiR3Ò9lë 6ÊÉÉ­åáÁN1Åvtkm`´´nÞ”N`É©©šýS¿È˜ñÞ^%ƒïãí­„q¤çã¼ •Æá·âö£bùÙü%Æ´_ÿlÚX‡ð^ÇUüÕ[wÂ`7&sÞ6xE$Æj@‹°P•Ëø^Všg¬K‚€B '¦O‘›b½½mSs…íí1tud˦W«£ýjÛV ëŸRû;&ŒÞwô8—ü“H³Ž ›TÓqø‰KLJ*š}jꧤ!;D,‡)ýë]ðg|D„PwNûõâåP]°øƒ7†(íÁÛ§pÐ2&–° P(yc®/YöW€ñC!Ð7»å ¦ üï§ßpÝ Jè·Ï<¹ƒ JýT‡&+]²µÆÒ¬×_еX…n€ ¤I˜EðÙûgêHF Ï×ò7eôpì°8X:o[@‘y*ØE “˜1ñ×YMVšgl¶„…@9NLç§¡ñVZtüÕù®æ8GT ¯]ÒÔ6;ZR(õ°‡Õ:iä°…«×#GÓÍV7'øÉu##‰ ®k3ü †D’΢_ÇÏfI•#œà®ío¸i¡L²èõéÚùÈÉSÇNG"’C¦ñÔ¹M«aýûèÒ$ Ô œ‰„jÆ–Ë[p0‚DQèJŸ8EÖ=š4D„ô´©`w(º›†n5wbùÉ‚¨LûÀ1ڃ摓 ““§¥®B à`ç@ØÁjÚ8ëm+7oWßœÔèð"¶;Æâl|ÀyÛZ„5Q)QÿäämÙkS3RŸòmÞµRåÿš@¥ML–É©iJKfò&ž4n¬xKpRùy2ê,Bst áèò]Mó² {´¹âjœ€0`êPǶý‡P¢vdåJï»}«–Ä+6-ËÆDøÉ-×s±ñªM0q›üñ>ðýÄú€°¬–G­ýÇÂ1¸úÊ;uißæ¡éS^{ñyäŒd <«²Ë· P“^‘¥ÞD’…¦!buTÿýÉWœº`&+»&Èæ‘(Q ªÛö„ýòÿÞçžš4ŠÄQ÷¶J¬&°Ží~ ¦¢ßþjÎ=S&:L_ê*zvêH“ 0DÙ¹m«Ø Iê^›Ã ö«6Eí×gýÎ=Ñ~ó¶±"£g}0<ÑÛޣሕ„*¶Ü›gj­ü¬©TÂÄ´‡v/÷í¡ /ÔŸ<òªùx¨H Æ9d¤—ÅŒÙý|}KæÛ¥+‡ôé…ÑRÏJ^|æ­ãX‘tù0«”h›ƒÇ¡ð“¬QŠ-„5ºŒ$›2jøã'YÁþô滜‹bUcT Øyó‹ÙHå¸ÖÛ N0v‰úví̈—âfËÝŸßü [ÇvÉK.msD·A‚@ @@xEV;WÉ(*%eÔqX#XQl|îþ»Õ ~¸NýÁrÉ‚Â/‹:‰œz¹Ëá%e%‡Í*u,ÐOÝ{'ZÛ0Õ)’MÒ?xOó&Wµ|Vç$’ï™l» £?%¶í‘Ó”3NØ€C{tÞrož.Y5Šž˜W4H¾C6ä8pŠaÌÐÅ^‘–ªß5q¬º\™’žŽ|Y]¥þÕª…êÒèÁ6ËFØB„©\ÆaÏtVšàW[X«Ú?“GEØÇÝUEAa~L%àn)w]áiÁ0†¬á2éÈ}u^4ǹ•ƲÀ#›•¦ÂË\¿xhÆTp!ÿÉY3Xx)îó£‹éÖ¯·Î.A Æ ࣵò_æ_Ú|÷<ÿàÕéZù (K¨UÂ#aq k܈™}Qh5bÑŸóVhÃØ±OP\ }¡è­ºŠÒ*.e©«`¹ŒOLâR <—ÚV\Kìã·§˜ÖmjÔIÒ…TBót]娪‘\Uõ– hTHÅMLÝàe·¢ÍÃÑóðŠ¡<¸Ea¯qŒâ··Ö¯Ý£óêqéZÛÇWû‚,ßa6.1»I°<©ëUjNH@[Ȩì¨Ó@ç±v¡atÏ´¸ñ(ñâ% ÓÓìÆ èÆë¼îÙW³ûZ$h.÷/$E×öN²¡¤”œ¤qøˆC7>2E–º nتK¶¦Ëñ§ó¶9Z Í+Ç7•¢ª71¾&Œlö8|-bOŽè”d ö±Ù”ןòöPiÆ‹fº v£©0ÓSdjÐR¦Hõ“GœùsøT"ƒ€ œŒóÎò"‚€ ‚€ Ø# ¼"{L$F =iÄ ÀÏ$JjBÕˆn”—ŠG€[Wrñªâa–ªD@$hU‰¾Ô-‚€ Õ¡ŠªO_HKA@A *ª¨*Ñ—ºA@A ú TQõé i‰ ‚€ T%BU%úR· ‚€ T„*ª>}!-A@ªD@¨¢ªD_êA@ꃀPEÕ§/¤%‚€ ‚€ P•UT•èKÝ‚€ ‚€ P}ª¨úô…´DA@ª¡Šª}©[A@ªâ­L}‘™}äÄ©˜ ‰)iéYÙ9Wj])Sq5:³G-:þ~õë5kÒ½Sûÿýº5äå®\¹’‘•¥yjz†m_qAîaëÁëƒ6´q*´îÛ.ͤ¢^S V›ÐƲڸ¡$ª¨”cà|\üÂÕŽž:}ùò•õëú×ñ¯}mJ–²Ìšír­ZP'##“S3<==ºuh7}ܨaMjö[ߤoÇÌ'*&vñÚá§£._q¿A^Dûñ•˜”µ’Aëa´·Ù²iX¥u«t„kP_£ØùÿBRêɨ(ÛjSÔqÓÇËjã–n›Z¨"—»¾  ðÛVlÞ³?´AÝ{& êÕ±eý :.—âÆRÒ³ž<·n×±ÿ{û£áýûÜ;õVoo/7Æ£Ú½úåË—srsç,]µýÀáÆ ò‰2ÈkéAûÊ;ïßûÞ©“*aÐ^눕Û‘Ž(Ý<Ñ'«MétÃ\B¹Öéé™Yo5ç\l<ôЈ~¼h““ßújN ZÝЦE1IVû¾p)Fw\"«ÍWsà÷»”]»BYín–ªo—,?ÿÜÝcÛ4 ±šMÒ9E$ÁTÁ„¦•‡ˆ€Ú‰³³³ç.[›˜$ƒÜ ÖjТYXƒV: òe|DÇ=Ëj‡ D‹’ì5¡Š,u.KUdtÌ–}ï;@H"KYNž  ¶ ,„‘eØÊ9!‚³œœœˆÈ¨½ÇNÞ%ƒ¼$tõ ŠŽ-)­kÏ¥#\ÃËÅÔªãÐ …¨u1«$w„*²ÔÓ,U‹×l@á]"K$‘+€*Ø‚08»’OÒ–hõæååeff®Úº«q° rK¨ªA»híг”ÁB"é •5‰ê8n—µ É_Cª¨äŽe©JIM ?svìÀ®¢KT2^®§U°ap.Ç=Æõ†¸iÔ‰`%&]ŒŠK;H¹¥apuОŽJKϰ”ÁB"é •5‰ê¸£§±6WÖ²$MD@¨¢’{•¥ê`øIL¶p ¿äÔ’¢T€-ƒ3h—ªÉTJ Cóóó³²²0¾…S¹uõ -R^:Â:òeL©:îPxDË‘ì5¡ŠJèV–*„ ¡Ô »D%€U†Ç` Âà Úå²Ç”¡-î•©%˜CÅ'%Õ‘An½ûÕ Å´D¹H~¥#¬#_Æ”¶Ž«¡ŒåHö‰€Ø+*¡[Yª8Iãë ^…™jÌÎɃMà_[5…Ÿ~µ}JhYçæå{yyz{]·x)-ÓÇÛ+°Ž_J-kVgÐsO1UV8­æW›1·Ï°ÅU¿nEà±Úš›-û+Þ~ÀÐǧ¬¶\:‚E\R*¦ Õ mX8áÿeåä!6ÒKJv.ë Î…ä´ÂË—ÃÕ7aŸ_P—Uϼ”`JPŠŸªj2boÚßÏW•—__P¨ÀR[Š,x#HIO/EFÉR㪨„.VKUfVVPÀUª¥„ ®?~í³¥ S_¸b§Ö6Oûd ªÇÏÏo_Ò™è ;œ™2¬WÝÀ29ûåç éÕ~ÖÄÁºŠ¿~¼¸c«°'gŒÒ1•`YÌÈÌ‚oQ.{Lå·ÿf¬‘-SpôŠrróšÔ3o7ãKUf›ñóƒz ‚$‡™RW].‘œöá¼õÑ.©ftnöÄŒQuü|_ùh1ÄÑ«?¹›x*úã;óƒëüö±ÛNDÅÍ^¹*ŠøÆÁAwسc ý kv[´aŸú Ó£Có§ uxp‚¦ù~Íîžšwk×\gwxóÛÕ‘1‰êQÓú#úvÕ¿óüu{7î ÷·;ÌRA‘¶ŽËÊ.c¯UPۤتE Èÿª} ­]-U¹¹¹……åvͤ¸·z#ÓS‘•£#Y¿X>²rruLqœœ–i_ Ç2‡Y¾møøAÝŒL))Šsž1A¹‡ÙRhvÚ¶=¦Ü˗퀵ÀŽŽ.—MpyÅê g$œŽ¾Àžš–yU_•}13;—oUÝÇÏœ¼|F=}ÔOûf·2þbªýˆu˜¸Ô‘º:Pê¢TFí•Ë6ʲŒbßÒu„±ñ”ðé¢MFwÿåÓ‡öê^Dô@«õïÒúRzVLµt6îbzVNÿ®­É»dÓ~//¯ß<:õ¥‡'Ó{Ÿ,Þdÿ3Ç|lúˆÞ[Žˆ~{îZ]c'éêbÂÛ³.EÅ&9|ª#U* ZíáÛ†ùúxCKÙ¯f0Ëí§?øêQ¤Ê¡`¦Â­ÿ¤ãðöïk½IYS^‘³žeΰa”}ÉsVGÑ3ØÝ±I)ÛDàC'Þs,òëeÛÙ8Ø=qÇHd^sWïäé?>_†­Û•3\ ]@IDATÛÿêGSiÝ¿¿\>z@×;ÇöíÓнøÐ¤e[þ°ùLr?_Ÿ¦ éßµÍWK·Eœ‹ÇVk⟞š®ª`Mùï×+á®Ã¦úzÙ¶-Ck6rΪÂ϶mÞx_øÙú?º}¹œ8ûÙ’-9¹ùì¶ì?yÇè~co$¡t›ËpµI`½.ci’Ý  s`·ßŠfßuäÌ7+¶3xŠ—ßÉÃzMÞ;:!ùïŸ-å"!›(ñ'ÏÆÿç때¶®¥[ŽèÛé¾I·ÏÈ\±í𿼿¶ïuyœndd’€ræ¸Ãûº`ÿÂ%ªfKè•–ñn`XÆBÈîjG˜j@è’ÝÛÎÛŸGíš7>q÷ÑÈûn½¥_×6«w=z:ºYãà#§¢yJ tÆ™èÄö-³Âúɽã/\J/(¼ì{£¤¬[Û¦MÕЭíÇ 6î>›x)$8èƒùŽDD{xz´iòÓû&üë‹e”¹rûؽC{w0=¥OMÅ\¿.­‰ÿôéâÍÇÎ\7øDƒ?˜·!)%Ý¿¶/KÜ´‘}Nœ{köV°'Î]©ueÒОoéà,hÛ‚ñÕ¥m3ÖF-Œ3VTrXN^%cäŽ)„WTB¯3mF­Š@êêßeÑÆ}šC½óÖìnß²ñÏîŸX§ö{óÖÃâ7ÐÆÎݳC Ö/xÑgã’ર1°ýœ‹»Ø¹MkÊâûûvn õ·+vØŽY—/sˆ3sL‹Ð?l:ÀŽ録ü‘Ó1ª1×¶ÞÖêçæý'£Â¾©Œ¥lŒN¸Á O¢yãௗo‡É¡RÂpÈò´ ékì[‡ T¦ƒóL-qÈx  *2¥´ÿÉ UûGÖcT Ö;¾两"»µk¦©0¸ÖMAú¨Þ9r:šÎâ'zBƒz´;y.yú«Ÿ,¡Û6 1Q0º(­š6â;ñRúÑÓ±á‘qØÀXX†tîý“‡ðˆÒXvìŸ !Ì s8âDIóÐûÃÏÂJËÈNLÉàÞOÃÕãôV/Ð_Iètk³÷xa6˜íš&Ícú±ð±Ö¿?o= ‡*ó÷OÓ*;b}7¨[Ë& ÕSý Ë5ˆJ×îÎ+0§®%¬îÿ«KdGÏ\e×Ñ\V&E7ÑxV†ykw¯X(,.Ý|fØÝ!UŸ5qPjFöáSúH½9ü!T‚ ƒ‘¡ÿ÷›U7nén[enü8JZ ýûOïþ× ³þüôÈ^¹ßÿ~ýÜU»Bê ¹ÆÒVOY»¨åˆÔ’§‡gÃúA¬‡ô ÆB$,”¡ŠÊ`yfçÜÆí3¥± Ê&#;§G‡PHÈ4Mçê¤kÛfìFœŸ8”ÃÜæüÔ­m3ØW(MBLŒ5ÞGqÚiT{ÍiÙòðfE@¨"g=§—* ²gåX|Æ–s÷5æ6äb,t&þôî|8:l*èú ÀÒöÎܵÐI]Ú4e}á Ȫצi#Î|ÝÛۮŒ‘]„ã/JÙHý9¦«ÝÂÔò²PîÑ}̈s ¦§¦Ÿ¨Ð²!@´Ï#òš”ÛÏkÂY­Ê Òr-†]ãG•Ššr^èfõ³_—6'¢â÷³õ¢dÐáécû3hw9mj޲©sìšÑë_®ØuôLq,O²›8¦u½NJ0f)ŽñРž™>0檞an’vhÕnrshS4¯Qx×MUÜ;î*†eá陣Y.P=d­ÈÍÏ¿ïÖÁJyHg!ÀùŠÛû%QüB³˜Ó¨ýüßßì=TÇ ‹C÷vÍXs6í ·j,Íy knº½øú·jÉBÞç0ý˜]ÑÇçœö·— DCtë0™D ¥C@ôŠJ‡[yæ‚“¬‹ëܦé{¿{DýÒ«ì­ ‰ˆ„}ýÏÞ‰F6ˤ‰¶ð1}t?þt!ã°it)=“³»·­‹a ñ§¼ùÒ*üÈ´áüþ× ÷ª'•î$?óèm|s¾§F”²Ñà`ÍÚ×4$X%–owC€sü†½Çg¯Ø“2п6ŠD ÀUp…÷½Wn?ÌñqŒ ”j‘t,Ý|ÐkJ Õ¿Ú¾-›4X»ëd \O˜C(‘°%s§I³ãOEß?é©YˆP+ðv­ ò¿[! TQuïn­²ªʵU..{<¬öÕ|qi,Æ#Ý`ÛØâ,|)xìam›‡XÌ+ÉjP OÎýåÒ­¼Zm_o®Ük­ M¢X͵D-ö/Ζ†°Æ¤Çôé™c>Y¸qÞãdaܲá!Éâ–'TT8Û$z¸YždÑ ÔWž»ÓX©bš–XŒ ãÒÆJ6‰ÉiÆrnº0Â)‡ò©â^Æ“«gs±&I"•FÀ;|ê°ûH­'`ÿÈùe¿6šÒÈOA UT ÐÜ. W¦Ï'$ŸO®[ W×P·Ã«F¿0Š>{~æÅ” 4ŠÐ¶6©ýåÙH“ÛFöáOáÁ°y­È¼² ´ÖàJr˽HÅ%=ËÆ’CÎf j&¨ªÂ¾Í–Ј-ãöèZLM•Ÿ‚€ P㪨Æwq9¼ zîäóWeI7?Ü@¿­|߃KÝö~l¬³<‹c Z,AåÛ›Rš pó" ÚÖ7oßIËA@A <^Qy¢i½,|ýpÃYé’K»§¶.V·^—¤A jÀÏÂÖ-ØŒì^6:„15›•ÓÛÛSë0™žÊOA ì¯¨ì–¦„×>[ú‹×¿ÕÞ±]ÆÏ_¿9·4eUQ¬˜hKÜUÔ©Vn tþðμ%›ÐVÜ}` @Yʶoú' 7½üÞûxƒ¶þýͶƒ§ŠK ñ‚@Ùª¨ì–²4!°««-aXȤaÑq´Ñsµj÷líW‡nžHO¼Îe4TbzrŠ8·ûè•]¾‹`’a_Ë{T㡌%XlmIÆš€½(ãwøjöë‰ÃdÆHl(: ³Ê=6}äø"—ÒxuÅ–’¾QÈ#½ ‘ÃH­›…ÐάœŠ% #÷·¤Á†‚ÃZ$R(D‚V.0–¦nDÅ&FÇ'‡4Âëj—¶M•5E ¼ZqÍúeï›ú«eÛ¸üŒ™;,ÿB²p!(8¨ŽÉÍVL°À† Yv&§fbœ µŒÁF0žh1qdŸ&&ñö€U˜N­š<ïøe›b¯!à߬ÂiviÞ_ò¸ØZ¿ç8û(6f:¶ }túû ÞVPùëÇ‹;¶ {rÆ(+‰Ý< ôÄœU;·<ÅlÅèâs÷ŒCp7à ·ôÀÅЩó ÿýz%†zwlùåÒmÇÎÄ@‘`ïQ™1ÓÐýùý…ØšþÓSwÀõyùý\Äùô’û±›€©<3âänɦýxr|ã¥øþfùvL6PÎïÞþ¾w§V-Ï]½ Gu,YOœÃöÜU;1±Óhב3˜˜âv! Ãÿô.¥½ôß9\Hd{êÎÑGŸ/Ùzèä¹:þµñ 9…I)Ý0 厀ðŠÊR«²6Aj°*qt®TGÛÚ·è8ÚÞ75—çqB„U:.$㎠_NØÌ·wóDûòòqC}Ï M¯Û}ÜÛÛëöÑ}±ž‡ïÏbÒœˆŠտˈ¾0ü%âa}:âaê;{}Vß_Ò¹lx˜GЭíËOßqÏ„A'ÎÆ/\o3z¤>JFG"*ÒÈE¸–Ðö?FœãAEææå³å8,͘À}ÂØÕä̃%ì¸fæäþ°i£à ‡ôÌl@€N…ÄÒ³= Bpˆ†g1X2ðª5¬€—4bð›t)“ÙûŽGáPöÁ)C_x`"žávc ³hÍ©áKoð‡•í̬\Œ|âpj†ŠFôéHOáºãUXìİþ”á½°U‹;a¼|(VFÉš:ÃWx/aH,ÛrˆÅgú˜~ÐXZ4i`2al§„²# ¼¢²cXú°Íj’‘‹E]]ŠEÇÑö¾©Ù(ä¹{ÆâhÓëwå§vódò/ÍáŒÅ¦:‹GF.E³Eeæä—Vöø±îƒô˜ÄKxgó¯íCÊ÷¤n¼â`kdG¬äûÏ&ýëáRC¾Âz;nA¡é¹¥Ž=e¸•çâ1Ïíή‰c/›ËØîmqêŽYm8š_/Û³ó±f#¡~>œ¿JÇ¥ìšð'ìK+®=žµzÇQ Fè×™÷¿n« ä †¯±gÍ‘ èàõbzƒƒ¿w¿[׫cË_><ÙÇÇëïÌS´æ/œAÃA+¿ U¡}:³Áá~¶ds×6Íž˜1ªEhƒø¤bèe(*„\ÐExØÅß"F‹µ ³9vÄQizujÉIlÕö#˜;§Ç¡}GöëŒÐ}ágñ%„§#Ì3ÒlèÜŒ¬\’AE1Bã݈ä#TÂ+ª8lK.bèTô#È#Ú¢ãh{ßÔJi@™õѹ87OÊ5þÏ©—oíØ¬¸ô¬¤¤ãúº›$à*È^qRñÍÅ‚·f¯† Q‡öÎÔÌìçgù&³ñÒŠ®IBr̤a½;ôhßœ=[yœÅIVP€?¹l7•.Û˜Ckà@H ïÓiÓ¾lóö¥¹ÚΚ”þXd,4‡ÂzÎö–þ0ŠzvhGE^ùdzÆAyÀìŸ÷É¢MHձ׊?.m›ÑkÔnØØY¹y¢H?~PwEXºGRÿ·c\ÝŸÅF9Le ú™;DDÖMmñáAAB±è‘¥} Ûº}ûÉ>a!õ!‰XÊÎ%\lÖèÀ‰³b´„d)i™|«rÈG¨ „*ª `-‹´ “¾§Î%¨eKå±è8ÚÞ75ò{JxóÛUl HëUi.ù—†6r%½GNn>‡QK¯*‰ÜÜPàò;élÌ0ðÖþýš]ìp¸jp(":;ÇÆH@µEAõûǧÔKÚ°EE¨(ŠÔ½­‘4?pò'Š"_}þðÄí°%Š+Í=ᇡˋã®*[`€Ÿ’<†6¨›ˆ"ãã€ã¯-éձŋN ðÔЃS‡ò‡áoÀ+â{ÙÖƒP9Ü®ONÍøÍÿ¾ÃÓâ3w‡G±øÏ†EM¼à íÕU!¸ÎxЅĉл®"M¿ãp B 'ï<|¦àÿ}¸hÍΣŒ„¨Ø‹mŠˆ'¨(ÖNEç5¤uÅ߯ÝCQµùT("A«PxK(u"øÆèrxÒIT¡ËŒãhXÙDâ8ºaý@ýTXqP@oºs›0å¹õí±»"eฆôáPÄyö{7Oºûk™õôâ9Û½óÝZåDÖ¾4‰Œ°Ÿ<‡¬Õ"¸Ao~»zû¡ÓwŒé‡º.~a•±l´F˜ è£pc@ÛîÔ½Tþº]Çsó vog,“›hÚçOTRìK3¦w·0îYðR·ýÐ)þXj I•) Æ ‚sÜ n¸7x6yÿø|Ú?íZ4ž9ÞF*?¡ mÉ ‡Æ èB€›7°šøƒ]ôðÔapzümþ‘cö@¡ºV­IÃzò­´…X‘£‰;ê¶}¾ï‹Šia¯kRêÒŠ«EâAÀM;hnÒÑòš‚€ ‚€ PB•<A@7A@¨¢›££;š¾9ÞAZ)‚€ To„*ª‚þ1ywÂ5vf·ÃähšÄ˜|Õ~£ÈîÐ…¸©"cöα‹k†“Bx¤ÜŒK&ì$‹)¥ü* †¥šh:PiUKE‚€ p³ ÚÖ•ÔS©Y¿{öõñDxXïŽøl¢n š­Øv˜eZùFõÕO–`2äŇ&A¸¼øúìá};a®C;šÆnÇâû1…‡'׿ÿôžø‹—°Œ»(ŠÂ\Û³w…LqX‘~Oè'{çØ¦f@pýéÝù·àµñTô¨þqŽmò°Ý«CË/–n9\K⺈Fê,¸§Å˜›®Q‚@!Ç¢X䪉Up?<2îŸ/ÌÒ³HA@p7„WTI=Žñ:¸;wШ^à¾ð(*>v&‚ã®±ý™6,.)»ûGç⓱VÇÓóñɹù„ަÏD_ v«1öˆ½¢l$åswÒ«=¤0ï°"Òè½slûfP¬(<á o§V„M¶ñW•p1ídzð†ÔvëÁšjÌÒ±Èo‘®Q5âø”ö¯Ì¸µÔ1˜aÔa(®dc9Nx¨X?Âz!G ]ldLÒUÓÉ×<*‹T(A@¨y¯¨’ú¢ˬOß5»‹°ÏONcÕ^°n–acñ5msu™™“ ׇøkkw"‘xCŒK¼DGÓxÁ~Ë& óèT,4âÜ#61å·ÞÖ2¬!wò·<ÅF‚‡SE¸ÒüËû •¿XÜ_›œc“Ѿ‘1¶ª)YÝy6yئ(¡vÍmNEÒ2sTËMYˆ”OÍF 6Ņ̃ÄnÐkŸþ`b+î9 Ń£‰ˆ³ñøþÄÔ²öÑ¡ðÁfã‚õ{ðŽŽ‘­GnÎ`¶/Ù!ôƒùŠã¡îÙð˜Ý~0£,ø‡Â•,Çß};)£ÕNœcg!1äkD׶;[½ËT‘rMO/O¬Üâ—R¸ÕdK8—„q<ûfÌ_·§yhm†Œö°MFÖbüic¤ŸÆwmÛBm鿯,Åá ñ5ˆlø”Ø@‡O‰óQ(rø”}»´6±wo‹½õÚ¾ÞÓFö  Àé›7¢^ B|RÊW˶õìмoçÖÈ‚á¡6oÒÀ¾d¬;jVë¢õ{I†*•NÔ÷sVíÔûtj…k$ÎL†:•rÞÐÅ"½÷ÖÁ¾¾ÞŸ-Þ§vêˆÞœ^P×ëÞ¡ù¨~u#«a€ Èçll"NT£âéŽà :øEaŨ†­­Ð&Å_LÅÅÞ¥ô£,’LŸ6ª/Ë‘6ë_¡UKá5¡Š*£[mëWÜžm>íq‚ÈyŽÕYtjÕòÀÍꆽáüE[î i"Î'ì:‰Nž_ù©M³dóSq’à[ɹ0 ¼jû‘!½:pÚ¶¯J'k¤çÃÒ¿|ë!XV¸Kûˇ qŽmߌá}:"ÅöÊÂ7¶w;SPpùOMç§Zq8ŸAA ujFKLYt^ ÔHpXfϧôóõ1±ã.¦AÍÜ9v€”ÇGú‘ÓR_/ß~üL °L¼¥.Ö¡¹Ÿ™9–Ÿ­ÂbñÚaÉöPÒçäåómÏC}óÛUëMl“‡O§8`G|VTE#¦6Î7¾YeâÔF'$£Q7}t_xK¤©¶è¶œÜܹ«vï8r«ÓFöêÖ&Lû#«¶Í®Ð†qb<·åÀ©W?ùaHÏv³ v}ÄQ…B^c ª¨2ºÖ觘TÙ¦i#6ü&nÜþ§÷àèÕ"6 ÜtàúûëeÛûuiÍùñ‰µ£iåïºuX#Õèñƒ»³©|¼p?»·k6}t?‡©ÄêÛÞ96uæpæR½ ‘È›ø”ujçD噽>¥zÅV= ‹¢„"cam2¼aáp|72 UúÞZ¢`÷ï/—só黯À µ/¹8(<Ô”Œ,´Ü¨…ÒªkÚÈ>¨ýë‹å+·& 1ûÅ8´: 8µçâáÔŽˆF3‰ù¯È¾…Î;þÖ5Ëy²>U$Qvvl³Ø¤´G¦’¨8LA|p“={åNèH +.¥Ä öܰ5Ú?–A@¨Á(rœo>>^^%YÕPØó)Õ#[ñOÚÑø@‹¿óÛ‡UØø²ÿÄ!=òò !qhìK.ŽZ¹¡½; ¨ÇyC%P3Ò¼L°g‘þå™ƶY Clù×¶9»U·’¥tiœåääFDÅî;àLH"ç0‚Ï”a=o:xKví[6ñ¾ñè<¯¥ÃÂÈBµ© ë%ÇCõ¯ífcˆ:`¯ÛP ©1;á”´Ì@Sd¹ÿD½://?33kõîãê KTîUÔ¼A©a½€e[vPó^Sިܪ¨Ü!•› Èõ ðó¿ÂßÍÔú*m+X]JÏòß·»ÖåËÚÄ@999‰É)gR†õéÕX¡ÕÕŒÂAixŸщ—RÓ°f¼”¼E% ³«@–*jŠ€¢‡<==½¼¼ra§V­ƒ'ÏUÓ¶V¿f•Ç•+ V¯¸ð‡ß^ɱ™3­ˆ|Žüüì3=‹† —ð+¢–Y&X¡Tt("…]T#»¸"^J¨¢Š@UÊnIUäçëTÛ{õöÃØì¹iZ_u ¥5;Ž4¼ë_—³}{ì“\´¹#,÷EyyyYÙÙhcÊÍí¹/XÕô·Ù ÉËF—òJb·E@¨"·ízyqA –"‰ÐEõÅ~¢Op䩤”ŒM{mF†äãPJLÉл‡w:¨/å‡yäÁ¼¨(ç¹Jñ´ˆ*ÊçöYzVN½@¿R”P\–Ë—¯põïÄÙ®æ—Æ¥xœ eçæÿ¨/¹yU&À‚0†ªEB¹Ô•îœXî ¹sïË»»;HÐàAùùùùdg{mZç×gÐ÷žž\PÇ ¨»£Süûc ?k­B¶6$¸wÏì?ýÑãRrAllÌjòúþ}úŸÕµ'€ /çåçs-'¯ Qݺ®å/>5äïWËwB©$í[„Üë 2: Ù´/båŽcÆ:Ÿž1â«;Û5 ¹ïÖÆøJ óF™YŒ€É€¯´ª¥¢›áݤ'ÍÊEùûû,[êWXX×V¯Œ´·f¯V.ùÊ§ŽšU ȼ5gu ŸïànÀ-¨k·Ÿ|æÓ¶oy9--îÙ§2V¯*¯7F&ÊÂyùyåhwÎìU»aìÛá™»FèÚêÔùÄE› ãÇø“0®ŠÈhŠÄ•)fÂள&ôW! ‚LOá$Y7aÊ[ºŸ—¯\@`Õ¢Òèn¹„Wän=.ï+Ü€€¢Š<£"½öï ðòÌõõh“ŸD<è×Inù4þÅ_äìÙ}%//á×/$%Õ¿÷>±,E)VGYÊ1æ¾pé|Â¥>ZLÚƒøÖa c.¤8=}TlüàÓ°IúÇ"ãÚ7o€h€I°íAÛ4nÔ°nݺPEècÙÔ³‚ê6}ëÀI“¶ÿõļv¥Ìzëìè|àÐÀê `츲„/$Û‰:µ Õ…¨0Ü#È„kø~ž4¤{TÜÅõ{lJf[NÏÊ}tÚ¸JÈÈÎÄ$BÇàªKâw럖™³ãð]ÔœÕ{ lø[¸áæÓî£Q§Î_À%l¤KiY” ÕEuaPiµjÙ\hÃy‚>Ã]£.­ì  Çò°ì­’ª-Â+ª®ÉÈÊÁã}lâ%SNHÔQnëY…´·< Ur{Äù,¬MC‚»·oŽÚò¬@Ê*orV¯ò8yFÑ•¦-=§NMJN¦†æžžéÙ¹—ÒÓg¯ØñíŠôfpP€:~n¥—Á´ÍÌÊÁ(™ðõêÜ,4$$¤Qƒ ‚‚‚ÐÇÒv“=||ÿß_½CBR¾øÓæÌ.HHý뫞~eÿj;/ßoq´ÊF%Vñ~¾>· ïÉ£ø¤Ôçcó œŠñ«í}<2>'/Ÿø“gøFEçÎ1}áëlÞw1M…WG¥Ÿ\×fõ[žŠnŒŸ2bŽGÅÃjºcT —ŸKHö¨åŸi®ÐÃÁBý§s«&:W9®ÔR–CQR„ TQyv2sï\üÅEö…GÆ1·ÙE˜óeT`,ÏöUbYñSqùy)ý(Ž9»´ ›6ª/žDÕ±{ÀjU—sr.½ó–‡‡¿§gÃgžË Eæ,ŸÌÀ¼¼ÜüüÌüÂ¼ì¬ YYh•8¤ï/g¤&ÝK÷ðð óð5;ò³Úšj“î*ñçQË‹7òô«ãà_7(þPÆ 5jT¿~ý€€Äg`¥[ÍoøÓ¼›7OzíUL;fmXûôaÿyÓ+Øæ:·ú|B‹Ô}¸}6 kkÕª“çxåÆÁ65 Ö1(!„e6e|O–²ÔÌìf!Á êðg½ÍBꌈ†QIDŒ‡'f›T1¶ïq;;” Áñòò¾ŠË‚R_Õ2^Ô<$ 2í:è_»Yãú׋“ P¹UTnxsÍSõ¶C§ñè4md/ ˆ‰eîÄŒÛràÔ«Ÿü0¤g»Y·æþw¹!.•©³¿)LHànyýƒƒÇOOOW25¸ „³²²°õT`3‚gÛÆŒ»ßµÚ SRRÌf#%"pâD¿ÐêE\k¦ËÿCðQh@ý ,ƒ9T¯^=¸DDŠQ„V–}¹õîœéÝ8í"L;æ>ýÈCMßzÛ§EKû”%Æ(À‹€w}‰ù‹IÕÒ¡Ec¨¤]]Û„í9~6!9}H϶xP!Gn~Á†½'ÃÙT‹:¶ ­íã I”œ–I–˜Ä”=Çζ mPLÁ΢{¶ÿö¾>®âúz{ïê]®²-÷Þ{/`c°éBKð¥‘Â?B‡$ôfƒ±Á½÷^å&Y–mõÞ¶h{ûÎÛ‘Ÿ×«biµÒîJo~òzvÞÔ3oßœwçν ;g¹\ òAÈúu©êƒÖAÃáè® îØ›«¨•×À½h[Y”ÉÞ`XQf?9ÁøÑÚ½ØÇüï0 ªVðÂ1ºLO=v!wó¡ 0§öÄ’)JØPèS/SØU«pêtÚÏ>Ãè±øÇ<ÿA€€)" ÑÒ›¿‡EýÒ¾øªô™§ìyy.­[iÐ1’N™"Pàå±EŒf+”¦Õ ©XxSˆ ÎSK'ëf¤’¼Øå#wއº$h"Q XqC‰š|ñÁùcqþp­˜;:ªÍ/*ÍÕÅ\ öÒRÝšÕTM|¾æÉ§ð?fóBöŒ„B!$"V+ÔŠì´¹‡hV䨬,ݼ ÇÔQ6òW¿–¤¥Qµ… w&>‰¬˜@\@@†!µd”üø„„O¿(ûͯ,n«µìÅç#û{åÝK[R¶cò€÷à¯Ñ¶À”|ÒrlNK|ªe¾2„ +jÓtàíùZaùñ‹¹Ø8c¤D·…Í?–NÆ èÑ39«Ëm‹0ÚÚ?dÙ©#EÊ»—ñãâé†@ÈÚ nr„F·ÏJWýCã°³x<éô™ÑóêEt=á+¢‰‘GfD}Ä–«TÆøŸò?¿d„iG·»êõ×ìE…¿þ êiy%™sÜÀƒz%vd‹L[ !…³,ù?X'à^gË¡óP¯†.‘ÿu¥’êð¹«í‰%X}ºÒèCh¬ðØeؼ bK¥êGõéYû1;`® ‘@ò N"úM¹g`ø‘‹VI¿ÿWîkÅØ§ÎðúJ³ùôo8‘ó÷7ª£¢tß~ƒt_å(+‹yõo¡yR/%.¿a2¥: +ò±­P«ÓçUÞ9i£^ÝBÔ„!½~ÞÐEñÔF´° “-°Ô|ôŽŽ£NÕ}+¹ª&B7Å\F£áýwqž5ÄüñO¢ÈÈÀv¯“Õ#Ÿ‘ûÿü$FÆ];Kª*á1 ’¤N6Rf8 áŽó¦îç BPu ˜ë3áûYK—,¸ €€±KbäA[s®P:,G©T­XéGoj?ÿÔY {Ä,É„‰²©Óü¨¡ Q.»'öŸÿb )=hÁ•¬½¸¸ âÀ ™A ”`X‘Ÿ³M ÌÁQs•LÌØ%jˆ€  :r²©Ue™ÌA æý÷H=ªâH¥­­@ºo¾¦Jq¹P‘imñ®œgÐâ?þGE™t²çç?¸Ò’y©+ÂŒA Ô`X‘Ÿ3âaEvX)d(‘4@­,ÀèGq¦H[°feš@ ܈åÒe~TUýþ»8Q…‚Š»î¤¦úQCW."0 ñó/y‰IÖŒJ{Äx€š&00„Œ^‘?³€ Ê1kø8“Ê:ƒPhC)1Ö™ ñ­‹6TÆm58zæ ª‡áˆ[}LÚš•U·e3*àÈdš_<Þº¶™ÜøII F¥ÏýÊzá<ì_—=ÿkåó/rfÎ ,ÙZõ^]a >V£5Še³F ꬒw*a¶Ö`:w¥`÷‰Ì·Wï ‘;' &j[…“™A VÔ 8M^ŠŽ@¼B5™‰¹Ð,4€ˆ4›‘¹0°ùU¯QAÑ]Kü¨WûÕ—¤”ò¾•¡inÇA±ŒgGýö÷ü„IJ7W¡¦Cª Ýc'û×%ƒÑüåæc%Uºe3GM–Ö)Í…€äMÖgüÞNg¯Ý}ûƒÎÓ”ynÿ`dJuqmk?oŠa9gt?ñƒkÊ‘5…!: ÚOþKšRÝÿ ‚"=«Ûµ5pdrEÛD5âðhÆb^{°L[Î{AAkûß‘ÅjûbÓуéù•s¦ŒèÛ)) F‡1¾°rNÞøùÆ#Ñ—˜ƒ@`XQdŠ3„¶kW)hi4~ оû†å93¨X²„Ó)¼À†ÎÌɦMYõ&G*£ºd³µªc DPÑûyFIµþ©¥Ó»%DµªxøfÆH1Þâ*ÆÎ¼_…ï<†ZÏVäÏŒ_ >A‡?ðyÊ:F¿+a ¶ÚÏ>%™•÷­€FKË ’œ®º:ýúuTœÇS.g4ŠZ‹ßíó‹ Œþû0—pû¬^9ð#%Ê-®<“]´dÚˆ®C‰/F}23¿°¬šy{ÝLÔVä?vLIpAÀ^XX·}zË‘Ëá Ön¶mu((Ÿ3—ÕU¤~Õ–"‚„„ÈËn gÅaXìí9•­–C—¨-­‡iYŒ:J-ß~,‹1~¦3jÝf´­CmF˜þ0Ú/>#^Ï”Ëïi­1k£Ù|1ûªhõjÏîk›D¥ûjuà»’5²Yl‰X¤RÈ¢£ú§õ”¶Þ¼Sk‡ÅU(xI”Ç–á„)Ô­.¯¬vù¬Ñ[—¨)@0êé£ÒWo?®3˜ÔJãpškpf‹ÕátB„†» |>_àùãriHSw•ΰ¢æÐa®1t•†0¶X¬¼ç¾–¨°´lýν™9×ÅóS¹×PÐ$Wè¢5,§¥å•„uNب¨ÔæäænÓÕq8ìô^=NŸœÛ®ƒÂÁ´Ö½3«Õš•[†½|Âoa©Î— cÿnÛ±¬Ü’‘ý»w5‡Ó8 ]VYUPZ†_ke¶º¶Ž·M;LU55Ñ"W&+äŠ(&&2"62¢[b|„š1þTÊšºs˜tN‚€>Ë JÖ-tÒǽ«7o;x*#Æcö&ír¦™RÔfÅ-˜ñÔ²é—Ö ƒ˜ÉÙs"óÕ÷ÿ7aø{æÏæñ‚lz‚"L“ÉlÁétµ\ÒÉìµfrX»Z!IÂãñºˆ¸èRεãç.\¸œm´Ø—˜k–±ËÄlm<['ù,+må²ílê¤4þØN7ßÉâ9ÜB«[j5I-uòìÒè3.µÝÅGqØ Ø·ïÈéiÝRº¸³†µê×Çdf3œƒþǵT§y<Õ}+ZÒ{½¡îýo¾/,-§ÍÞ”|ò)(6¡%5t¾<Þfr~Ü}²¸¼â©ËäÒ`H„»$f‹ÁhQ)‚Ù“P˜n•\ ׊D àwzV”“WðÕú eÕZ¯:ž“%¹®â Ø~JpM.¥Ö_eO=—QvèÔÙ”¸¨‹îH‰#Ó ò Tie$R:CÉb1™Íøƒ…qŸÏãrEB¡Z©P+äøŒŽŠŽÐ„ÂýЖ>0¬¨-è1eBýÚÜ&z)Ÿ3Ó|w¡`µÚÞûzMUmíó+g“MŽªjëùK(ÈKŒ¦õl¾†Î}•˜ÉIüàûÝï½æ…Gî¢Äˆ¸b´ ØìjM«v²™‚!Gm­Ê4"§‹OÉ>Â5€…èêê° f4Y`ûÖ‚ñ áL,–J¨¿¬«¹_¬ÛÁËŸ(Ù®áµ}œŽñü,kk#)³röªÿV?x×¢²ªêKW®ä•:]Ôfãsêø,ŸUÇcYÝ,¶‹ÅuCøÄ’ZXJ“CìfQ;¿"/%!¾gJò€´^ؘ G±ÊÚ~S150„(n›M#Cž zàæ{ J=•o7m-,+‡}<úŒ·åìERP:qLó5t‘«@æÉ¥ÓþõÕÖï6m[¹p^PFÉ‚¬ªÖàD GAéCH5 mb°X(åâðq8]¿ëEŹ…ÅWó ò‹‹àÊ·i zÈù熉~l!jx…c¹Ÿì2½ðñêyl{$÷z_þu·BΩsôÍÌ8 ·¸e:g¬Ö¯+LÜ™ß}ó¾C2‰pp¿~“F MI¨—<5SCè\bXQ0çoxÏC„”$’"ÚøŠDxöðyf+.¹ÅBA »HÊ"3žbaýëžV›ƒ®¿…UÑÙàvűgO§0‘0BÀ°iƒ³º–Lš,èÖ½™žJt5¯àXÆElœÑ”E¬™Ù¤ p`ßfjèR—€Ï]ÓF¬Ùq|ÒÈ¡É7ö:HðÓv:¨M4Ì]G6²máÉ @ Bè;œFo/_Ï…bЙ‹™V»“Çv¨¸EÑœÂTa­˜­qôB¶‘ÍrA+ˆ ¡Œ›msK<bKǻܔˆÌ,‡í-úÌEˆ·pºÙl·˜ms ±¬q¹95Îä2Gï3gk¨]¹øèãÇÐ/,Ø*ÊZ8éí’­¢VÿÖ·»QõÂÉƒÇ èŽ§Û_lÖ'yéŒá¯;`±Ú÷àlï¶á2úøÅëBpï¤ ƒ{zŸ±üäçÃe5$3”dÇ è6f`‚ÒšÖX1gÔ€ž Þõ´0¾ýXæ± ×ßxfq ó3ÙBÜNÚ¯¿"ýQ?ð`óÃ6¶b6í;¯¢>fo¬—.“²¢ô>ÍWÒ¥®¥½§²~Ú¹ïÙî ÊÀáˆÑé¼Q¢‘ t Ô°„ZÇöçô¥¬õÛwUÔèd\m*÷l¼4SÁ©±h˜³>…ͰÌ,õ†ÓAÁ­lc+ S‘¼<ü¥»w–:úäVŒþß÷;Z¾`^äÄ6VÞÞÅVÔÞß¾~‘€ŸWR VTY[gµÙ!6jªÌõ⪭G.ö勒!Π^·Üa-ž:Änw½p}Ó¡ ƒÓZdøÄfw@?±Q×Ó£ÒS{%Ýb²™!Çjª‡Lzè G°öü|ôG4h0þšéÞ­qÀ»²ªújAñòY£¼ÍÞ¸ívkÎu”åÅÅp™³»^ ¥i#û­Ù~Ü`4Ê¥R¯+@ ¤#ø×†Í[i•6%.ÒûýªÑúŒf+¤ÑJbµªÑ,T"î%x%ÃCÌ[å¹…e›¬´¥ê=v™––èð|Õ5ÿ]óc~Iy4ïÚDÉìXux:´AP=h,á¯Ê‘r±bþÿ©œ7yüÓ&…²ÐˆYÞ:ôi´±¤u~)õWZ¥.¯irû¶²Ö¥–-Ÿ9—òÎçù°"ìà ìIñ$ÜskvžºRP¡Ô«af^/ùz뉌é³óxÖÞSÙ/=2W"ü¸çÌÉÌ<׿g|FvÑÿ»¦÷ßSYùÇ/æþíÉ„ Î]¼V’§¹pµX£¢ɱaÖ Ñéè4‰7ýÛ¯XÙü ˆÙ›óÙ9XN|ÌÞX¯\gÙ(.ìÏŠ|Q$frÎ_¾2nØßkíÿ¢Ež×9~´ŠóñÚ=Ùùe Uئ;¨çÝ3F6SφýgöŸÎþè‚ô¬Ýur`¯Äô·¼’‘²W ÊÿýÍö§–NÐëæ+]¶™úÛ~ (ÔÒöºÚ§†³™—?ýa=ßU=^òc$zcé:ãÄ}ÿŠmâæ}îâ²ò'î½Û›7‡ + þt€jäVàX)¸âͰ¢Qý»á=ÎÊ-5Zl y vÕqÉæp=O½ß«dbúMONêµÒóÅ'âÈPR©»B£Ãú¤ì8žÙð½§]N|êêÌ`]K§_·÷,¶Õ¶|(™Ü@Àš•i9sßx‰‰ÒÉSn$7ò?^îm6›Ñh,)¯P+¤Þœ¹mA"¾½} ›­6Ü?‡3.J¥Šqë%%y'n3lCi(®¢!ÜápÎ »ÁÔé íÔdÉW‘ï-¬¢ó´$‚Öq·\¯%u6“Xi”R˜0ÀÐBößTÿžËÉÎ+{lñä¸HåOûÎì>‘Ù·[|Ï;U£EÆIë“KÐÜÙú2ÌÛЬˆ’póy8ªí]sЇ ‘+Óeé ÔóÇÍòPAj…©eÝyÿ"§/fA…9ž—9D²žÏ¶úWIS¥â¾Hc¬¥tzB6àIPpÊO\¾wÝö=KæLÍ®2¬(øó‚ô‚"üÍÓ4¥ù>‘@r“12bHÞ ÚŸo:Šˆ”fŒê›‘[\åÁ'ž•W†”‡ŒÅBR¥­;p6šLo¬ÛF²ýêžiÞùqOß5u(DãÏæ”V7)Ðò.Âă…-(RÝ{_ó¶’¡˜â1{cÖÕ}(:﬩%CàÅF{åÄÅëßn;Š»‰0ú‹ÌõcD Y4,þoÏ2¹EÅŸü°.žwi„xÍ­ì±­­rùÒÓWÅö¤ÎBšõE×OXx±^›°­U·Où8~v?׎‡Ùã† Š‹¾E=£}lu­-5-ßꊙ­A 9Nséz xIjìmtaý¾ŒéÝ]8ž>hF7% _zxîŸ÷Š™ÓGÞrbˆÍ¡ÞäˆèÈêÙÁתb!+\Ä(òçóòAÑ="UQe˜’8ÊÊêvíD×à V~ÇÂæûˆ²"‚Ù‚ûÇ'³³FKR¸š› "úrÓ!™XîòË»§&Åh@z°ÎádvW/^+&E.^-BdX¿T,{[à ú—'á€vm~Ú[ϼ›—zdáÄÁ}R.äa™¤/AŠ@¯ýDP‘Wr“ë{_¥‹@zAÇI¤a6"ÙòÎQö•¼Sn—ˆ…u&30¼mÎPË0ºÈù^ûtãß[ ¤”‰c"”i©qP3‚©¾¼’JHzrKª®WBþÖ·{<¤;Ec÷Í‹ÏQz í“züµì¼R› CҜɾZXN†Y­«{bÉ”¤ØˆíG.@¦H—E€=¿´jż±Ñj®â+ —×€L÷ICs”©sìP½~ƒ”U6Løãô½F¿•:íüÎçv~˜Vš³±ïÄ?‹ä üØš„1òÈ~KYD¡$_Qýit‘™2™'P EÙWª¢Ž©rx"UìP’ˆRêøQlN€¥'ÝÇùÛ‰sÁ›é΄N$À£ …WORb#@wTr±BFíDÐÏègrÈW‘‡;ˆsº'DBé\N‘T$èÛ-ŽÎŒ^³ðnçBDZ\!~6»ûk(KÒÓ»ÇCLJ׆öIÆ%$Bâ½dÚ0º Gtk¾Ã>z®Xr7§Y‡¦`XÑ¡jÐèÊY[/+âªÕ4‡3®`¥¼{Æ¢„åÿ?}°»*ƒÓ’‡õë¶óØEÈ"UrÐ#¼ý#r:+œ·w”J;\‰]§Föq7BnòôÉúý'3sK*kÑÊ~Ü‡Ú Ëœ2¢ï“†x *º'Fû\E±Ê¾·z'š†Üëñ»¦€¨”U7ÌöÕ¦Ãè•Íáè‘ýÌòØòûzó‘#ç¯âE¡o÷„ÇM‹Zd¯XÅ!į†(ô#{'ýí™»O^ºÃã¯=’¡~ÝãÁ_s‹+^½À¤b;q‘791‰C¼‡Uƕ̓‡âæ2<|À˜1öIÃú@¯¨°¼õx,ŒÜ‚$‹Ãú¦VÔè! „+• 9…}ºÅù"d·d ÿ/¸7Î]¾RX^=V²•˦Tôú.©):ZrùGT{åÈëE—¾µ[t F#A(ÅÊb¯ŸúàêñUCæ~Ìfsy™@¬¹zü­«'Þê9ê…ÔÁ8l”ÔÿøÚ»R=¬Iuàˉ1Ýg ˜þæÞO‡Xø¨(‘¾òÒ©ŸîÅ£"P=8Rg¶X<¶È¡öÃadEšè6ÕÍ,Øðò©”͇/¿=§²³ó©W1œDûa×iüí>YdÚ§T£_£Õò´”ð¡u{ÎÆG*I¨àxE­áÈùkDOÈG>ÔhULb(#à2›õë×Q=äñ”Ë–7ßU,fx‘%ÄÈånä½ÍY«#5xËŠÊ«uPý\\ŠR+@}ʪ©œÃû¦âóÒµâ]¤Ø+Á×´”Xh,A>ôü›ß²` \éM…”øH\ÂÙøƒðò…ûçŒØcÛáó¨Ó[PÑð*J¡r|Î?Ho4o;rñ†ÙнÃçrVÎwg³óÁ²xê°GO¾œ[=liðX¿l”S¶´†`䃄æbN!6°ž»oÖßžZœžÍý/ÐD+ª1êß´QH•hÝ ÔÀfÕóB"t¬_í<ºŒÞC„ù4|E~’Hi;Þпó.¶qÜXøÏe]‘ruѼÀ>lŸñ…Jm¥Aßgɘ¥Íz/¦Ç¬¸^ \Në‰õ÷\;ùN÷aOðrd¨«ÉÙûɰ¢ÌïczÎesø)ƒÌ>ü'Ö-ãp…IV”\^'Q¦J”)Q©S« öK”Éê„Q§6ÜnÛS‰c"’ÆpôÎ(“ ¤ZpBð‡ÃÈŠ8×­®*6BI›¢#}âRѯoUë!‰sÇÝ”ú´÷ÔÝ“}Rðµ[B$]óÃwŒƒÒ^ÓéCøt'F«' é%—ˆVï8 ­X(QzW²hò`ü!eñ”!ø#—ž]6Õ;) ›7¹ tI6}/ª9òAºu¬ϦF$²ƒÆ 9â›7thÛûq†ÈêB©’]¼VDV<ˆ¨žHD¯<±èÜ•‚sW áØ‚¬µ÷Ì®×A!Ýðþ$;5`ZËgÎÈÎÇ"}éjZÄíšC½9AE븄Eýéå3Á>Ðùm˜ J0ÈqQ¯ä˜“† é“ò㮓X¤!”ÂöÔe@›¡…¸5 ] ‹+Ûõ¢ ìáEzÌæ_-—bf!~MDbj|ˆ)0ż¥“ž©…Œ’ì=“öœÌ<œ‘ƒm2L{zÙt?\  NK€ê»mGq¶Ö`RÊo*œÝÒn~ÁoRØ’òr»wTËê´6ƒ*nŠè+/‚ñôø2öÅ4‰c =pÆ›HוŸ#¬ð[·™*¹<±"*/T$¦ßŸ¶ØX{Íå°Ö³Ô•a3.2yRæ¾—4‰ã(¹é„—QCmÉI0§–÷ªùœ—ì”y™B"虇½{¡Pj&7VÔü v¶«d= G…%äççðLÂóhâ^¡&̤»ÊDZ‚¦R÷Ý·$§òž{o[„ZÏ=ü´†ò«Ý À^ÒØ‚[¶“°‡r>ü¦”lšàæ¡/ ëÛ ŠÕ‡ !ˆˆP<§ üJ~)vU°;þõÎw;ž¿†hª5hJ¸œKiïbc9¡ƒyÆØÁ½6<ç“ùãµ{^…Æ-–"¬îe3‡Íù`í.ŸlØVûã# ,û²0Ôb°ö#g„Jy„ÝàóiþêÑñ¥`¤SÂ"²dúüØœÍÞ|è^Û' é=wÅ~úõH8s9¿[|Ø!Ì5z-$ã³¢ˆÖ =~H/lD~µù0.Øe¡bßÚáOšvµ üpÆÕÞ)1xQì4 Ü…À8*n*§û–_Pk!j&IöO‰ý–'õ¿´F¦éÅá  HaÖ`ó ’uü( YêJ=5ܼE-†8X+½òSáÅo» ùEM1Îè¸QUê_p¸üŠÜÑÝf ’uð§­.¡ßR}å…fúÐòK¥ö´³–ElŽx\¯ž DŒ¬¨åÐ19;¬=¿{`V^i ¶`›9lßA¸·[3æ£Gíy¹¨^8`€¨ÿ€–´C­ç$4F‹8R ´ \F“wU“†÷Ùw:kõ¶cP Á^ ‰puæèz)æð~©Û^€L!I)‹Õ¶ñ@T¤'ï VAd“ )Ñ©¬<ÕÁŸp¦iüàÞX¡•qšûì稊"^‚ŠF®ÂgŽÕ¾aÿY(»€·A[¬†Ù°U„þ@yÇ£þòÑúªZvŽ -ŽõŠPPvi~ƒÏ *~ãè“ozh‡ïã1óA¼Gs ?Ò÷¿>y=Èöhñ¤qÂa2°Ñö×'@üw82}ÊßQ³¶,£üÚvD®|W¬HžöØ9K] âÔ=zk°š*2ü¹Çðgz~¨àâ׸^’½Ûm8a÷­ìêæ‚ _\ôÜÊgýpƒWÝZKk¾ÛÓ³­“ô®X)Õ;.J±À§þÁÓšz: /#+êCº 8¾QHw‘é\‹¸é öÞ-.Ô\F®BN=ȱÅf2q$õ[ÐÜÿÅâ)ãôBG²{§Ä’Špò2†JmÎ(‘(ÞBeûYï®Þ 2„=šy¨mYŸ€ƒlHxiòð>wO‰84¬·<÷›ßµnlè ZPÑð*Š@á„lÛ‘ uE·a6ˆ¸N]Ê…Õ<–¢Õ3Æô‡\ ^¿ç4v‚pkéÌz>çÓÃÎ÷d"?Æ…}xR ‚x‰ñ£ºˆÉlƒÄ÷ ¶ó°NŽL§NBu‚€%ŸØGM‰¼x-ïªm\á¾€Ëi¯ËØú8O¨„”»ctý§7Ü•#»µ^/ðàW“È¥+GßÀâ…¾*¼ð5O¨pÜÈSW}yÛ»É7jpgîûÃåCÅ»HÒD?ÿ78#Oš—¡0‡åp7‹ W<%Ì ½3 +òsš™b ¡†€-?ßt„ÚËàFGK§Þbkª©®âÁKͼ±qõK¦S_G³"ÞÉkÏÜ]­­ÃÖ´­i•[Ò·Œ¤Ì3PFP¡áþ-T ™éOØ@š2¼/ÔàÀÝ»‘ê-¨ð¹:np/’KWg‚8 Ï\|m´’çV̆TÉj·Óf“î›3f錑(ˆ#W­zR“×p‚ä>2ÿ·èu½öô’œÂrìxBh§¹S‘VTÔ¬¸% ÁDÈã&¨—k§ˆÙÚEýhfã]3M‰¼o»-Hçq9,t¼-9·jšôjg²Ö™Xaïy¡ÈQX«Ä&&&’i[*xY†R¦Bà  ÿ~5iXy÷2öWù6v…£‘\:=ëVCŽxËk­˜¡¡•ÈÛv"Ÿ†yhAE£W‘Ÿæ:¤l£Ù`IÞ•c?*ÞÞ)L¼cÕYÈ7b ×$¤DªŒ&ÓYËâJG÷¢mBÎ-[ÒÞE:kÄ,Ö °Ú‘|Í:úç}¶âÊÊ•‹î€Ü(¤FͰ¢š¦3 ~"€.ýÆ Ta>_±h‘Ÿµ4(ÆUÔoŽ8ÁŠ˜À ªø*΄@?Á‡ Á>‘ÀU2žV_bPR×F®“n!mkKO‰­Eº8ï ˆtÕât[ Ñ=$‘^þÊì'3ï‹%+Î÷Éܯ + .þLë AòÝF#ê’ÍœÅUkS)l%Ä‘ªàM2rh ªeê \ vƒXa8Ve4[ ÌR=%‚D$I=Öã#ìvÇl°¹ÊéE&œøsË9U½’¾ç+[2(YO}”½ÐjÑv=Oõ/jÓË8õóŠªêˆCû…Xþ•î Oq'žW¯•Ø~͵¼fjß  ᎀnÍí³ÛYnlÕH…ýê#Y/e·ª “¹]€õ“݇ ËíÚ\èW®Õ›D|. K(t H,ÃïŽÊ‘I${Àlˆ·Øf—ÿ;¶vKíáïfáÏå´á€="yŸâ¾çÆàR^;„J¾H¥ˆ{EIˆÕ¢†)¬nqB£ï‘)S¤êžtJ`#‰ü l–;'¯ X­¹-µ1²¢¶ Ç”e L'NØs)˹ÂþýEéõ'äÒ3A÷˜pt[¬ÖK—RaS•Às4¿¡èÓÁ~ï›êO¥‹x¬r½ šì~¨m…Ñ0›é*Æ^˜¤V5“§ã/VY‘R©Äa4(_#ǯŠëªô6–ŒSÝ[x Ño7 Øá2TeyÆå¶šª‡UÆþS_Ç92xö}÷Og6=Êå‹Ìx çêq ?ö~: F}Rø"µ·{üsŸ¬ÄФq÷ìЕg(cÁéìõS8áßÁM a›‘Úöh£Uu2²¢VÁÅdfEnêYTP„¡²¹\a_J\䬮µ—U´ßà_ÿlÓ°ï¯ÙµêË­í×P§©K Jå;¦Ã;ÍÐZ;ŒÄ(DÖo¿üØDÃb 4Fev±Î•ÛlÚáâp& ªE~S¢–÷ªMç·?søÛ™<”}R¸©?`•2šQ0btnûÓuÕí"*†î¹›Å‰T*BÊœ#ÊZ~w19BGE¹qÿ>ô \e3f¼‹Âô4R§õ|fS•ã=¸VoôXök*K}:ñSFg‚}H‰`%¨¬J—š‰zòK«aX 4¢³ùDÐÏU|E=>Ùš¯Ä" ëV Yþaí§Ù<Ž˜ë„-MXÈ V‚Ø.F±ƒŠeþ† AìwÓè 6Ñ .’ÉdùåUû32c9§ÉÞ¡vŽÚµì[¬ÆkËÎB˜„Žqùõ&ǼS4‰c‰{Ȥ ”{~=+*ÍÙˆ¿~“þoè¼Oà8Ö{\‰»Üœ‹ÖÙ1*™B"‚¸(t6ј´€Ì/S ƒ@ÐЯû‘åYå ±ÛጫhÈÝ×k1¼º]ûe3'7'Ü­ÃèÖ€;'™=và+ÿMÒ—_ëùx=¬üÁ/ýÆýgá&›ðIü«{ga»gDZ‹°Üï´ƒÓ’¡d2³ð®¥5_|k54ˆÇ î½bÞXï‘øÕæ#™×‹ÁŸàzöÁ;&À½ë—›]Ì)‚–ÍØ=‘–Ú_þpݘA½.^-‚AH´ SIwLrµ°üßßl‡[´A½’›)r×´áÄ©­w»!Ç'..¤À‹¸|{¡¶îÀél˜¬ Á®¶k—0j˜çNx°à–vm®µ•ãGŽéêŒ÷LæŸ"ú©ýø±â¨Š®Žvk?Ñ»™âëÄHù G€`ÉaÓïùdHÏ&x ûðk=•–aY`tEŒLM%‚U'ü¢I»Aÿ ­(èp´¶íJ÷[Û™ðÊÏ@ù‚õ\ýúõTUl¶â®%©Ó§ñðÁ\ ¥±a>~ÚY«õ¹ZT^óåÆC0ÄÞºo&‹5B)…bäzɺˆ¤‰-G.A “÷ ý&ýÕ¬/¤ÝƒÀ›,t´§ %ÿ9o™S`ÚÏÄ"àCBËûÜ®9YQ›àåó8XÚTE—, Ð]—z€½3gµJÆçÇŸvOuxՕΜ¢_½žåtÕíØ§\¶Ð»•u{NáÉØ»¸pµžba3äŽWíçÖÃçÇ é£¹x­¥>ßx°_·„ÇONŠÑ¼óÝx™}èŽ x™F6ˆˆàë.Ò`Jñ··Ú5ÌLï8zQo4­úb+6ÚPüù•sày þ°>@^|`.ò€ õHŒ‚×3½‘:n´X 3øÃà ˆ' ¥L ÷îÁ½Ú¯î‰â·-â=ÀÐŒ Ì„‹D‘PB áê+œ¼÷VïA„ûÛÐìv`{…‰Æxy,G‚Œ`à¶¡¶Ô†!(ÍTÕj%œÚvÒ"ÚñA½Iw³>øõdoGèy镟ñévÙho S|܃Ð9;çÿqØ-€”Èîž2-)wöŽ‘ cTrŠËz¨lè̳2ùsÓùçXÀ#ïÄþÔÒ…Ë4@GÃØ…‘hëÐõ?|OªP,YÚÖºš./ŸSï?İe—w.¨æd^/é– J„t¨Á­˜J!Ö(ÁböœÈ4YmغÂ%Hr ( ‚çµÿm¨ÒP°gr îT‚Ý·nñ‘È–[\/ÅÄäôõ¢JÔ†=¸^ɱi)q}»Ãk‚ôéåÓáÝ âŸ-‡Î}úóxwÇ[&®‚™õë?ðɱ Vàa„¡NDP?E{%õíß’"(úK Ä#ÐY‘H` O$ Ôn½ÝbZõå–½'³:·ŽF‡1b¤«9^h“J),!5wÄAl¬Z¡uD]”̵½C }4Ò…¦JÙÌU¤D•Žn{랬töŒóã±‰î ˜8p£Ð‘ð…Ö ÔÈ\…v’R"ÐÕYôžÍ‚Ðîiõp4@B} Ï®ØòòÌ'O ï¼¸xÉØqí7ATAÏn¨ß~-Ïrñæ}(·Â©Y^i$1œÅ»ûâ©ÃÁ#ÙrøÜÜñƒRq®î÷ïþPk0þrÉÔ‘ý»Ë¤"øðÊ-ªÈÎ+…—Y°–Ôø(È–Š*jªtu8RÒƒ:ç¤VȰņ?ø)ÃBø·ÿmÄ¾Ø +çÀ ¬LL9B¯ðt…—û˹%`Hb!¿ ¬›q41enIeNAù]ÓG ±%E財Á6Ö±š¼R¹T »8pÓ«riùãšÇ¡Yµÿôehq…òüèF„qat£ÀiŠæe1†<øQm{WÕÚ’“W®:úÃ]E»—òœºcÆ{m.q{´úuš\Ê“¦%‡M9YÊD©0B.ºÂ@ å VÄì ùy;áÙŠ§‘â©|)·tÌ€î~VÔõŠ.€FAç ]€€Xÿã¤.hµ·’©üÎÙÕÿúÍÕþç˸w^#íB‹¢ ¨Zã,=|¸"¯±¸Aù šêQþ…—+HkðqÑóÇGªåã‡ô>xöÊÛßî”–ŒÌ–UÉ=üƒ~´v/<ÖNÖêJ¤!òÙ3)Úæpüã‹-h·GRôÝ3F@fp½¸âÛmÇÀ‡À–Æê…-<Ÿ¼YQ´Fnµ9¦J‡ õŒÔó¶E¼ Ù8VJ£H,‚i@³Ù“Rvl>¹•6“ÈnÖ×ÚVo;5,•\¬kˆ(þ¶¢G£Éb­5˜1Û¦q¥&—K }DŠQn؃8J8„5Tœ¯.Ø‹¿šÂý6‹ÞbgšZÍ,yõÚØG÷Ö=>D²!šw=ˆìà¦õÎè+ÖñEŽ8Ág)…Ð Øp‚µð"zœÔ îÄyc°"o4Z'Ë9^”ñv¦–òœ¹22=Z~--ß…óa\ Ð…Ú1ÚðšŽÃaØ´‘ê3§Xx‹®O{ D>¦î›e–3çÍ'ΊGRûb`ðT7xøU…JI„Ûùþø ‰“Ïß=<#º*«W¾oîXH’$"PpÓ7$)…ƒúø‰a;Ļēb#þñ«e¨'B%£Ä>µt:¤Ø7!.`ñéÓ4 á® Žco[„Îâ¬%B¡P&“Øl”@pJt˜Í6slv¾Sët³llE[W¢­qUêz¸…lŽ+ˆ,§Ìí²l.‡/äc×L…ÅU »ÑJ€(HÇL´®êaBÕûÖ›'(¼Ý`ø,_©Žè!tÖr»1=ÇËì%8¤áQÊv5˜]Šb{z¡}°ÎÇe9U<Ž‚Ï à@`BàC„a ‚ÏÐÁ!÷Pè ¾-=±ÅDâYÜ-B|ºÀpìBî¸A=ÚRa)  jô¦aÉr@GÁÇPI'>6ç²KOyl•M@ÇgMugþUß[õÚ¿‘¡æã/n°"|ÁÅ_SI:¶Õº£‡žP£¥š© v±‘¾Ê4Ój´¶Fý(Òh=ÁMÄÏúÅxó†ö¤è Þ4ø<>\nÙì0êä¸`K™²ªìY›Ã‘Q¬C£ÞEñÉ…¿U š5¤D Dj¥’ÒPAjG>O`ذ^&T°Ê7 ïØŒŽJ˜ ŽË’ Ò[䥥¥•••R®Öl¯t¦•šú©8ÅÉ‚³q¼ËbõCT0ºÔRNm jk´‹Kæp Dœ:Ú»-n1‹[ntiê\µÎ$˜g4¹Õðæ!d»#xn©ç°$h+ñ¢V«A‰ 1 5AÆË°¢F'ý6‰„á½DÀhdbȰéÐù¤ur¬æ6%»öe(|(ˆÐdˆ‘ßwDò… RV±ø.¿+iUAÙ¬)ºoÖÚó‹lÙW{I§ŒoUq&s{ €_ô¬ÝnŠ_Â1~S8e0 Í& Ž>Q$p"OhÖ;¬NŠax£ˆ”6•D„]AlœAJJ¥"€(Ú»?ÆÚk`BѵŸ‹m—|¢kؼͫÇG$OÁŸ"f0æÄb±èt:WEÔhÀV1),–Ifg™î:gìy˼ó¬ùJNi,/[Í-Ts‹…TÁœn^­3¡ÔÑ·ØÞßâVôîJhؽ€¤€í1>msS!¹,;›²–ÆÅë©ßÍg;l®†ë–pÙÔ#Þ£ç9 ”’=°"|"îá²1q­8ÊZ× ).Éß'Äž 8*ì¦d–[?Ûpø¡;Æ1Ĩ)@A‰‘íꦡ@£ÎÌxŒLRÄ#ýoª4“~ Òê*M %~ç'%‹†SJÄpÓ«[YñÒßÑVõ»ÿÁŽ‘¼Þ n´Î4Ñ(øù€ ‰E”(…z( (ýkiÄd2SšFvl«a×:¼‰††1ôÐíÃÞ¥c:o« ž%y…¨‰f]~ýîXþK]1jóqèÊåKÕ ã(&”2E3ŒÍ¹…œQaX EsüæêÌ·Ø%ÿ­I%2©$EnÍÓ[?úqÿ¼ñGèÆèy£‰§26Î %°œ© 6à‚Ž$žÝ0Ò9q 8Ó_™H3x ŠÚi1h´uéäq¢Áý-UÕo}ýçÍÆ$v$¸°öˆ©Õ¨þU Œë®6 ìvX &´]9êÈŽ¤-r{㣻!u`AÅšæXqÉc$°¿‹¡ø†žÐ^³.¯á@\,~TÊ"RÆh˜‡¤ Ïè-ØñháY8uuuf³™xEå;roµ»\6ËæVè\ЧËÁbƒè4U-Iç°\<Š‹p”\·€ãPÒ4J‰ïd]±M̵LœJÃ-ä°ÍWÕÔUøåл¢±)Váè‰?'‹'å¸Õä7i(‰ƒ !‚1zèv:ÉL‰A‰À I L—ù›j7ˆéMÎeû:M“™Æ'ÊoµêKnn¬ír™´NFI¨.*,¨Ùàt:{eâ°´ônqCw¡-Ó5Xœ8;xæJµÞ$ãØâ$,…L<€èðÃŒtýZ½1F­¢1§Ó™ˆ.«5!ë"•ÈãÉ,ð¹ÚÞ_#ÿø\ñýO»ÍfãŽ}Æ £™}´ö¼%õãWCÖlJãg…_—ÕFiAá¼!+B=aÍŠ°àRºˆ”† ¢BT ß@­¬Vc9t¥=dh©Öר:ÐãpªøÑ`Bû²\A¯ùËV´djÐ=t-dÆãŽèÖ BŒ¬VLVbžœ G"Z¬çæ êò¸òHd–æ³`"øƒC`JJC‘Ï?4ãa$d–ù.—ÔáÔ;D¹¶ÑWmã8,‡‚S¡ä–J95"¶^Ä1Ø.ËÊcc;T̆qElŠÙÝb[ýŸ´ŽRŠ‚ž›j B.—#ƒÆ4ÂcA‹hŽ~b#‚D\ÅMˆ!P":à+ to[‚açaXQ‹WJ%— JpÐŒIL*~“ÝboÛd6ë/ç8~Ü$‡ØúüYýÈq?Ì?íËPJE°« Ç-j£se2™­0Õ¨3Zð²±<«\Ä“Aˆ*—C :H~W:°­5˜z'¶‹uæÎ-˸{§ÀJÙqî=kôøq1Ï>VõÆ;H¯úçûÂé¼µOækPð<”¨ œò øŒ»RPQQk0˜pnßN/«Aé[ÅŠ‹3‰r©0V£HK‰•J„X}‘ˆÐ–š¡%]uýHç }]uVê ©¥ŒAdBêÄq\žy¶]_Ý0g3)è*(ºJèaEF£#ÈôÀˆ¡G*‘@XlC.KFOÌ8ÅD<\qt µñl6!ªs8­.·ÕűºãJ1N7µ×LWo\rs))+ê¥P,åN…a6ú€€Røôô¢ž!î@/¢>‚€"ÈFúy£­ûŸaE·™Ϥ³ã#Ô˜z—ƒ º&³±Gí®,ÖW?È7¹B5×ÁÑ›Ý\»ÑRiÔ¹êuÐèr]"ÂÁ63 ‡œbhÞñ©ÝF©D¢V+q`ÇF €4À{p®ÿ™Ñ—˜ˆúuëHJ‡éYût@¾`¦ñàQó‘“.½¡òÕÅ®zJ•>y˜¯AA€<¯ÊªõÛŽ^Ê)¨€B‘F!¥l‹Åm¢AŒW£ txÑ*©Ö½Ça_è3kt¿ø(ßÓˆ^%šŒÚ-Úš’ã†5"[öþh$6¤b†AIdH0ž'hü¤d#›N ¼b<ú ;‚ò5XâDhÔ 7BÝ G¨2ËÞ|L5£\E%¨“Ô ADPÉr»aÜÀ öҰˆ#‹ÞDõ‚.¡z|RÉ“wCx\£ðJ“ /µž8ɆFIg‡d£¨“‡<‘ú†täA@æ7—¥ïhPºGߨ¿ÖH%;]„é9]ÒL0æÚöí:çêõ4¡VÖ=•=†Ùé„ðZ=:‹õäÇÿABÇ7êó»*ø½B‘R"P¢ÈµJ©ÀWüŠé_¶@8#‘ÀÞñ=‹mù–³gÐU£RÕazÖ ‘‰úݳE+Ÿréô–SÕo}ùâÓ ó0)6Í68wâRÌ‹/›5 ÞâhñvÇw¦=Z„PoP»Od¾½z¬ÄÝ9i0̇޶!—è-¾TV“¡-=®¯¼„çq>ÅÆ‘1"Ò$Nä }4ªoÛÂí3àɆâ~Jb=!ï@47¢Â(•°ªò7V<IUø¤8Èí*ðıРH¡°CGdQôj#ëùlØcïG7Ý é0h ê%Ÿƒb>G"ú@—BN<ªÿ )t¶††` Ên3)˜rÌ+&µ{LÄÉk…NgOñ˜ëE1—ÅRý—Z“R?KžlÊx׳õuFìc' Ûú_Ò·ômšé\—~/ž_,el ºDØ8£ì­)ÁŽpú€€ÓƒªUÚº=’€3ÒQ–¾ÄD|0üôI)J40x@q5êèWWöÜŸ ¯7ü¼—¯ºw±OW™¯Œ€Áhþró±’*ݲ™£ ÚH¿¿up7Úµ9<ìñvЇÆÚÝ'!{pþbÀ³a»‹hÌÛ]' öàìeÏÉ;à=UÙ?2e*È&i_Ô€)yçP8B_ð š‚5ˆÇ' 'òš¡ ž'ëÍ3š€  àáIXEؤ+B ´î)%3Þ}c4‹©ÜóèF7ë œçŽÎRzÓ`rDC²FŠøtŒ”Å'}•n"," +jnš0¯¸'p«áÐ(dáÚ]'àL.©•Uå¿}Õvå„aNÍãðfO5Ô¥zƒ¬Nj„uzìä;¨±«#òKÀ/¸Á e•_æ±ÊO½cP~,*²èá< ¨[ œQ W|sÓU¯¹){Ö0zHÒ‹ÒoÚk ®¾i>œ»÷¨A$¶¿,ÔþÀ _#6 o`ö:>ZÕ¿GbS«lPpnI£X ­6û›ŽÖÖ™Ÿ_9¨–” ß< |x;Å£øýïw}¾ñÈ/O¤íª{ª4ë›hsýv3.U÷ŒH{"_jà¦?¹ò1 úR‡Eð|CÀƒ­ã¡ÞSÏ€nüGRð‰@ xÐ=òtÅ'©OKDÈ3Ÿä¡ RqÙ¤#zK„áZ@…tµdȤ ª"5£*Ð=ÒCtÄÈGCˆ¤+ÁWï”Ã3° ‡±©3‡a°£„ ÆÍAn|vV_*®xoõΧGöâ®z×Y]ƒÉàHÄ /¿(= t»BpÜ\ï–Ȃǔ:â¹ ‘÷b`'/k#? ‚~°0Õä9C‹Ýhü¦êOãÓ¿P"à)ä²-30§3„àoéÒ Ã»·$¶ÛÓ¡ƒÎêjT_‘ÚÃ* š­ 8,ûyß8½Ç[=h°\J)À›¸ËcŽ¥ÝFß¾—LpU •¬ ðæŸnp0Ò¾M¨v„¯Ð¬r•|’1â&1t„t+í€ a{Žð!ÈŠhê$®™®ô§žy^P u#ôˆþDä$Íu¾Ïà°"(Tx(EèŠéÇ!¨7ô…STRǕ붗Ö@ú‰Îsc¢bßø3q'Ž»r%,íØrÅ­HîAÜ „ë#s—bEDVDý¢(+^”Ö£owS\(ø®;U …0pFN`ú7é!D‚ÑgÐ\ÿÓzÒnaÿAošð9z(#'J%»cÒ Îg~‚Ø’8œqõoŸl?¸×òÙ£!+ Ô-l”P"¼]œÉ.ÂÆYסDŒwÉ´kvÙ/,«»7nŠØáוÓêÍwŸ÷U¬ëÞB'Nx úãýô#KFÃ…ƒ Óg°ôXðÔE%Dƃ'*‘‘ÏtˆúŸ®œ®ô#|¢ò‰ÒT»tÂ=VíWróÃ;Ü ¸½ 6Äš å8iVfß¹þ^©‰´>ýKEL q…Û9qGR#±ˆH‰ÈÍ×ð¶‹±ûÝIú×åù-Q¿.’ˆ:‰¾ä®c—*µ• )=æ½€0ÑàCf¿›îà‚Z½¡WjrÇ4ꨬ4>„¶¸•ÝzvL£Þ­à¤÷ûß莉ƒ:«©RØ3 ;Ôxawtóá ŕڧ–N“I¨óØ¡ðÞ©ôžSÙÑj9t‰B³“íÚ+ŒzÏÉÌíDzZ0_Ÿ¶ìÜH;[ï“._ ÿðƒ… ˆç¡K©!2D"ž„úÈ#Ÿ„úxGÈ%Ÿüõ«ï=Ô1ãLˆŽÚ¦ÕaEQ)¯êØ!àþ€ôê00Nê>tP½u3Û³VûóÐ1Æ#™î#™PTË%P˜¡ßSÈM‡ž Rß:Øþ…`m7^×ðCBï蟀€¶v(ÀŠpAÄeÅ‰Ø ¨õœÅ€7# œ60Áa5ìnàj­7sÃKí‘bظÁcÔ%Ÿ‡»Ã!‚ìóý5»`üæñÅ;½[¼#ÃÙ3<~¾é(Fý›•³ñºÓsÚÆ:±Úaã¾F«Ë+«]>k4ºÝÆ Ã±8F=}TúêíÇu“Z) —§GÇ@M¿Þ˜%éæÂäÕú‰í•Ö£Áù©÷Oë 9\FVöäQÃCu¼PZfGs¾þ’P¢ònÝÏŒíp`ŸÌätÛÆr£H™Ð<Ä«†Ç‚×@8?%‚§ÀÈÈHøO+Î ßöš¯3ˆWqã6ÆÍÜ}ÀSLÿsýé3ùÂ…¬ýG; Qº ´þݶcðd÷Ä]ŸÑ£ùÃù¦~<€±¯œ7ެôÕvЏ!üý¸È–Ôm(Ždå–á ‡ð[R¤SæÁØ1GY¹%#ûwÇS¥SŽ1Pƒb¨Ïm‘ +‚­ƒô^=v91aøPì]Þ¶—ÁÍ¢mYûƒíÝ·‰(¨nøÈâ©Ó”ƒ|Ê.Žß×@?åàÁí(´Nÿž >Øm$›’àC`Ep^‚"ìâÆmŒ›¹¶œ>å(*DC¢!CÉ),VDZ"ÜÒyÅGÎ]ÅÆY§—ùL%Æ;oüXš0¸WjBt{#Ë¥K•ï¾ãLíÍêw{ªÇ4há§Ó!«îdv‰|&¢ù¯”´^!I‚g‹·h¤ù‚ÌU†‡¡ §O~õýÿí?yzêè »R)5ÿù¸öãI—¸óp—,©­étlàè#4ü‰"20”¨™‰#+ v»ñØÂûTˆ M„3FqâÀ3Œ4ŠpëâÄÀãË;ÈHþ†™"ÅÂÈܗ ¼Ø°ÿl¤J]¢ö¨?ÄëĨÁÀS˦ãÖm§Þ:*+*þµªbÛVÈŸY`E-˜,‚ǃѢRÔ{"jA¹Î™E%—BS€x›‡íœCeFÕδ×ü¶ÝNŠ‹8bèÚ­»Râ»'%Ü6°2TýûMÝW_’Ö<$zèaa]ÜÀmØ…›Èò–«·!Û.(ÞáÀ{Èኰk>„Ù;Cž‚wÇ®ãÖÅ ŒÛØ;½â.ØbÛ»•³!O›6½Zi´ZÜÕðÚ›_Žg]Vmeü^öŸ•<àr·Ý®ýö›Š?Ä$×O°Ež!­„m>X£±ØìjMèêƒ7z_<&¦´µZ"rºBõœYÀÍTØ.a4ËçÍ*.¯xÿë5O­X‚ÄŠûUM¿n-^óô³ªÂû‹8~QVt˜%öÚ±xà‘1#Ÿ[•p|‚ô h%0Ä*OÌáRQ"Ü´) q¸}FÚN_ëvlw[<î`gÎâtȆ=ÈAÏ_ÉÇ1áÓ‰]-‚±ÃÙ3p?´O`ÕVLGWýó {Ayhp$RÅ3²ÛÛIB~Û»­°¨ŸFÈrߪnÛr¯W½ñºùä‰úR\®òî¥ê'žäÊý´d[¿þã5,ob `}ôˆÀý#]¼Vo„»Ö¶ *D0¡»ÁDü@ TXé:VQƒ›c!~Œ°UE [·TìÝAЍÿå¿x¼UÅ™Ì E‹¨³¼uŠGæÅĶòæk£4Wêw‡VDAÇ ÄGkÕV\uu5ÿýX÷Ý·,óDT%6ªŠÂž=›Ÿ‚޼ Ãп÷ºEpšQý{Ü;w ¼^щÍG®U¿x}ÞøAØmüÛ'z§Äýbñäæ‹0WBÐbEÁÈxà@ÅË"/^Ê{WhJÜù`Zg± ~&0È;VÏš4JˆÞ¿™©À4˜´ ©Û¼©ú;««I)nttäsÏËf¶õô"™|’HË»Ô|ÎA½“FöïA<“}(ãJŸnqÃûuCŒ[g4ÃXb3$©´J·ÿôå)Ãû€=°`‚L,ôn n`ΞNh ´Å€ڡ¦¤fÔ½XLè>3‘.‚ÀÍÛ´‹ ¸©ašOŸ*ÿí äe ¿y¾©œL:ƒ@Ç ƒqï´Å‘ɤS¦vL£t+ÔzKù•¤>éÄ®¡Ñh! Ö¬¬Ê7þn½p¾4>_µb¥ú‘Ç:ضB«¦,Z£Ö7E`:§ ¼ÎdE|Ë¡s›fàN ø+æO:w¥à¿ëö=¹tZ¿î ›dl;rþéå3¾ßy™ÿñÅ–—»ó›-Gz%Ç<’0 Þì3.çwOŒ>s9?J%{èΉðrŸ‘ÿùÆC«}òð>‡Î^Y4eØ´Qé(ËP@ Ô½mt F–ÌK¥¿~– ‘Nõ§?·eO½cúÌ´Òé ÌY©eI6s6lduüx)BJÄ"½GAr»àÔj+ÿöE+ï¥)‘dü„ä~ŒxúÙP¦DÖµÂòÎþ´÷ôÏûÎÀš9DGUZìzí“úÌòñQ*¸£ô»Ýn‡“úDü‡x´F1}$Ålà0N!á€<®ã«Ãá„ChìÇ=°`"&!qÝîS<.çžÙ£K+µ(‹ÈA€‘±pò¹ôé'Ý&¦DvP¯»¦ Gÿ¿Ú|øpFT… ÉÁW¢ND\¾@ÄaS› äÔƒÈx›ú„Xˆð$ض äK^ŠY­RÒjª~&A °tiV„ßgù~gÏ˦0‘÷ö»!.ßìÜ3µ…2†-õïÐ| ?”1 Ù¾¹ª«)CÕÛ¶Ò=”Θ ­ê>6H·ÞöHRL‹•£5˜ f„Ý4(Ük÷‰K"!¿_÷øŠJ†gV»ãT&õü¤ä@ØM£¿6ŒÄFªb4Šõ{N[mè'5ÌÀ¤0.­WTóîۦÇ0™<ö­w¸Šæ~ÌÁ'¦õ®†€aËf2dÙÜy]mìa4^¶Ó™vþ´î¡ûiJ$èÑ#þãÿƾþð¥DÀ_.¥ìv–ViAqfŒîø\Ô¨¯U,™>‚ŸØeÿ àCßl9š£!ó5ê(µüƒïw×èo¸¹mb"ïž1ŠÛPâŽVS\F‰³ œ˜äà ÐueE†Í›´_~A¡ÎáÄüý Ajjpf€i•A pñá(*B²hè0~\|ƒëLBH `:qf·_Ë´µ¤78*#gÊ¥ËØ¼ð{®ªÒþø +DDÃn|Åž¬ÕŒÐ¿†«#ä×#:ƒÑs¡€rßCJÁký«OÞµ$0žUÏÝCï›;$þû‡ 5läytÑ$L;•¾¥&˜OP@ ü~½AÍráBÅ«¯ª"~ýœdlýï6 •3•0´PvRCønŸAézµz£E£@„€á@¥Älµs9l¡ Þc†ÅfG6˜Q&ƒ­ÒÖá@¶W|гêɵŠ)>—üøŠ&°wíà¶ø¢¶—–×¼ó_ÓÁc”‚±'Hç/ˆüÕsõZQåOû2 ’‚ô¥tþ„ýºÅÑòpbayMaYZ!é•ëmݱe0¹Ú.§W‡Õe¿ù515 äQ|©Ñeªfh=ƃȹnɤÉ~;Éj}³+ñÏê'+k “†öúå’I#ú¥\-¬üyÔGöJÔՙ˪)¶TTQ‹-˜A½ßq<"„g–MyòîI›cÍŽ“ä|ÓmûáS™2éä ‚òþŠ8²‘ãc>é­ýj>y¶ô±çµ_¬!”ˆ£R\˜:}×Ky}ú¶¶ª®œp8“?~Hïô‰¢DÔI9&0.'+ª|åeØœt¼ØØæ~ î!¦ŽÀ"psûln‡ºƒ Ô(@w Ëk‡¤%ÍGù4L‹(®Ðf\)Z8yÈÀ^ Îædç•c÷äråß < [ZߤÆG¨ålÊÃt L·ƒŽ@×’i¿ùö‚:L‰Ä¾ùïΪô»Šé€ß8u:Ó¡ƒ(ÎQ©ÃTÝ­¢†ÚKK¹éË–Ä!=JŠÑ`Óär~2dç—%ÇR_agHŸ¤ëÅU¯~²ùÝ5{/^+I‰€ÚOóž¼”wµ°âމƒ–Ï^«7í=•üÛŽ^Z:}˜\"Ürè{e\…6Ò/ïš4¬o2òÀ¼róÕ6sÕe¬/+”žðÙ;Ï=Á•ÓjEÍ”k÷K|«-ãj÷þuHZ½I ˜À Ðfº¬Èz%»úÝ· bѯ¼*LëÓfô˜ ŒÅÚT*›5+2¡çD'žhhHœ¤ì™x0#§Zg,*¯…þɳtúðQéÝÎåèì>yùDfÞï˜Ý¼6ô¹«EP3‚ø5då•KÍÙ2'Ô9¬oJßnqz£6N|ézÉéËùÙùåø þÔ…m®ZñäCqógòC@…ˆy‚˜Ï.×™`[Þ[iÌ»Tc/LV« ]jìÌ`Ž@W!×P'*ÿÃµbérÙô‡’©A íÜÜ>›–Ûg@ FC8 ¡Ñ¸RP.í9‰6¨w"t†6º€«=úÎÐ.Úu" “!øùÍ}3îœ4È`´\ΣäIÍT‚%dÀÆ Ô†à§Ê%4—ò,¬¯¶Ûpà4¸¡ÞÔLm-¹Ä‹‰ŠYõŠlRHœW%Ë?ˆ&$m*1u6¯+[DÄØ@¬J@HhÉ„2yE «ÈŠªß\eϽ`c GñÅ‚Id.pÉGü‰òSSEéýƒÛ¿[OˆV÷Jоt­dÍÎS8Jv*+¿¼Æ0v`w¢W›¥Gü¦[|„B&F+8¨ðìÕÓYùSGô‰ÖÈÏç!¸|:p&»»l$;n`T;g¹\€Cã``0¸¥Ô(¤GÎ]-ÛwúJAYÍKÌ…^Q·øÈ1z@ÜSÄ©žKùÔÛ¯Ü GÔÈYôÛl§ l(^q¸°$áÚv»ýe┣Z Íj±gб+Er €%4ûÉô*\è¬È¸Ÿþǵ˜uŽ~í ¸—‚ég íYËÃSÏš†š= D ,øC"4¯çŒ½Iò ½çT6ü±“üð“uÿ¼ÑûNgãp>öÚà˜}ÑäÁ á>w®¿{bäÈôÔ¬Ü2´‚DЬÉÃz#2cTßµ»Ï|¼î ê\:c8La‹m÷‰Ë¯üwSzwê¨?vî"=NLéªÂ4‚c\ @\{y|>/RèÌ×Ö8=eDß0‘ßÝÆ¨a bP¼@À‚àwmLAÎÏŠ•¯¼LfR"aϞ̬3„&ôö™lîÜÐìa {!Ðc‹&`k çða4/ñÞgIÇŸwJÄ(üÁ%»Åj'$ï«!áÏ;…ÄŸZ:õƒHÁP$öM8´OrÿñP1O"6—§ì;v`$Hªî»QÅÏ,¾ ×ÿ1^JJ$à‹D‘P(ñ¤fëÚ]'Rã#áy#\GÕú~çWbÔj¡;R!°›¡õ•1%(:9§†¶AÅŸ^ré(û(’ñ”Ëîa¦A 4°œ?ï(.Fß:—°!~|(Q3àƒ¸4¤DÍäÇ%l´y¯‚¨Ö± %"ea;‰Í׎W!ðÁŠD‰X,ÅŠ\<·ã½Õ;AÂq8~ô#ÅxlW÷H!@„V,ó£B¦ƒè䬞ÎÌ'O`œÜˆˆè¿¼ÂL9ƒ@È"`غ…ôMθƒ ÙI ¥ŽA(‚=#±X$“IåR)hA$×d·˜V}¹eïIh¯ß<J½L_0:Œ#uÚ,© ¶R† Ô&Ú ¿li‰©¥ë!Ð _¡èI´feÖ|ðùý—W¹êÎ颈/ _ÜN§q'eI‹Åã…©—ð?L{íhAD"—ËÌf‹Åjµ;ìì:cµC°fÇñÝ'.Á×ý ÞÉì¸>áãÄÙ®c+µuR¶5QÎQ+å … Â(…éý:Ý-Vd4›/f_-®¨Ôê &³ÅÍÂ?Ûéœðͧ2é—ëCGl¹VÀÂZƒ›ÅÆ£V¥'DGõO뉷ÑÖ”nE^óñãÎZÊõºdÜx®Â×=j+*b²v%x0´-Êd›Mép8ˆ›ŽÉl²Y ZÛêmǾÛvL%«)Q’ÈÏ£w¡'V£ÉRk0j f DȲÇò¬*©P¥TjÔ*• Ò"   ¡Ñ_¦aŒ@¨ÜC…¥e?íÜwéê5ü°#TJ¬CÐó÷-5ÝO—ÕT#¢ŒÎ7™Jbƒ@+€^ZeMmN^Á6­¦PÒ{õX8}rR\l+«¹}vöÛg³çÜ>w˜ä€ÕDhO“Î}g¿;Gf¨á¶¯ý®?L B.)‰D+Þn7µe†»”/µ&“Øn¶9ŒϦ7•êj]ÐF÷ÿ3Øðx‡åæ²\*¶KÊu‰<‰D&—KA‰""ÔJ¥ FPì©ê íŸ9ÎÕ›·8y&&R³|Þ¬Á}Ó@‰Ú­½¸¸ð÷¨‡›Ý÷Í7‡„­é—6âÀ^fdeï>zâÕ÷ÿ7qÄPܨ¼Û¹¤hyÓ01jÜ»ùÙx´OœÔò‚!ž³FgüçW;èNB-§åáNñŽ= ԧѳfÈö—ÿl?¨ç‚‰õ¶°½ vñ8ÔhDBÛ-l6ÂI3…f“Ån·S$P{·ÿÂ((Ô#ÀF#j>´Ë!ÿÂÆ¤D DP*ŒFQXOqèt>ȬÈ`4}ðÍ÷ù%¥ËçÏš4bX ŽT½þšÛjÊŠ»—†¯5¼Ð¹K˜ž€©O5|Âð¡ûOž^»uWqyÅ“÷-•Kã`Át`¿ÛDùØ’NžÂˆ´sx„Ð3¡¤B{üRÞÇ뼸r&l-bˆX­­6}B-3·To´Ð¬Èç* Nï{(k4ÇâÜ]¤àxä(Þ‰#®&$†`:«O‰Ž t,­“˜ ‚M#;¶Õ šL1#„02Eˆ@‰8”ÝJhöAµœÒ1’áMBJ'LÈt;¤&+‚””¨¢ºæ…Gîïž”(\êví49ŒÚpîLóäÓª–©‡A«ÎÔÑ#Râßÿz nÝç^‰‘aÛV‚­|μÎ2ÎÉÃ÷þàöË-ÇNeæÃqýásW·¹dw8á&¯V^+ªÄÚý¿Ÿ=ºp¼ÏÕî<¥Õº¿¶U[gî½bÎh¬ƒ ³Áh$ ^×èK-™6!¯‚Ó’*vߦOûÿì`×ÿÕµZ­zA !‰Þ‹@tˆfܱ ¸'ŽS§_îRþW’\.¹8¹4§9ŽÜpÁÁ0½÷^$„:꽬V«þÿÌ>iXV•]iv5cyyûæÍ+ß™ùίʬˑp† HÄV„BMòÕÇÒÈÛ(mMˆ‹ZÙL´ˆ%Û#1\‡O)j¥ÉíN ѤÑ@þ°ùÖ"H½J‰é’Þµ '+Bq–[Pô­I‰ÚêëË_ù…À4èÛÿâê3XeÜðžut"ƒé™M¯üu ð36Übkm­áä –éà• ÀõZkJ‘¡þte 8—J¸E”bï¸H¢û§×Í¿u»”¨$Aëº÷+'r`NaybüÄüÒ*ò{ Xâp‹N¾ðèÒǯMŒ ûÜË?=•|.%VtòZf¡ñ…‡‘Xmï™1A‚cYkQ éG#¬j é„2ôÖz56A‰ZZ[ D²"¦j׬Y‘”áD çí&Q!iƒ ªNg ¹gÃÆŠ0¯Æ–Å™¥Dœ–Ê?ý¡µ¬”‚×ü>k×9ΉRW¢$¸h¿oÕÖ{—'̤ñuýÁN&OIïÕkyåWÒ*­<Îôˆú ±ÍW7&&g½”^^]‡úƒ`ŒØÏ¢ 1eµØ+æ?y B&Êÿñç7² É"bÑÌ$)q»œšW^¥Ÿ¶pz$ 9£@ãéFnä&›ž[â¬H@+L΀4¥CJÔ©?mìôÚ'mˆ‹L#¤¶B>D®H¶b¶»ðöG0¯Æ–ÈŠÐ4¦¦Öl}WêÐÝ=øß¾gÅžÕ®T,àÒ=tú<—ñËÏn¶ØÕ¯¯fê3Çñ>ëÔdÔ“«µZoøõ»‡Èá:Z,?Eµæí{Ú+HZ6ººk3/oZõÒͬ¢}gn MûÆ“I5õ ‘!~’%¶ÉMk>–ã•%ê ‘„FpÂŽM,“/vº^V$fnZ\LJ®E¶òVD\¢”[™8òX˼ ÛÛÚÊ~ú'ÓûhÀg?ç=Fùè«3´_¸tW-J@‰ÆÅ<à8F-¥%Æ‹R~S·ˆÍŒ™ö‹F/3ÏÇÎ:9›´¬§¯gùy{ ¼]R…Xˆôd0$t^®CΈsjë*ªë»Û넼çzFAIe­±©eâ˜Q]›Õê~ûþáùSc^xxÑÎã×i €¡³?:´ ¬“¦èQ#(”kƒ±)5·¸¸¢¶®Þˆß`¿õr~‡d—D‹0Ì'¥Þ¨ ßI1a$“’qÕAF"ÃÊÕˆX'|+B^ûѶƔd:tŽøì VìYíJE [¸€ßùdóüYÓ»mpÏJýÞ½ØzÐLç@aŠ,V}3»ˆ?ÇbÂ^>‹d#ÒÝûzkßäUbQÄ£îvIå?vžyé‰Ä®{é34ÀçáDØMŽ G gÑ ž:wRô©kYØé¼1ý#˜˜ÞWð"á»9!9䇲@ 8½)ŽqÆqú‡®«¯Çµ…)þx?7nÜ}zy™'6ïg7JiÎeÌÅ<°Ù4åä4¥¦r¬Ç¸ñcÇ ¬û=Ê_gyUÃ<=ÜÅŠºî¥Â$S¢žšÑ‰°"2GF§í÷…j~¸òˈˆ°­®®­gß…’ʺMkæ/‹ŸÈÝIù3ïï ‘{-Ÿ´dö„cÓ>ûâËyo³ÑÛ©è.­[†çeºv5oni««cyºµë<Çïv¶«„è\OÏø¯ß½–u;ÿž£ ß~cÛþJÊ¥mö²¥fåïÂ^f;¤ó”²KI†AØôûöŠ£tkúM.0œzˆC" (q¬?9~­¤RÿÒ¦UO‰äóÈJ_Ú¸ Æ^……¼K-¨ áaEžn×[«*«ßyKªwu üÒÉTüå'?äïÿ~ðí/>ùVQ*½·ëNÊ'1O¤hܺιۚ{6îISC˜¶Šªj4S]»¥’ä¦=ÈܪjîrŠîÚƒ\óážÄ"—¿v-pKb¬Â’2”k]÷2ɪÚÞÆê×dºöow5éiÍ99LÛsú ÷ð»›¿:a… €âŒw­ìü’äì’ÇWÍ9”HàÏzOšw9=Ÿ Ÿ@¡“¢Nî š!«zãoí ’ Ã硇ÝGµF—ìø©“+«k?øtv~!z%ÒA0™.ÿsÿ¨åÀ€‰±c6®_s+7ïíw‹µÿeëG3&ö‘û{jŒ@X´äóð™ó;‡7 ¯y0iYÒ± žñæ?wÝÌÌ&” ¡^c¢"ð—=ûŽž»øÏ}‡ F# 5elÜãëV –¯È~ÞúxWiEeyë–-Zµ¨7c,Ò/ÒøÐ™ók–,C[|8uîãG°‚¢áöÂ937ß¿†)ñ.õþî}'.]¡àç»aõ ‹cû;‹Ãíô«~ï1sUPÔ¯3ØØÔ\ol™Ôºˆÿ¹ן‹sSs«” kÌWÍk!¬ ñ+ÙÀÄ–È|ï)³êCçoºölˆ?çžm„,\]¦°ï ˆp/µ¼/Aãîøâl„Ñ=»2.V´É/*¡p+'oËö]P"Ÿèð°òªê¯¼ÿé~îÒZ//Ññ’à==5–Eóîν¼RÅA"uòâ±÷w$ßÊ$®]lT¶ÏéÙ¹z÷CäFì½–vëíŸ646NŸ0NVR^yøì…÷dìíÿÞx –Ãý|kõõïïÞ¿ëÈ Ñg·ŸÖ¬¤~—‰™um@=¼J„©¬ŸNEƒþñEËwwî9rî"”ˆgTumíí0ïa“1?Ü~Ëê3ggÝê5ö»Š!žù™ëYÿñ—OÞÙs®Ûq ÌóŸùäÂÍÜó)9È€Öm3GªDmÔØØXQU[PQ»jþT‡gÝž;VÍÚ³‹|ë¤Û6j¥Š@ß°oVTõÚkí&ù„ßãO¸……õ}ÙÖm)ÛM ]ŒŽ2sÒ„ŸýË×~øÒ‹ gÏ`¸¬¼üIcc¿ùÙ§ÅÐH‰ž¸o5åžË3„d<²zÅoÿßwÿç;/úûQ/ ž\y…Å ôÝÏ?ÿ½/½ðý/}–]¸.çQ¸|C2>êåç6ûsÏ’®kltT­^ò–úhß!ôïä¬øÅ¿~ƒ>/Q¹ÿÄ ¤(t»M7câx8Öß `}zì•s§Oa½tøPÒr¾¥!¶Õ'/Inæ¤ñ?ùæK?ÿî×c£îJ<€ÉXLÀ¿“¯·2sÍœx·{\Â`æÌ%M†.óP¿â¨oQ)ÑQ8ŠÌ¯Sb#ž`¡ù±”iÕO¸#Ê£G%’ˆÄµhï_ÈÀÐ`LÏ-å¾c¬k«`í\B7rФ[[‚ô©2b°c Zs~~íŽíœ9gÆÿ…‡ñ ÄèH€(<¶6‰?d6g¯^/,-»”r“JR5v;Ã{6F¢³~ùbŽ%4NÒ‚yì9€ð‰[!îú¿þáwÁdäæ8uöZê-Ñ?u Ñá£N:9A›¾÷ËßOŽ‹™2>Ÿ/![ÎÉ/ Ap`ÀÅdibB|… ©ìGtÒõ—’‘IêºÄùw%iÉÎ/K[ž/B<£àûäÐ1nR©™Ùò¸¢·V,cþ^•ø«¿½%÷?°ÉȇÛiaÄÚYCS0Œ½œ–ǯvÜèÏ>¸¿!¤;Z$õ¡‹Ö.˜ºbîDb1ÿ|ËÞÉ1ayÅ•µc”ýï–½´IÏ+!sIÐÄ©ohl"¿,)>È£ì‡ú,<Èz¤Óz¾úÁ豿°a©#zæcFÓÜÜÜÐ`,«ÖøhqY·ÖÏÓC%&¡2­Õ§MûaíD¨ÂK@ȧ*ÑlжÃwnǬ¨êµ?‹´š~O=í8œ!k‹JËÅ…à'=øË+«^}ûƒ‚’RÊHw}}tBÿÕíÅtÏÆAþþòPQFíEhŒö;%HIP€?Ù cþÌéç¯ßÈȽ 9CÇ_p€ÿ¦û× 1Â2ƒNÎ]MæOî™LËü«EË$\ñ €ºÍ|—PØQ# ª(@ ‰&PTV^^]£ó–¢`çÔJ…••(ð‰¸k`“‘{°Ç|Q¿Ÿ4sWW]R’=.aÀs>q%ãJúí¤„Édç8“œ]T^+úðÀÅySc¦„í;{cÁôXh Oåj}ÃýK§ï9•BΠ•Çá‹éO¯K0Oïzð|jæí28SuŒ"$ôÀØ¿´²û¹9ñÑ\xξ]\9%N²ös° ~ûF£QßÐdÝðoÙv89³`ɬ ÏÜ¿È^@ó÷ñ®ƒ577kZÛLö2qužŠCÀ^YQKy¹H«é¢Óù?÷ü0âÊCN(‰ÜÜ\'ÄJBì¿ô ”ñÔƒ÷ÁBPx}zÁM÷Û=—VT"VÒ…&¦EGÜÓ²r„>k}â’%sf½^þÑÿ»D gB³–_\І™MzN.&ì“~ò—ˆÈ“Ùl% ðõí~е÷¯Xzúòuôb¬´³Î jQ¾]T÷xœ[ýó ¯®•"$A5N]ºšœžIyÞô©B…”WTÌ×esçLËïóܵ”®HïÙsŸc.­˜?ÉÊ¥©t„Z-ÏdÙíåéùÐÊå¼3 Æ’zåõ71>ĤéKQÀ]¾‘ŠùseuMeMuXHp~q š¯Õ‹%¿³Ì¼|¼Õ¸‰ I’ﶀܣ«ßüx7j;¹""̨ym=uùÚ˜Èpî¡ÌŒ-´f”±¿~tÍJl!‘ZÉÇ2íOFîÄî ú}Þgk×ÚÝä3áÜ¢ ÞåÉbF'ÆÆfĨ(¼ÒóJQ‡‰-„&,È×Oç•WRIPÆš¬®ãF‡r¤¨»ó^¥d"R"«{kô |Ž%¥ž¥@K>s‹+y€‹ùêHÖ3üÊZ[$%šùðk¼x3Z³fá´}§““3ògMCŸ˜×>:m%kÊÜ)±OÝ·°k ÓxkשS×2YOŽ‹üü£ËQkž¹ž±óØÕòê:\äž½ñ„1a]k?gzàDóö ÀÂÖ™rØ*}«Œ,ì’µ77×n3¹89;ûmÜ<ôg ÍÔwÿ÷7æã¦ù©׉âVc²ƒãæÉ¸ “ !yw×ÞE³g®]º°—Ærÿø:}Sn¡,[·TkO?–p =·2›z1‚\Òv>ÆF‡BÀÃ.<΂6¬YñÛ-[i_TZ†¤Grìom›3eR_Òw,ŽŸ/"(ynH­„f Þs-5^ˆîŒ½XsÏš<vÖWSo¡z»˜’jD•x·}Õ`&#ÏÁŽ ¼à×0© ÝܼWHž}#gÒ‚K©yüÞÜ}fÚØ’X!Ê/­Ê)¬¸u»+¢ZÌã-¿¤zú8IçUŽ­±j$ˆ‘&Ï‘®(ÔpÅ5'Å yúµ–Ö¶]'¯³‹–¨~(`«4:4oíéã"Gõ[‚­]"F¼^I¢+w69 Cõõ‹g:wã\J¶`Eg¯g¦åÝ·xÌcïéë ÓâòŠ*,j ¶'¯Þz,in ŸîõÝ~äñ“>ôË[Û–‰f” }8E…M’EŸ}œŒùˆö[ÉÁa-ß|jUem=ÙÊdÙìñ ScÐÁoxÇàÌBž~þòqŠgOŒæO”åÊ}¾CóH'ßyf vÖþ¾ZÙØåû/ÜG{žŽõÆF‡t=hðÙñü‡-ZOVt.9‹ž\¸ÉŸèJZÞÂãôœ^;2WB}œœ»ÖTÔè]œ]‚ü}Ø¿vÑt^ظá|ï…Ž_N¿vëöŽ£—Ï\Ïü¯/=ÚµF¾/ÉKXv¨R¢A§eŽ€ý±"ãõë)’ó”{ÜXmBG”gó%)¤Úc˜M€û]Rý^‹åpÔ§ëÒ00š§k½¨!’KÉ¢ O è‹Eå`¾ÂQvCëÚF²Q×½VŸL×!”Pƒæ·þÐAfâìéé¸B Sâ9ð@E;c1(–¹š@w‹Ê>~¥CŒµ»6¦Þ±)Q×%[¥V„“ÿc«¤·8˜åß?9~.9V4}ÜhâGŸ¼r Ñ*°¯nZÕµß@(g“¤­¹46*Âú?û„üö/o^õÁþó—Sñù¨{åÍOÍkD«L^íDEÀ*Ø+ªÙúŽX¹ÿ“OZµ¡AÀpöL›)œvÉR­vhUGqHа.ëŠFr‹Ê1Þ"N4öÔ´Ó×2°°®Ñ–Ì‚ìÍ]’/-$iÊØHÌá-j&ņ9韇°§lä·qÍ|ÂÑøÈ…Ô]ǯb;ÿàòÙ!¾5ˆ”¬v‚$©Y,VëSíhä!`½+rH°k)+Ó›ŒU]||t÷IIÄÔMEÀ^±ÁíåðyŽ þÓ>cÂןºcDø£¯l@ÒƒšP¥…øZÔ¸¸º>}ß«`QA~:¡{dEüC‰s*kô½Ô˜¨–U†;cEæù.9ņDu*÷D€Ô4†£Ghæìå…¬èžíÕ*æ i‘ü«¬hZmÞý½ËX admÞ®k {q* ö—Œ)åf÷¬‘«aGÀž|Ðî8仸ømÜ4ìØ©Pè;†3§ÛLyè¼—'ºh: Wû~¸Úr$#@èã7‹ôMmÂ)o$C¡®]EÀÖØ+2œ:ÙáÕŒC~Äp:äÛú¬¨ý;Y>œœt«V;ÞêÔÙ¤DûΧ—ÕÇsÛ ¤ö¬" "öÄŠd¯fßROžŠ€!€˜Ópì(F}æµh±Í\ê°#°ëTrz¾•-ìn–¨T?UT¬ˆ€Ý°¢6£±Þô\!ñ™V}®XñP»²=†Ó§:ÔgË–»xzÚ~@uAà襴SפBlZrhˆ²M?«jë‰nk¹ñ'IBY›®Eí\E ¿صµáÄñö)Õ^œ;£H÷wµj{aA@VŸyÛ¡úŒêCÕ´Üm.ðgŽ Ê-­¶õ‰(­¬}}ûQ2Ö1>óO¬š·tÎľJð!¢Í5ulÔ=ÊÊ/û囟~ñ±³'¹gcµŠÀ!`7²"Y}潦#²ía¤¤"0ÌÕgÚÅKÓÕP+<«ùôpw50í|¿Àp1é×±kœ•_úúöcÂŒ(~Bä¸p¿õÓ÷£ýõŸGóKªçNúÜ#ËȰûö§§‰W$÷€ ‰„tòWQ@ä#Ë{(½˜šSØ¡ï äCØKds[‹ ,¦§~Uè/ö!+j3 'O°6__mÂüþ.Rm¯"0ŒNwzŸÙ§úLãîZ\+‰iGòFN’0ß¡ólljyõýƒˆ^À|ú؈%Ócò î°ò—åWeqóÚ AÞVBT×ÖK§>ãv „‰XÕÌÍÿÔú…Ôü~ëâ=Ïšè‰$Ž]»pú+[vÓxïéd’±À®nå“ÉÓ|ñ‘Ý'®î<~…È¿~æþEr H­EíVE`À؇¬¨þÈáöFémÕ;i•³û“ #õ@Á @2cq¸Ý©Ï°ùy{Öèµú‘KŒX;€ƒd0C_ŽEš’SX†Í ÇF…<‘4‡qûrà Û—K9wå$ö~:ís,I˜GåûÏyº»}ecÒÊ„)'¯ÞJÍ.ÂÒF8l(Nh€ïÞS×ùúôúE4ž?}ìœI1Ȉ”–[¼dÖxRÆ’Ê—7¯ŽñwÏör¶êá*6BÀ>X‘ØF§_íÖÖÜ¥>³+/ÁH(äÃ39%Ûæ² [Ÿ‹÷ÏÚA@ÃÖĨ¥µUolj2I‰È›ñ… ËEÒÜO¾ï¶·KL…<Ó‡Ài÷,ž=aÆøÑ®Œ'Nãś٢Íú%3ã'Ç`d06!I ö§Þ_çå«ëÈýÃJš?•Ü ÔsìÔ±‘«Lƒð¥fЬõ©F,°’j?–?"ÒZW‹ s ôŠŸ«ÀªSRè ;ê³¥Ëì.x£‹ Y\½4îÞîÇ.¥Ì÷{VÍÚA@Lz:׃¯GÁtöfF«)€µ¯·IUµÏn»Ò#AÑÜ\¤ öÝ6ëW%$Œö72 ÄQ¡_½¹ç\J–˜FUÔ;;9à åÛ¨„,ŠC,>Ù‹*JÑž)ƒ Ÿ¢O‹öƒùZß`ô1É!­ f¶ê±JFÀ†¿pk-»þð!§ÉÄ„³««µºUûQôö‰Q¼WßI)5ã~A‰ÜÜÜ<Ü=ÆÒUÖÎ\ï ¾s;êU³vа)1:|ára…ähÆã¼ô‰2zÍÝÅ©ªÎÐÓÞ¾×OŠ GØsáFöÖ½gO]½õÇå•TL‹è5*ÅvB»N\mln™39¦ûnMì¡‘±©YjÐI–'ñmç±+˜<—¢ñtŸ!5°ÞV]kðuS4v`m]¿¿X¦nͺA¯×Êè †ëi…%¥ÕuzCƒ«CþS·nàÓÉYë¥ñ÷ÑEŒ >qœn$—ÔgG€†”ûÌ®ÔgÒœyqñpw×hÓÌšÕ÷…Ù´%1øó ‹·8|3#ÓÈ@?ªt­—gç«‘M·ÏÎMt±¬¼æVNNå =–)ãâ^•f£gŒ`²kõ"ñðp÷òÒx{k'Eø\Ê­ycÇÉÏ>´x„#(ëõrmŸá à`b~iñd•_ý…ë7ö<+zÖz¸ùxwØå˜e^TW(¤ü½\œkÚ®¦ç-ŸdÞfeÄBß{áA¬Ë÷@e0!L‰‹üÅ77—UÕ鼰އµ:WT?ÊWŸ‰ß”xÄòÀ×›üžd M¬È:à£óöóñ™lL-3þiÛÑû—ÌX0=VˆÈ©€-Š3¤Dž.mC4¬ÀÁÃÃÂa8ìå)IhswJËÊÙ²}—pʘȢr)ˆâ=7g¬¾$»/W{Z×&$1KfO°ÊIÁVZ6—–§AÏa&Ã#¹¦§‚ =]÷"| ÷”̱­»q²X»ŸÆÅËÓ@€Åºý«½4”ΊΟ§V4ìçQMmÝÞù ¿¸tÓšùËâ'Zå4ìëâ @"y©å~ìbÚ¶ƒç JJ¿òÔ~¾>ƒy® ñú2œÝ©Ïxà¦Ï|i¨TÜÝÝ´Z¯¯¡¡ÁhlœÐÚ’]Ù´ãØÕ“Wn-3ajlx×ǧyvWFL‚ÇÙñKéµ_·ÖØ@ _??а°¶†â‡øßCÕÕ;…%eÜR„%û”¸1SG‡õ…IV_ü¹¹ºK›[°gknµžÔŠy“{Îñö²êòjýÌ/€`±8GŽ·duE6EÀ~XÑpo„¹•WU}ûÙuD'³é‰qøÎ¹yq‰&Z¨~ó³Ok<¥x0³pY}¦]²TÉÞgBPħ·Æ=¿¨ÖÂ<ûbFƒ¤¤©É¿¥Ï¡6´5u åúº\Ù~䊟·ÆÏ¤>¶÷ghh$TcM½‘KÐË¥5ZÛäãà€9œ78€†ù2%s–Úúà¿_·Õµu¿Ýònƒ)tø¸è¨Õ â :\ÀÌêZfD“$«/~8>7ï†Æœã5¢nMÙe¬:À³=ØW BÅ9àÓÑgµf"p×\iëoãõôúufåá9ŒÓƒµ´´¼ûÉžÛÅ%ßyö¾uß±)ì ùÒÆ¤WÞülŸ}ä~ž:sG«?({ ¬µ)†ƒé´Ù$Áƒ«k·sQ……y »xÒ`UÓJ¾Sä=gI€äSohhj¬krjlhª0ÔuXÞÚ©«‰ŠcAãêÜâÞîãáä%-ÙÇ×ÇJàï'Ùƒ…B2gqv7:Ò„b¿ =oYP¢ÊšZÎ`ä¨gZWS-9 õ´Y Ði ¯Ñ¢ÛÓjÂŒùÆ–ßoÝeϹAA‰X¯‡s[\°' @[…9¼€¸YìU¿ªX  hVd¼|Yøä{Í›g1ï!þÊ#!3÷ö™«É(ÎFÈgÈÏǓ潷ïì¢Ù3ÆÅD[¼ŽÙ4¬;$ºþØQútÖh´ ¥€¿ŠÝx¢C‰xÞó¦í£qïjžÂ^ܯx1` 4Ã9¯u^ø_6øãª¥…h4bSìï91Ak°ÕåòƒýðˆõÑéPœ!%‚aUÄ’ÁÁ¼Hâ3)Áq¸ù®¾”!™zwºxøù~åé'îê½³ ѳ˜áxhuî‘láÑa®ÓyCR ÁÍueÆvÒn<¾*Á±õû€â )‘»Sk¬Ÿ‹ŸŽÍ($%ÚݧièØÉ'H-Ø)ŠfE 1*⹩©iç‘ã˜Ws¯±Ó3­äiƒê¡ó7A#ž»lJžm_æÖpá|[­$À!ßÅëþD}éÐFm€ÀƒÄÓÓ“'J¸6­¬ÆÂ<…G2mxäP %Z d:½w=Ob#ÁkšMb$;&F‚sHþ\ð 7)’!TfØñI™ ™ùY¥²êºG–-à(öö÷¢Ý²}ç ÉßËÓóëÏ=éïãSWWgÞײ‡›3:>¹ž%«// ³4}IçÂY__ÑâÁ;ap†w<_á«qàLrYµÞÛ¹1ÊÇ%Àdõ@ÑÕðkˆØÉgG-Ø/*+º÷¹ãm¸²º:#73Þ©ï}€Ú¢Ÿ€êªùSÞÛ{œCƒƒyîö³Å5¯?xP̉Ì}Š›œÙ„щÍ_§õ«Ñw5O¡™‰uز «èx›š›{@Œ„0I|š ô¢à:“NÊ”´RÐ!ÉVE?ˆfòbLæ,çÇ„E„]×rËn Û÷>}ù»)QĨæfSØÃîZ3´Ø´®5RB:Ù“Âè:¶©ÉOíèÀÅÐ`@¹YÝ´uÏ2Žùcå#Q»»8]w)¹A%$ÇþêºâéÔæÖèïíéïç'Éóü‘i@ÌW!ØM — ¿ØÌw©ež¸ëê©Ñ°Ô“è£1U &æë2l¦ÍЏa]O½…–€¯ab$ ¶ÜÁÁyÙ|¿¼y+ ¢ö¶¶ú£‡¥)¹¹y/Yª¨¹u OtIü£Õ¢„¨­­ò÷ΪÔw5Oá¡"Î ¢%?ÞÞ^0"ÅØ`K&G’¬ˆÎí— YkıÜÓH°Qá[7B Éœå½ý:Ç‚iÁ–`ØØžjN\¸¼ûèI±÷3šÓSKêMOs ¿$>äížQfÄEnáô8qâ"“¾«/N€”ÅŒ–ˆ»Ü ¯æ†¦–zc›[S­¡¨¦ª N ûÜL|ÆÅ©ÝÕ©Íß¹ÍÛµMãk¤ÎÇÇJ„íºNÓ‰°4üÊvö‰¬:ënP.+2^¸À-–){% §O>w|Ôg¥e¾Þj\¢n® +U-ƒ3hó˜é¯>ÂJ³°N7Æ+WZ+¤3Ú ]t:ëtj³^xÜšÌS¼`EÄå1‘MÍùuÆW¶|úøªyæ)&­Dx#PR"Þ$NdúµÚlš¶í¸“|˜âÿˆ(@&ù…€A6gÑz¸ÏŸ<ÖÏ××ËËäÞgV”œžñÖŽÝb1­Mš?sÚ=æœ#¤JP»IÙ¦ÆÈBkvs»½Ý›~°›çÔà’VWïÙ`0ò:' ¤3Ób×gÇ´4éýUÊ@ƒ5•Vƒü 'R"(FE€æ`J'k¨Ø™«–íå²¢†+—¸Ã©H°¢š:½J‰l}­ƒ08ÊÀœŸ­‡³]ÿõ‡eõY’íF±VÏ&yƒæ3¾¾¾¦BdloŠvj/Ô1O9tîÆªSÍÍSdÁXzàš61ŠÖšÕPö#S±4ñi1Ns–l‰‚½53ã¢Cƒƒ@ ÜúNâ ˆÿç­ÛDªÔå ñk—.´¥ëWA‰$êî(\çœ^%%¤[‘“H|å ÷#z»ç' Ž2ïVt…– §p´÷ì³÷èXxNâ/Û{3±—ÕÕÔId0+‹{\_zPH›úC&Väââ½,Q!Sê}Чx ü LÖN’ 8~Zß:ƒEB:ÚHĈ–(ÔLyë` °[ø­8¡‚1®=#qp0ÉÌx_’B4i4&Û/ÊÎn ¿†2]×s­ÖØ5}ú=Ë M¬ÈÅ××-,lX&À ’Ú©¥ïÓ(©¬}mÛáüÒ*q9ƒ>¿!Q«ñøÉ_wpgúŸ¯m¤ž;Ô¿ÿá#ÔFd úñkóÈù¯/o€ qÔO_ÿä¹/š9^ñgßURQóͧ×NŒ §ò§û‡¸—7¯–ôT IõÙä,2a¡ùo¿}ÆøÑŸyè.K—oþò• S6®¶‰šòw[àûï_x¤§é™×óÔçN;•6 y¸yK%—)É-ÅÅÌÐ+~®«¿õóØbí²¸ˆ«]\êÔðøqw¯×55!p¨onmj0” m#&ýq‡ØÁ™ Fθćkݼ½|}t0!øPpp0&¾}Õ74üfË»µz)txLdÄç7má>žJ~B_‰É42’(cVM«EB:AŒè«/dW h$3QsK+F_²"†³kV$W¡K”Ây»ITHÚ`ƒÝ~õ5Ýݲ¥>žµ™Ã# PVÔRVÖV-Q ñ†÷bÔ¯» ‡¼ññ1ˆÑ€ZM‰‹øÂc+Pxí>quçñ+¬›ò3÷/š;%–0¾¯}tä+“Èž½óØ•=§®ýüë›hs%57.*ôRjnˆ¿î³/£[úùã‡X2kä®Ìž<Û{ÀY Ý/ÀïÙíP6¸ÃŠV¬Êq9I ã%$ìx ›¼ÒêëëÑÄEŒ½Ÿš~A$ž¸ð Ѐj@2 Á„ñI™»çc•+ùm;nåä1:u¾öÜ“ÜûL,Ò™¸Î]Ùèvª/nhéšNž¶™ÑW‡±—í~S•µ†äÌÂic#}¥D¹VßdÅêħÅ(H`eщúUEÊŠÓÓÄéñVVĸ•ô÷nRT.…íŸ:6R,A”o—TWÔ@‚ütÉ™kNOÎÌ_Ñ&qîd¯m?|}–| y!ØßgÆ„è^ŠŸ#×#=òtwƒ¬¤å‘…~ÎäÆ%]Õ™ë™Æ„ÅOCš]'®>{ÿb_oÉ.ŠiÀr²òËŽ\¸y%-O(é2n—®[4=§¨<%³àZúí¸¨G/ÄÎ·ëøægOR,“9-ÿP¦7œ²qa¨ôüƒ‹ßÞ}¡+:q%=¿´ò¡å³ í9u="¤Z$†£Èk´£Bã­[ÍyÒóO3sÖ0†“b›Â7^y™¡Ò(?º2³aJ©Ù…R»î6îãÐ,:?xöFA™DïÅE†¬_2љջ;ÈaëîŠìJ}&ŸÎ&`¡32%òn”ü˜$o¡Ùi¬HÈŠÀq‘ɰW2gÉ õR8túÜSçh€~ìÅŒîQ8!ø®kB:êQŠaW$Ίltîn]sscsS}‹KsCc¹¡¶Íi⥷kg”æ99·{AÃú»LbhîkH`‡aÀ¤±1ÀnýY©=Ú9 eEÂԚȯcÇ#ÂwžBè>Í%ìâVîcP™öíc^Ó4ä¹õí0¥´ª?t@LÅ{åJ¥Ì©ÿóàÄuš§x áI,Æœ;=/ýÇ@:œ©˜dFÒ‡¨ìK‡—o¤¾·{Ÿh¹iýšÙS&õ~” D ÑmB:©¾‡ltØU› ¿ÄùéÏ­ª÷ õyo†Þ©µ‘Ó˜!ÎÕyŽ$6qìk»³)qQ8õ÷y‰jÄ€YQ¯§¹9œ÷11Îv˜ü!:ÿàÒ7vǸ‡?úÁòúÑñr‡ˆ‹Ð‚Í›+×ÈXÎ’Yã_î06—ëå?c˜Ó¯ßéxݰ2kë_¾¹‡P"è‹ÜR0ô„ýáýƒ?yé1‹]òW„XÿØyJ„uöôñ£±°&¯$qð-DA›×-@D›ic#áCØaxT£/·(,›3ñÒÍœ¿m?†y8vB{hÑÆ!¿êv ŠV$9ÌûËfáƒYHIyå«o½‡Þ‘Nâ§MÞ°æÞ‚Cpæ!¹öClÝ&¤ë$F¢¥»r²Ñ¹æç Š>j0¸õëXР½ÀÍÒ© ì¦Æ‰ŽpÑI¿ÆU;<ÎÃòæ÷»7·‚ìËÏnîßÚ·—ýè?Ùøµ¯<ÿÙnÛ M%8'—––¾»û@p î¥M«ú5®^JghöÓyi<úu`¿£ÿÂJ Õ˜N«éé@Tþp^nØ&ÀOwG¤ IÂÀfcà±¾¡QŠ"{wdý®ƒÒÆŒ~º¾xõˇ¿úÞòJý“ëW…††bç˳¼K±ó+9ÿÙ§o¤0ÕÑþÓ#¶ÊkÅU˜kÅnÕ®@]}ýÏþü÷²JÉÆnܘÑßüìÓæ¿ ^úçn£×ëËÊÊ ‹ŠŠÊÊË3+êÖ}uójÞmÌä÷…¥æ^MPB6º¿ì›žÙaï\‰²¢&dE¦Íc\‡]ΰÃ4° @Sza*ë³ëQ¼¢ ‹¥®»äš{†BeN‰8ÅY¸§¥<¼k”#yó½á1g^ãðe"7 JägkJäð`Úï1¹ûÝ–÷% zé™}¤D,YÈŠðqã•z„÷_dcS· éø}u~)"°>ä34d¨½ºàFlzHŒLŸÝØ~ñêxìbÚ‡Îûy{Ý·(„…¡ý^iêÌm‡€YQKa‡Ç“ûèѶ[¹Ú³Š€Ћ,ĤIrõ™ñ ]!ÂùëûåH·/_÷ן{ÒÛË«ï \VDH¤{&¤“Ù€ ¿˜ Ëä“Äd}_ïà[ŠqÅйFîÜ<]d ßòø£B‚Z àr3µ " # DVÔ\Øa³â&e·P7å#pÇû,©jVå/MaxwçÞ«©R( ü*^~nsP€¥¨õžý@q0*B’jLDCàå'¤1>øüËGGï¹Æ¡i€'žy;_/„qÑqÑQ!ÁÁÀ È@=43QG±;”xeY‘kH¨=: ÙÝ Nxð´TT¯\¡·ÈHÏ öakð¨=ì>zòè¹‹à€™Ë7?6&b otH/Ðì8[… jÜÜô¦¸D MH'B³ÙõVVþð^ ’ÌJl¦vn®Q¾^a~þ~d¯cëWöºÎ¾ÔGŠcEmõõmµ’¸{¤¥;ÕÈ:3w¯/wÜûñ™Gw~÷ž¾~ì¶¾Á‡µ]Ò¥õµ µ]Ô9Dœ%vêTAQ9võÙ«ÉÛ÷k|æ¡õÓ&Œðz±ŒA’!œ`Ð é5˜‰„t&:/™žu«ÒÐÒÞNö“Ø€;î=5¶u½P¢#ÐA1Eˆöd¯³õ<Õþ‹€âX‘¬>s³óˆ/Ö:實æ?x(-·˜ŠM/š9î ³ˆØ}%» ìWoíùü†D‹Ð‘}ïAmÙõ‡‰]Þ+WõÔF­wTÒ²rþþѱºW.[2W ?àM0!žå„ H¶¿qÆe1’rˆ‘sua #KÞÜ/Ü*šÀ V„åáÈq4c“³×,l‰6VNíÄ!P+ºcjißqð¬u¹œ¾z+-§6CÖŽíG.‘ìlrlÄ´qQôÏmWù®nn]3ØÓ˜hŠ_|lELD°<1‹´ö=õ&·W Ý"àf46\"•£óõœ6­Û6j¥£"PXRö‡w> 2 \8{¬hð+å™Í“›ç7¬ˆç:a +bæb$(‘Bˆ‘knqskD$"b8ü‚ëðÉLt §@”’”ÙØ¥R¢Á_¢ŽÝƒòXQq±@Ü-tè‚)ùߺ]Êoœ”daÁþϬ_4ojœÈ§A@E²ÂlHîñùG—“¸þ[¿|‡„hÙ…åHƒJ*jÈGöóolBaö»­û‹Ëkž{pñëÛ~î‘å´OÏ-þÛÇÇðÎ à5)Dˆ*Ùµ7%c¢¨¹ÊºEÒ;¦D–õ†«¨ScëÉT×ÖýfË» ÆFš<6öÙGî·Öˆ\H#íö +¸ nħ¨écj³Ž€Y‘–Í5PeE3&Œþï—Ÿ8Ÿ’EŠÙ³É™§¯e|ë™u$â j6éAÈ_ÆO¿Vß_*áúÆÓk)m€0ŒÉ™RŒÈÐüéc‰UM™ ›ë¬üÒÇW',œ1Ù£¬ò¤YöF¾6Ñ^ýì׿¦àÜl¸øhfÏ饥ºË‘himýã;æ—²¨?߯=÷¤ÆSÊ»lõM<Î!<ã;éd×ÏÆWQöO&ÉøÄé}x'#f"&# “k†wbêèv„€òXQ¥,+fÃ=…œÅc—Ò¸ÿ­]8¿ªÚú¼úáñËi$ezD£F›Æ*3_o)^Ú11m"êΙ4欨½=<ØŠƒÖL쉻)¼J|Å‘¸²Ö@¹ko¢úÙ !9Y®&Ÿ ïÄDÌzi©îr$Þܾëf¦Ä†½4žDkô÷µ9Ïxebˆš‰ñ‰®J™3Tg¥"Ðw§%¹£ASeE¦Óˆ\çÝ=g>9z™YcÑøxÃrH1¦o0’Ì•Œc©9E"‡¹kÅüic+jô䮟?=Îü‚€$‘ŽãÈ…›ð¤7>>ö‹ìŽ ì¶7ó£Ôr·ŒÊ”¢ö±y'®õÓáØ~àÈéË×X&_yꉈQw%)søå« Tpl”'+ª’4AÎZ­‹¦Ç\§Ž}J,V÷øªy¼&»œ¶ëÄU_×ÒÙÖ/‰ õ3÷/Ú{:ù?þø‘»›+VØ|Z8~L˜¿YÂÔ»XÍX6ó­]§pÔç(¬‹"BîÙ›EçêWhoi Í–rö9k4^ C—S8qáòî#'Ä>»á¡‰q1Ã8uh«# #¾.˜>nΤ$Iı]{“T =!Ðpé¢{£‘½ÚÅKp¡î©™Zï0$§g¼µc·XÎck“fªæÜª Qè@@Y¬¨—SS`k—þdU '2$ò-;àÔôD¸F•f­Þ,ú!_ë+Ũh„,Ù|™õ Éi¥e8¨ŒíNJ1þ5ŸdËØÄh½4˜E††L›8®Û¬®¹…EÞº 6}&&į]º°«ÍTTì…±¢†³§ª>³£«h„Nµþ¨ÄŠÚœµK¬»ÏŽ@¼]T¼}ÿ‘”ŒL(B ŸÎ_ç¥õò´ëhÁa„Ý¥ggWÖèñ?˜:~ì#«G‡‡É'¥¢ªúw[¶66IŽœ3'ßü€äé©n**އ€²XQÛV¤ê#ïbs¨5Þ¼ÑZRÂ’*£¢Çûú:ÔÚz^LKKë»;÷¿pyT ï¦5óñ…Äv­çæö·;¼«éy‡ÎÝøñ«]:wö“¬#ê R1¢5ÖêëYOLdÄ‹7ˆ=ö·Ž¯>ÃòÝO>Í+*þö³÷9<%’¯XVúÒÆ¤_lÙM(TBKõÙM£‚•Z¯î?|ãV™|†ÑÀ‹C µJ.™C¹ÝÓ l('£Žå(‹µl»"ž=˜5è´žD¦¾çå…D?²žšII¼ÌuÈxjë>ZÙ·_ÛÜÒêêâ|êZF^QÅ“ëd”‘"çýÓÀ|2Ñzyzä–“‘m$S"`©?Òá}V<E\–Ùù'.]Eq6r(‘øu°Þ'V%¼·ïìƒ+—ÆFEŠJ%|"ºÛº ¯+’×Z4ðê/ÈÝ„õ·µ½ŠôøXtd Ú´+"ÁÙ‡ΓŽÖòpâìu‹fÏzïéëÿúÙF ü`ÿ9üûùûŽã°,n凇ø“Íœ ½˜J' Óâ.¤dc%úìý‹fM¿á¶~új/”>ZÍK›V¡8p6eÇÑ˘Pœ¼’>qL8¦°¨÷÷þÿWÓòþ¶ý×Àó-!F6aµ—diäY»]R996bX.å j¦>s|£"’’î8p$4Ð["圂!› «>tþFZVÎêÅ  Ù¸½ T[§õí÷o•8°W/ËïvWWƒ°—žÙäãíP®Ý.\­´: 3 iC *m#Í3?¿¤rË''fOóòæÕáÁ~ðƒ±1È_GbWxRMáèÅ´s'ûzk2óK‰¾8wJl´±…eÕü ÄÄ'{‘ý/–×G¼™]D=ÔêÂìÍkç ²ÞظóØe*!:°¥k·n?°tÖòø‰¦U SIµ–]XÎÞàŸç\L%ôÑÑß>>îîîÆácÝBªÄp#ó“¤4Æ«WY»[T”>ØÁ-lˆ V]S›š•›”0Å\ô8rN=«^5*€h ï‘Û¿ë½2“ÆO#ó¤ôt@LÐó–VT`‡D­§–j½Š@O(Œufva‘‚?:t!ÍS÷-œ:6’ Ô„!Å=oçœ6XÈžÓ×Q«­]4½ ¬ÒóÀÒÙËæL7:”½°þ÷?lûá«òGîØœ‚²ñÑ£P„‰ô±n®0ªýgRã'/‹Ÿ´páé<õ û!™Ú~ñ‘• SŒ" ËÄh?-=@zNœ3ml͘Ùk™DÐ~ááeÎô¨ᬨþèQ MÀa$ä>kii¹šš?W«k—ÞRÓAc—%bïìüôvq O#M›ÙwäA|0ƒÃÐú~ ÚRE”ÊŠF’µ52›Y…±‘¡PNInQEdh€¿¯÷¨@?¾–U¿”þèŠxL|r ˨™d²m¤/Fþ¾ÚñÑa¨À&ÇEÒ@(¹h“W,9ÇD†ÜÈ.¤ÿ±Q…‚ÜÔ§‹ªÕ7@¡M #L…= Œ¦“øýó9&<ø\r¦VãÁ”øÊQ´„NQ±›¬>óN\éØ innÎ/* ôõ¶u\"Æ*(­JÉ,(©¨¨ò<ã‚äõ@Ý15´äSTd—øÊÛ‚ÜÌêÖà gõþûÒ¡ D9yg®$?ž4O¥D½ƒ>%ÍÓ8•õŽ•º×E¨Éå9‘ñC”]4#(Š#±t%ÆSTžq»äfv!öÎèÑ >Þ§;@Q£0™œ‚rj°¯”Ê…e£ÃÉøñì‹h7² (ÄDH:ˆŽ©|ÛD.§åzk=Ñé¼5«çOMË){ùdCnÄ}òÔÁ¥:z7—c—R&]¼™—:—œ`n‚mê`}`úÖpö, vñÐ̜锜æÀ‹Ç¢¨©©©º®!¢M—YRYûÚ¶Ãù¥Rfh6Ì×>¿!.þ“¿î@,÷?¦t~<Ûþýøz£Éýñkã7ð__ÞÀ¥ÈQ?}ý“çXL.?q¸->A@ÃÃÃcXbRp.ŒFãÎ#ÇG¬WO+a‡/ÜÄ$î«ÏnVˆAX— ¶&+êdEÎî#(Nf@-ŸÍ+ò+[>Ý{ê:e¡¨â‚€56µl\ „:Ù…e1áÁ”a0XYh² S"*áL*œÅ„JKí_ücwIEíçY†¥6ýH-Ã;̃H:[­7ì>qUæRì¥ Uâ/)a*AA^ûè¬ n‹Ai9¢6ÃéSíMM,Ù{ùrgGØ$XQ½¡Açe÷¤/o|| b´fÁ´y~ýâ™ãSsжî=Ãu>wrLU€#¼¬3çN‰×õûÏ$‹ò|¢zX˜ ÁpCC—WTfäŒX/ LîùQ:X)Ä ìž³U(eÉŠœ:L¶7ß›+,'yœsñ¬ñ8—ÊIìŪ¿÷ƒæËÇM|åùßÞ|åû—ÎâOT~ù‰$QÀÉ?54_MC’»Âlˆ?ùðgï_Œ3 »ˆÜ¸~ÉLQ«š( q{å[››šZl-0ôÿ6ÆY^ò€ õ‡‰c^}†lF°"°á[ t Ž4tCÒ\°EÛ‹Ìò|JöSëÆO‰Ý6%%3nrF>{©:5^öžN^´ƒ«£hÄã\úTiÑ@/• ¹‘xœ²X‘‹——8 JcEZ­ÖÇÇÇÏÏwRxÒX‚P>Sµ1Ì/ôÀð•-»ÁTÁ„ÁY™¬¨þh‡ Èáƒ7未Ǫ¨¨Ø;ʲ+rññ€*Ç3Ó"wwwžÖ¾¾¾„ ÁdBKKvYõ{ûÎ:wcÕ‚©„Oíêcï—…Mç>gΤ”U×ùy¸Æ†øøA‹|}Á´hm]ô¨ÀÄ;1Ѧਫ˜# IˆùoÚÌëÕr?&ƒ°~ÌPmª$ÆŠtk•#+âdaÿ‹½ ’ Ü@Dp[|Ç+kõeúú­{ÎQVD®Jo­Fq¶0JºÔ¸»×›ÌiaE¥us‰ñÕúê‚‚‚Aœ˜Ç±µ¶Öxå²t%„…{Ž¢J:uŸ ûDHFÂÆÐ ±Œ’{©ÆÃ_Ã÷ I ïvù½dpçê**#e±"Òmºètmz}{£"|ÐÄ•Á¤Pë`õ"¸#‹^ollªmln46” m&K>Ó‡õ¯¨ÖªÊ¶šZúu "«ÐZQÞV§§[·ÐPg­õS ¾H°b7çP«¯§;a¬u:LJìïïÂà¬@A‘áä bžŽvÙr«#ïØ’íx׉«¬ñ•onƘ:=§è×ïìãëK“¦ÝuíG.¤646Ý·xF×]æ5uõÆïþæ½—ÝIüg¾·˜¢–UTTÊbEÌÉÅÇVÔZ%eÉVΆÍ5b øSÂ=JH¨!à,$!@b—h`‹i7U—Öú”ž53fèV¯±úU¿ßZQÁÚ_úªrY·2=؆ÕP"gH‰ A{­¾´ÁwX/‚7:9y/WYÑ@à$^ÆíÒY£3òK)Ã{ä^&!@Bt$j®ÝÊC”(³¢æ–~s$é“ÛÓ˜C䯢`ÑìžX®~UPP¸s»‘«†· ™9µð„VÒ&˜r BP$ì¯õz}cc#¹e1’ˆQ›Fãâ&1·ºZh„u±!éiK]­q‚BB‚lã`n:x–CžžžŠØ°%‚Qº3ÚXwiƒï­½¹¹áô)úqÆE.^Jê®nýE€$¯™ù+ÊÊ/ &¡=ðKykשS×2(MŽ‹üü£ËœMIË)†÷üæ}_Ù¸ò/I¾•ïìâòõ§ÖÀ0GûûŽhgw¦þ€Y4C:Õ{'ý¼Ú^E@E`D! „ 6‚"YVD3±"ÏqŒ×¯:µ¶„{y¹æ¬[[Ÿ›ãnRaøN™ê?ºk‹öýý*³" ƒý°LHÀפ¾¿}Y{ñ£b,U}6`Ìã"CáäU´´¶’×Lôsñf)ïÊ«ëk×IÉ,xrÝ/O÷ƦfòÂò‡íüéÔÕ[´/«ª¥eQyͦ5óÍ‘J[ʾ‚üÉ¢Y€/õØÉ€— ¨" "0BP+ê|Þ·–—;ÅÆ)ð4Vij&„´ƒOP0!±Ùn®S&×ÞL¡½^;eŠªÂŠËMºB0Z µbÏæ]ÁÄt‚ñ)jÌ›)­ÜaTäâ¢]¼Tis³—ùŒCçëL$ (‘ìzVQ£wqv ò÷Á£sí¢éæÆC¬ëÄåô·vŸZ2kš…ÓþôÁ!jêMY`¡Ð”ýuͺ63Ǥ÷½æ-Gf¹¾¡á­ÆS•€ŽÌ󯮺{”ËŠ”fZdŽŸx–CxÀwÒ¡ç3¾š·´b¹}ò”V“1²gq¶8Vì¹¾°ÀÛÔsÐÔiVíYž$ˆ‰²€NùdH̶1#£¥P2‚ÑÌœåêç'/G-ô 7W—1áAnd£“Œ‹ ¹’–‡5VG¹46JÐqg\îkê yÅ…~ ‘¿(~Uc"‚‘¿”à{üRšè§k3S}È£«øÁï?üo<½¶h¨Î}}Jmf×(޹‡@Ñ )Ù¡|º{OœXmâí(¼¬jàÒžíNÏnnÚ¸8Œ§”ûÍðŽúLõÉèp ®ã:¨Ô×Ê„)eÕúºˆKÚ¨ ¿kæS9}\TNaÙ>8¸yíXÔ·~ùΤØp­ {Z>ûÍ]'ýöÞøNkë…3ÆumÖ{'ÒØêÖø÷Yí'‰XSÑGç¾®pŠñ=”ÇäÕ‘n‰Ô ×P Ju‡æ•jYE`èPÜ#x<…Öre¹¡ ý¹±Ñ}LŒ¨i6I/,öæks‘$q­R" ë5jFX dúøõÁå³ù£ñc«æñ'Ž’3Þ?}ß«jô† ?&®˜7yùÜI»@­ó‹olÖ7ýtw¢gÁfOclj&nêç;gеYït§þ{ûíû(7K*jJ«ê&Ž {ùÉÕhÖÎ\ÏØyì*æ\!>ÏÞ¿8-§¨wç¾n;IÏ-þÛÇLj¶è ©;%ûöîÓð$$8B”÷Í]§2o—xÅÜÉ₹39µ¤"0´(ÎÊÕ-¤CVÔb²¢¡nΤ"vÒ3ˆQMήmèÑDÍÔ±‘SÇ>ZU[ÜÈ".@¿–£6V° Š”EJ²"¶¦ÜÓ¿êGÎ1?Èçj-Pä®Ô$_2¤R<L­Ùˆá´h±\¯¬‹À¥Ô(*•sÉ™æ=“1 Ïmóʸ8!¨°¨õr%uΧdÉ_Õ àÔôËC°Ûá :¨Ò’È$I®ÁQ_¥D2jaP¦¬¨ƒ5çä8-^2Œè(xhsÇÁMS ›Ö¿¦Ô›­e¥T{ÅÏuñöî²_­°g¯gñtÄÖ„ ×Í--WÒrÿþÉ "\'΄Uï£+â‰÷øÖ®S§®e ?žI¼–ºFÇÙ}üªPçëO­±ÎÕ^TTFJ”¹wÊŠšUYÑÝ—£j~q7¶úÖ‘ûÌÉIÍk+ˆœPÍdÜ.™3yÌ,S`Æk·òÅX¼€×™b‹Êª[%µ·g”¼z O¥7$¦fn?r‰–]£ã˜Աݴ՞UT%ÊŠ„µ5¸7åæ ôë’Ó2 J˪kë x=HQdFÈæìä¬õÒøûúD††Dw*ά)ßéìËØØ˜|åº ò´‰ãdŸ|­jTd³ŸÙù”lúžIÔc“- Wm¢ãÐæéõ —Ä:<øgoì¤ ©ÐpêÆï$LOR2 Ĥ,¢ãàÖ$Ô±Ù¬ÕŽUT%²"’~8k4x¡A»]T¼}ÿ‘”ŒLÞ1úóÓyi½<­§=²ƒ³K`"Øà­ìì=5úi×l6å7>ü8=4BÙ·±áËiRòQqãÝí`l³36TÛæ÷vÖdKôoïËHÎÈ—œÑÌq€BìÂV×ÅÙ%Èßò´vÑtÙúÄ":ŽMáÞÃlƒƒM§­v®" "Ð_”ÈŠ æÛxófkyÙ+¿ùc@hЦ5ógNˆ¶ˆôßߥÚ{{œWsÿ·Ì)+…ì;yöþÙñn&ŸŽÁ¬‹üWSÓ£M],™5á…Ç׫ gþm«€43,2¼¥uð æ)áX77}C£ug’_RYXV=grÌœIcèÙÁs7ˆwŒlT /)c›Z®¦ç‰AÉ,‹áˆïdk–b¶Ë¬:êøùÜÉžÖ¥Í@*`là0#ÕcTTì %Ú ó˜ã¦é1ÿþÅGˆ7?Ÿ֠QÁ~–䌬W^ßRW/ŠðÆát’WT"z˜¦‚ ‘y9cí®ƒyÀgG9zy¸W×Ö[w>ç’³èpõü©¤ åï¾Å3x•O¬N€í>q54À—6Ôcm½tÎD¨ÒO_ÿ%Zü”Øž&#Ôé©Á€ëA|¸z Š€Š€½  Ä·g«êD8á™:w²7Û š¶žgkuGHëÍ'ýáTê«o½÷Ï=70a sxYEÅËæ8¥]gæ­5µ¶ž¿òûo344\ºÆ<]ýŸ~ùé?|ph0 +½½Ì:Â^>ýtÞ©·‹UZ‘4oHšËŸ<ºN«ùã÷Ÿçkkk>ù$Ã"•:æDY÷„YOß·pãꄽAŽÓmt2y-Ÿ;©©Éjq+Ä Y;Ù»&DEȘÈ3W **†€ Ç»;÷d´wèð›óò ñÁ,§µªƒEOŠûÊÆ¤¼Â"°@‡ø²½óɧN'¡±q2[+«ЕƒÒpî²S³ôLõZ”:™ð@’¬Ï²éº\]]P–½òæ§?úËö×>:Bœ›¸¨ŽtѼ±ˆÓíL°=ÒxZY¨ÃÚAºQ­TPp$'+âQ}üÂå§î[ætî8@7çuø›8è^Kk¥)ÑÒ3_ŸX?ßÇ’æ½·ïìò„9Ñá}ïJ”_pâ⬵b#CŒUYxUV††“g’ÞKæSrâüøÑáa}Ù1Z"a#Þp€·×þ3Éý àös,»]Ry»¸2ÀW;>:L¶­.H ÀÚA Ã5u\!@@Y²"Øxœá©»8i¾³·d/©ÊŠÌ/A\\ýý¸;S¿,~"XXßãѲ¥¥åãÈN'we1lä•q€2œ:Ϻ==5óf dG‚¸¸¸¸ººÆ†`Ðsì¢äšgëñɇM5씈ŲjÖàÀÜTbdë @í_E`xP+ªÓ×ßÈÈJJ˜ÂíÇ#Vrj¾]Ø÷GþðBiëÑÉ•Ûf2ýq èäƒX¸õqôÖV˜UuZV®™£0 Ǫ²¢ÆiaÍÜ™.žžrÊ­L‚fõd‡i&(lÀÏ[èåñás8‹9Ìêú²Ö˪Y;€˜ôå@µŠ€Š€"  _8áI.ßH%”-Nø éÃg{ccK§“”Bl­iKlSjq×À¹O°1 ·Îà.ò®®Ú466^N¹2m\¼4bPPY‘…vái+Ê/.!£¹?‘kP Gµ–•›ÇÐë¥GÝÕ`RŸ±:íÂ;îQ|•@öÓ›£.¼Ûu¡*‚@4bóôŒðöhmn~e˧‡ÏßÄÚ¦Û££’Õ±ÆW¶ìf½¬ZƒFÕ´˜ýõЯÔÍÅÅ꣆~C<¢hjˆw€á”bm£©©©²¦V~Z{t²¢¦Ôt§¤¥Ýb/±àp2ñ÷õö¢ ê6ƒ± 1·ì‡Bâbc+É^FÉ-ª ðÁØ¢C’S65·ŠJŽ¥‹}üŠš‹ MžÜÁØCO5eæŠ]î±|Q|1p=^d{T \___m²èeeKAQ{Sss~¡Gt‡Kš< ,*¯Á99Ø_7*H ˜Ô;È4(­¬å¹‚‘܉((ä–²ò¦Œlæé1.ÖmT‡ßÓÿgï=à£:νõºê U’ $zï¦ÛwÇŽÓÛMœä“äÍÍÍÿÞ7ùçÞÜä&¹îNw\°1½÷^H€„*j¨÷ÝUCïwwÄá°»’%¡²»šó‡9sfæÌüfÏÎoŸç™çQúï¯ñ¬®­Š.V ÛMB°"OOOooo///o.¼µ¥L׊ÿþÓWÏH²?ߪ|ÎÙq¶÷dzyM½·S{¨‡‹·§‡aìÞÞà XÑ`ͯ›‹su_;ެ± Øsq4æßáæmÀ*dÓX +ÂÞ¥¥¥¥Q«ôLÝn¯ýÉŠðùöáÎÄÖ¦0F+çŒ_=×`!ûomfÝþý%Íúý¯¯|Žhä—/Ü¿õpê³WùéÀO½Q1aϯ™ç§¹ãwï©+¸Í¥ •É QO¯šmNž¸ÛÒÚöÙÞ3) QXƒ‹ßuâ+õÏþôÙ•]zཫJ7/š³ k6‡[üãÿ'B €èak¶@X§Ó5êôþwnl0V{Ô°÷ª%+Ï„—êÍ ËŒ{ßFqí/·Î@ÎÈ+Ù°ë$,ŠÖB|Y<-eTtGG¬díñ³¢Ÿžw«ÏD¦d]×+ô›„`EpŸ††><ïövMK[]]ý†'?ÚyRàãåíåÑáKÃ6ßîàШÕã”VÄ@<œÂ\Û=]ÝÝÝÅðA€ÄÀ³"!—âÌáíîVT]I•ŸŽ¶ öÀõ¬¤£©ƒÛ^ždE¬ˆ/ÜÖ6¤5Î[ç?'¿[5µMYhvï½ "zwëQïu‹§i¼Üñ„ é‰ œ3ë=§Ò‹Êª#C ÕkõKf$ñ‹2ó&^0eôµ\ÃʽéÀùgïŸc2øÕõõö¸q#õZþËŸìûùs«D¾_L\" :tîÅV¤¾«4ÈXìŠu¦’c^ ‚oSo´áÞf‘5g牦„Å•Ò,}âY Çšõƒ’o’à.e´Z­¹®Mi°9'Ïû¾;˜ yûËã¥3ÆOŒ9žz¯z@÷šùAæ‰[_`)ýÅó«AéÕO÷ÿcóá?ýøq®ÖÈH’°›hôdu#n‚¬=~Z”÷š}—Q‘ÈdÐèaQÒžÎLŸ(Ø€¯¯/Ÿ=¡ [Zøˆ¢ÖvsbŠÚõ·Zo64´Û~0G‡vGÇ@çv(‘³“A{%BDÄØýüü8ƒh˜|˜rº½=ùò» _ÿù\Û}–t4e»s7ˆ=· VÄ÷,ë &/·Úî2VpOŒÓ:ß®#Ll¢P`KÍlm»µnÉTašâÿëW>‡¬ÀŠ+JÏ.„r’òää•c•ìïãâï3lŠ_ Ÿ·š‹(42bX°ÿÔ¤‘ÿâЙ+¹ÅåÕ<å‰×»<ç>0"Ö”ßu" ±xMÐ/\º^À¸îŸ?qÙÌä7+Í‹½·õÞ{›[[‘*}ýúsöJîÛO òC¸õ¢Ñ½¯Ò1‚9ùùºÌ€Ô¸òDAÝÔwI+@6³q½-|*$¥.œ2¯¸bÚ¸‘ 1ýd „:bùL‹ £5Ë),FϦØýàñ%Ä?@·»­9Èè4°¥]/ttrŒùáK{2|Ȥ í&Èhõç.2pA¹58,0?]›—·Ô²XÔ )1ühim>c\:¶¶¢câ0ŽTœ Ÿ4ÛøÝï‹áõá6æP¢€€€   ÀÀ@ò~t¢Kœñc©qs0ÇQ?Ò¾}"/¬t4Õ·‘ÖîH2qÀ|Õò…Ë’c²`»#zÕd¶ý§´²ÓŸÄÞ C|ñ{{³Ò ¸!„$«rZ¶ÁýcZv!Žø¸L> ª|è'úˆ}%ÈcºÖp ¦zyu=üõÜKϬ˜‘·óØ¥ªÚ†'WÎâÖôä¸I£G˜ßå»WZ8ÙÇËcË¡ úæóbéÙEÈ]ž^=ûÇO-¯oÔ_ÈÈg9Ù¸÷L|Lèž\†ôëµÔh´VTÞª«§e·¸†Ü}Pô”Eë+a+j¿‹zr×52ÜÑÍ„"Ž2Ôqp(©¨áœi¼2œDœ-‚ÌŒKæ›?ýŸÿ-8)&®§Eö)T@NÏ.F€÷ÔªÙß^·(»°ìÈ…Ì^€lÞH÷A&ÊG»ÞÕsÆd©¤èªáltõ¤ž”;·ì7…hµüÀßß?888$$–@Z±³A|W@²ÂAa‘°¡³ÒgFÁX¬°£bŒŒ”ñ2j1^n ¼ HP"ÑIÎ!Þnæ8ÊÖ?ÔÒÑ”­Ïà`õ~úX*œÀðCÜᮚîI¿Ú›®fú¬\¬®È²õEuE]5y숽§Òá4ùÅ,™FED¿ýÖCT/f\Í-†3å—?¾|¦ºMuÚ¨Ës€i­_>#5#ÿäåìô¬BžXYÛ€(„’˜ßúj<ÍïŠFXàÇÅNýh÷•ì"ób˜æPIFBLò¤‰£‡C¤PG…9œE¦®A‡5OÌ0ó8š³òŒÿ[fEàf@¯ËŸéa3/†¶À56¦9# '·´Z'¯«#arŽ€@<š3Ú(ÎÐDÎæ “‰RrîÄQ½®æ–l;zñhjæï¾·N™ ¨d¤zß}tÑ•œâã û Ë«ëfO Ñ# ÀnÒœŒFº²J}fø´X<ÀÍ:‹%í,Šƒ˜DŒ÷öÀe}}=:5E€dÄæ®—×¶@PÈ£ƒý["gH‰ DBPƒ2(A‰ÑÔ¸»ù¸èp¡ÄÏ~ï Jlâ¡|JGS61SVØI«`Eâ+Õ|Áö¸­Ëhº’i‚û›PQ±ôN3‚[˜A)ÆÅw˜?…{÷‰´û ~ŠEë7J3óKÐÇ£C$õ×öœ¸” üäeq‰èâ‚bŽ’ù%hÁfMHØvÄ dQ¯vÀâ]A× m懴y1¾Ñ~õÂý‡Ïg Ï Þ1ÃaÆut[ìªkJ¿&n #EB}@žXºÔ·DZAòt7óì(ë– +â¢)-ÃsÚD‘+6‘¥çÍž`à(Ñ0ãN4s´z”˜£†{t©!\ÆÁ³W7ì:u9«`BâpcmÓ“òÑ ™ïo?>g¨¥3ǽöé~“ræè‰& ›7Ò};öä;;yN›dòtåê)%gˆ$` p$($X›iÐh4ìg–F|¨ºþøY9P %btˆ‹à|HÂ`BØq&-tgøJ$¸}£‡!ÎEÚVÞß[¿D#‹3"MY„Efv«`EôÕâzã¤ñvQšö–G•)1‘±ž»Ê.6k<ÝÑÓFÁb؈X hp°qû5ôMÍ[§b%³`ʾ¡Þ2§Dg¯æi<=àO×òJX¤ùÄ’qÃîØØOâAÂîTcî{'Ã`ÏÉtg'öm½³å¨I#G.dÐŒ‡Ö,œüo¯}QQ]?3%ž½o :}rÂ8¸ãRÛTêιsxŒï£¸TÎTî’èXÒùÏ-òœ˜Ò°uÅtg.(¬(&<Øïì•<ŠøxsVA¨‡MÁÐ ƒAæÕsÙhÒH7AnÎɾ <’Ç:ûhÔ]½+m¤Ewå ™ ^AŒ+‚!AØ’+B\+â0|²Œ3ÛBEpÎ MŒÄq†’/Š üÐDÇè=…¸µ–77ã8ê‘ÅS‰H#dºß7+|"ßí(ι::DjÜ­ÄÑ”%»ÔÖŠ:ë¢{ÒhX‘΄²rÝÇŒRŠáºðk¾·í˜ØN 'VÌDJ¡@’±ãØ¥©I±"'9!zí}“ÙÌÿ· { C,ö«ŒÛø•ò"ÁF6X ±ö³»4ÖÛ\|éO ³n´\C)UH˜ƒ #Déöãÿþptl8l š‡›kOA6oäÁ»rÍ ƒL‘Ãs–…Ýgâ–<³é˜-àAݪ1”Á~ÈÑ æP_yàÇIZ¼ª „ANfEOA¶ØÈW‚\òÝŸë/¦3¨È÷^Ñ÷ÌÈ º¶o>ö Â&ŸdóŠý”3XÏe8|cpÅC'›VŸ‰ ‚óq¨-x„|ˆÌ~šÁî7 ¼0¡êêêÒÒÒ’’¾'kjjÒ~S[{ý-'ü# ¦³ÇQ݇E”dà&ަ| ަ«aÆWbxxxXX{ ¡•ÌoOÛ·X~ß>‹ý‘™}‹€õËŠî\;8˜²"TNlz„H–jѲâXRý ~Evq—[ìäW—·ØœI¡MJaL¼•´Hèo«Ï<'§˜ÜêÃKÏ©+ÒMÕ,™¯n[u‹.”ÔeÔiHgx'u!µ&”ˆÌž‚l±‘®An«oЧ]åY.ÃB;£Dê¡É´‘?rÁ8 X”„m¡¤P14q¶ž!5?u™xBVçîÐææ„¸ÙbÔÜØ`Ž£zмõ;šêéˆdùÁEÀÚY‘ëˆG/Ïv­Žmhƒ‹Ô >]wºÃé¶Ç¤ñý× XQÝÇ›h%š +꿇z˺SçŒ^²ßÍê=-+ªüŸ×Q¢5ì;øýo»èi;]”W@>-ÿÃ'p8NÔ´.Ê÷ë-Å¥µ§¥ˆ°ýúhÙ¸D §(ÄH°"rûðÕS((â X˜@9H£M#_ëEû²ÊEÀX‘³¯ÆmTœ!EñÍ–›e®ÃB-βevÝALÙ¸d±™l’7ÙÆ.z$Cøx$b·ˆŠŠ'J²Ã¼³½Tlgçú.ntúf“–»n„öé¶Éžu•Æê³ñ\ öš9E{ää­š:Ý™ó^3MMmº²Vߤê¯$5ÈìhÑ&.ðÒÔÔÜbއÉdõä.ª¨ŒŠ¦Ü~ ü_"`½b$4höä5ªˆ+¬4`?ÒY•£©^ JV±l€Þ–…È#×ÕKÍß2®YAyUðÝ·|VÊo_g;Uûo¾ùæ‰ÿöú¬¾Ä¥'V+Þ!Ox^þáËØ¥ŸSX†ãfB»ãº ´jLl¡OqZ]SßøÓ?oÀOÏì £p®¨~"™ïm;~%§Šyzî¹ÜÝ~ôâÎã—a9¢ep8cœ9>!-«—ÍÆP!wuæ¹ûç¾»í(QâùÉ3+%žGTÕ5ª«ZÖ°ÝàošÃ{á‘èï³fÙBXOA‰fŠÌAÏ]'.ÿük«£Ã?Ýsšÿú5ÅåÕ›]€bÂP]2Çæ Ó~nQ…·çþ¹’„!J.qåÔ£3Ÿ,™¾ÿzg;3 Ëæéá^R^c>ã&UÔó²nb\@vOt‹u 1–‡DÀ&€ÀàˆCŽ·Qvà5ªàó…Ï!ˆ‘Add¥»(Ñ|ÌXQaiÕ»[ŽÎž8jbâpbŸ±$Ï›”äçSTNŽOã›/<5x¥p#<ô~°ý±Ù'$ÆüãË#ÓôÀ™«*b.²HS‹(°Ëg§°¦ž¼œeŠpÜLà‹ß|s $àÜ•<äC¹Å,á/Ÿ•Ì?7¥)be`œt&=gFrܨ˜a'ý²L:ƒKîÒÊ:"Q¢Fpòšzuú ¿tÅà×ÛÁÁ}ì¨s¥ã5kVn54jœº¥Õ9yu0Ë ûkè3¸ÕÖkËX6så_†Ò½°fAЮåDÖd<+Þ¬¨®É)Ø1¹ÅåÂ{8ãå0Ÿ,$O& ¯ž7Éb1—µ`òèÝ'.›€LÔ6“*êy‰*,h4>ËKîÉ7â O6„€Xø…ЈŸgâý'mC¹—®‚¨.Ðdè^À”ulƒ ÊÑÍ­½¹YoÉ´èóýgñåHÄœ:‹”H±,º‡¼JŒw Y LÄ¢OË.dØÿÜrdllä‹k Þ8–zGÒØsƒ¸c5ƒASð+ýóçVãý¯¶AKD³ºFíßÙ"†ê?yz%ù{õÓý¬â?}v%žØ9G‘QVA™F}á Iüòùûé “Î@`BqQ!¸­kÔ[¬Bfýæœ9|X.pÆè˜Ín [w·75±ýÍgåbñÐÎ@æ.¬hç‰ËH†–ÍJ±mZK+kg¦$qåøÅ,sóoV€&QMÖ›42òwÿØR]ßø¯¯l¼ru-+“ð»¿o1¢†ìgÍÂIˆ)œš‘¯žqæô³*êy¹ù³ßŠ¡yš) E¾fd$!Ò¾·~ñ²™ÉÈH$!Aû†ê :3#ðñшa4”ˆÆM:ƒ¡1¿çh*cGFP…çšTÁç²Ø}†KïõG€>sÔmÜ*h0”Fevä|&ñѰ²Ÿ=»râèáLà N³²ÈÅ„ôV9EeÌܰ 5È&“UPZi dƒToâèœ9L@îz^n5Á° »íœü}Å'‰€D@" êØ+b–<¦Nse²ÍÉ Êä’WR‘UPºåð„kŒdû±‹+çŒ÷õö¬ªmøÅß>E ñíGîÃÒEãíAè êbBDœvt^Ž à‡WaYUEm!=´¹jÎø_ÍÓ«gó÷䊙”ü¿om?*ú¥§Wøx{ÌÀÅ ‡cr|z7”A©7 tßÔ1$ý4°¢µó‡¸èÙÕstºhZRf~)ûð!4íÂc nVUhddÈkŸÀæ>ˆ=”g‘ˆmnmýOŒ|ã¢C×-™Êvª³S»ö›×¾ „ÙsÌ)¯®G¸¢fE&™5>ɇ;O‡ X³Æ' õSW!¿þ¶œÆçÁSŸ)#õ[¿¦â÷á²vÃ&ñX Y™ÄB.™&¨^âˆa›œÿù_?ññòX:s\ʨhX‘ ÈÔ‚¼ÆF†>Ÿ‰úðð Ò•G“0Ÿ,ìL@FË µêdH’Iõ¼(»Ï¤úL¼LK$!Ž!tP4 ôasX‰èþÑö½Áópîô~Þ¸ÿIvŒc ³ý#“ß÷¨ÃØY)Q‡š76+!Ü\\Ô{¨ˆ/A?ŸÛA„n×!ŸeÞÃ’g?è íùkÔ.ب_Û cc¿ ·›éêÿºÑLÍwòS§ñÐñ²_ýŽ„û¸Ñ¯ý±«Vç^QÕðøÊň6‰è®TT.((Ø{öbTx°E•òíÍ-7þÚ­ê¶ÈEmxÃ52œ[Ý™bõz´–¡> Œ¡4FHêéP:`q²zrgU þZki¹ƒ³Óðmñ‰RžÛYtmß|ì!„M>ÉUïóüÁznŸD6(°9äÛgsSÖ£ÛŒî€%ÄŒíQb—¾zœlPBÔ%¢0j5h“ÉLEsJDaò-R"n¡fxÖ]b6.i\áê¾u–¦')åk?Ü(jù=õHgÕû5­¤ïÚU†G´·×~ò¥xVw@¦$j5Hj(:™4&Ó!ÄÙâdõd‹Ušsò ”µlJRw(‘Ò+™H$ûFÀfXÓà9¥Ã´H{ê¼ÏŠ.5H Ðux”×ìéƒ5R߇V²ï§7lÛÓV×0XÝèçjOžÍzμKs×Ï’mJ$‰€ !`S¬è¶Áµp3hC(÷¨«µï&Êû=ñ°ZâÒ£Fî½°³¿Ÿfù}´Ó®oªß´ýÞ´žt':X¦ÖÖÓ+Ù‰€D@" tl‰¹ u‰Ž2Cô²ŠAÇ®?:Ðt=Gg”d8i–.èGt¿MßÇ:"ÁÕ~ô9ŠËîW´æ’·µúKéôÐ%,Ämä]¦ôÖÜmÙ7‰€D@" l‰‡÷œíÑS€ÎÀ?¢úõwÄCÙæèê:ðP?‘x´Þ‹ç‘s«¾¡æ¶©“º€-¦ug.8´Ý¢çr÷™-NŸì³D@" èWlŒyÍ›)àh«—Ѭ‹ªßùx{Ò'Æ¿ƒî”w:¸ºxL6Ä ‘‡D@" HlŒá¦È{Žq[V[›ö¶Í¬2›Nhnº|•!¸@DV-±ž±øm½ØŒVÿåΖâ›ÖÓ±^ô¤93»­Ò ñòœ˜âä1 Ã{Ñ[YE" HcE ã5ï¶iÑ‘“ Vÿ=®ýÖ­ªÛE/>åèâÜÏêiË.ÁA¾ë0Ôjm­ùû=­nUå&-÷ä[Õ¼ÈÎH$+Aà.W„VÒ§®»/GGOÏv¯3 u iy7‰NŠck­Þ5luwÝÝîÝm½YÖâé‘âçé>*$›vdLrþÿ~v«Ý!ÀÇ Ê^¶·T+³èäÔ k*nÔŸØr ÏÎcbÃX0)fXР¸,‚ qä—o9œz-ï&ñ7üÇ&TÎËÓݰ}«­Mé¹m%\ÆŽ2€\¯=u4íƒcécGFmk²·‰€D@"ÐOØ+jniÙÚcX°¯×““bÃ;‹ÑOõw³ÄGKÏ-9ššõûl•·~ù ·Ý¢Ox2}SÓ'»ÏœLË ö×<0¼ƒü»¿o™3!‰€Ûß3+Û—H$+GÀ–XÒ‹ÚúF¢ÙÖè˜?aFr¬³“íÙE}å’73yä´¤'/çn;z¹¸¼æ[,ôóñ¡·µµûö­/WÔ>0oüùØå¢²êï<ºÈÇÛó+gGH$;FÀfX 6ŒW?Ý_^Sÿ͵óf³KJ¤|ÔcüÖÃó/£fì  Üíí·¶¶Ö7hßüü`e]ãyí¼²êúW>Ù×ÒÚÚÀÊ6%‰€DÀV° V$ì »N–Õ<·zV̰@[Á÷ûÉH/£fìP–þ#FaNÿéžÓÅuCä™7nV}´ódÿ!|ŸY]" HÛ`E¨u² JO¥å®š“³pÈ‚|übæý„°lV" H¬`Eÿ67·l?z Ë_Ì\¬Ó>ï!£òóp>o_ ÜØ¨Ýsæj°Ÿ÷9ØO³ùÐ…þ@¸Ï§L6(H$ý€ °"4GÕµu× ËçLˆ·o[¢Î&˜QϘàëu>mêõúòªšüÒš9†,Ès&Æã† ®AÛk$eE‰€D@" °i¬ñý¥¥õÒõB >ØnÓXßKç;€hô­0C ŒÌôìb칇8Èxfºt½ o¾—y—u%‰€D` °vV„½Kss3»Óý5žvæ—¨GÓÌØA@£o­‹ÂZ®´ªÎO‚ìãYTZÕ·÷h¢ea‰€D@" Dl‚µàØp(S"ñùpÀ´¨o×l#+ja÷Y½Vï§±Š0#ƒø>à¿»¶AÛ·âpä£%‰€D GX5+BgÔÖv gÖÄ8³é€=š’Î ƒ8€˜ôÕra6 é›[½<Ü;{úÉäFÒ¸>Cxˆà&‡)Hì«fE˜wð«[à[í}¿ñÊç@LúÊðEA¸¹¥¹¯˜–-«î3 ÷!Âê–û0íè ÂÝöa“²)‰€D {´óúɰ{XÙ`)«fEà)–m¹`‹–Q´Óg”H´)–Òååä¾"J›}žðöòÔêô}Þ¬lP" øJ´z=/àW“l«fE¬OÆ%Êp¶Q|û¶Û }ˆY 0„?#Ñ·½µÑÖ€ÁˆŠU£á磩©«·Q„e·%6¯/ MAv¾ ¬šÑïŽõi@V(ÈAIEmF~iyuÇzÃÓ1åijnQÔ7Œœ¸Ô5u$Ä-.ÕÅ”ò}œè‡[ ̹»ÚIsæ Sè@UéCKk—ˆ¯dã§®“î[KvdhHeM­$FÖ2²C^:^=^À!3â!7P—!7âN\QÓðþŽS°"q?>:äÉåÓ=Ý]ÿòÑ>øÂ/¿¶‚|–íÿ|w7;ä°þ¾—?=P^Ýð‡æÆE^¿}¼×ÛÏ?0»“æe¶‹ cà¼çôÕ#²¾³nþðaAÛzäÒÉ´ÜŸ=³ìí-Ç$È?:ããœS¯f,˜>Åb™)ô¼t¼z¼€ýѸlÓ°jY‘pîo9Œ »Ï "š?)áÛÌŸ:vxVAù—‡RS¢jt7+ l©°¬ºQ×4>!J™¹-G.±[I¹ìïOR0é“g)­ "È $%ÞiF^©Ôµü›Q¡9— 2óÜþÿЉÁõòìíé™”·÷øi$j½lBV“Hzˆ¯/¯/`«Êâ6ƒ€U³¢CºSPZ¹rvòˆð GMŽöKÍ,Dƒ“’I7Ä‚}͸lÓDÇ}½ð|xöJÞ€õÓ¦ÔÈDÀ ðñ‚ 1@ ­©×)ÔS‚ÜÙ¤¯Y¼ ¬²êЙsù‰@ß"ÀëÆKǫ׷ÍÊÖ¬ ÉŠ ÓQVe0$J¦ÌH#=Š ðíX°3òoÖo_/Q,ÐÏ{VrÜ®W06R*ÊDgt2U`ŸEe5 Z½  ‚Œ’/Aî Ïèðaó¦NúlÇÞœ‚¢ÎÊÈ|‰€D ¯àEãuã¥ãÕë«6e;Vˆ€dE†IAO̹U¥Œi‘~'¯¤²²¶±°´Z‘aˆ¹\<}4´ýg2Ä¥,Ðß§ƒzÒ ¹3TׯZ6<2üå÷?–Ĩ3ˆd¾D OàãEãuã¥ë“e#V‹€dE†© ôáÌî3ež2o”B”B ùãGEax´íèeÒ)ñ…šrxº»-1öhjöFJ¦LXD k#C}½/^/Ì-®pu d5ê´‹‹ówž|44(ðwÿÉ3ÒÆH ŽLKú^+^®?¾õ./¯/]Ÿ4+±Zä4ÃÔ°$'D‡4þã=gÇÆ†Ÿ½š_ZU?+e¤›«ŸÈ,ÓsŠc#‚ÌñMOŠ=y9çfeÕα•t¬kéäø„Èç2!£x™ôY‚lˆréãíõ“çŸÞ°m׆­»œ<³hæ´ cý} l^‰À½ À&|vœí;qº´¢ ÅR"I‰îO[©+YQÇL=ºx2”èüµü‘511zŬqÊ,¢ßÙ6cü¨h%GI ºnÊ››Ž*92Ñ]ƒŒ;¬(62˜­&-HMQ_òMýÔƒ+çO›´iÏAèч[vùûAŒ¼<£ˆÐº…9ΈÊð™ŒÑg`¯©×‚˜II‹—v€³ÅqÉL‰€D@" èoìmg~°¿¹Ë´±#ÔÀDâçëõøMž2vøC 'f”¿³õÄì q§ÓrÇ'D#aÅ}kÓÑ“GQñ|Æ÷¶Ÿd½_0%qùm7EÇR³®æÝ$“c-§Íëe”ö{zåô ? ~ ¯åÝüÍ‹«Éüó‡{ý4žÏ?0{óá‹iÙÅÃÃ/gÔbýÒ©„˜½’ƒísMÍ-ãÌü8«{nUièã’ªAÄ·gwýל]QÛ@¦¾¹­;PD…(8}͆Fh¹?¼³«º^øüƒ³Ä`Mp&sßék{Ï\ywW—‡ï›D<ûÆÙª&]vF"  )ìMV4+%ÎÇË}ljtõ,n=r™ØÏ­ž9{Bü™+ùP"D,çÇ/fãOyFr¬Âõàüñ¢Vuöé•3p*xðl†¾¹Ed¢Aƒ!i¼ÜwŸ¼‚ŒçLz^VAÙóƯ_:…òÎÄÒ¦bVx@Låâìüèâ)03ƒPrÇñt/·GM¶¡jÎNNGGçUüûß·ýíãª7,ÈÕŹ›PLgŠsQy 2e`Kkq®ªkD@• ¿ä»éP* Û7Îy–H$GÀÞX‘‹³Óª9ÉDí ¾½@³ŠX÷eÕÓ’FŒ‰ 'ˆ«ø¥ÛÊ28 ²؇› ù!ÆX°Ôš‘<’ÂI##PØè›:X%çLˆdÑ$–dbÇ^Ì*Œ ˜=>nbbLâˆ0uåšz.¨>itLˆ¿¦¤²Ž•¾¬ºžÖ\-š:Z]ÒÊÓ»ï<2ƒ©ô¾3×þöÉFÝM(œ™à2!1šxsŒœ» Η®‘ÏÄ%›7)d´3”ìçÎ(ó%‰€D _°7 `‹‹ŒDG'ŒQgÃ%ÿX8óÓx[¢Š·‡›¡°±ŠbÉ" h2q îÅ¥³K§trt$Ÿ"!ii¹³ý AUD›”Óê –4"Ç<æ—¨n…gÄZ'.猌 Mҽ㗲¿ÉjÌeZ" HlûÔ ™Oj/ŘÚü.9˜ª¨éŽÅ2fœ ý‘IIÈÆËÃ$Óâ%ûÊ'Z¬8è™ˆÊø3éF7¡èÎxa 8› ÜËF.-#«¨¬¼¦®^«Ó·+æZÝ©,ËH$óSG/O_ŸÈÐq‰ñÞž¦_ò$ûFÀtm³ïÑÊÑIì‚’››öLÏÊÆ=Èßït/ÃþyH$=A9tyUõõ¼;kjÙç‘”·fñ‚èða=iC–µa$+²áÉ“]—€ñä6lÛuøÌù°àÀõ«–M“%’ÈH$÷ˆ×Ô«ûNœþ÷—ßš7u/—Ëm þ=¶,«[3’YóìȾI¾úFí+|’_\²~õ²ùS'å+*ÈÛ‰@÷à×Å‚éSæN™tè̹Ïvì-*-ûΓúx{u¯¶,e«Øÿw(îqýÇŸ®É4ÞV&Í`b|ÛÏu*…ÂäA™e¤DP¢²Êª—^xæ¾S%%”Yµox­x¹^úú3¼h¼n¼tö=^9:û—±!¿¤¢VÌ´›«óâ©cØémqƒX~Iå…Œ‚EÓFc‘aþÉøtß9v†ÿêù•æ·dŽyP>(Îò‹Jø¾9(• xžûÔc|ë]^º§”«€=O»ýËŠ˜=\&>¹bÚšàºýxÎÅ”²K\ 4AQ8ðݬÓw„ž0¹«| ÌC²«ÅÈ¡ºR…Â*áµYiOé~ €ÔÄ‹„9ȸvÄÙ=¡ÚÅX0¯Æ–è‘‹%%ê%yK"ÐWð¢ñºñÒñêõU›²+DÀþeE€Žû锸(øü××6ã{zîÄ„c³ÑÊ:àëõèâÉl5ß|øe^ùìà_”žS¬¾K˜ nášè›áÉÚÏÛó©•Óc†š4B±òêz¼8"Uâ¡Ä [:c,~’ÞÛ~²¸¢_;÷MIÄ14MÙßÑ Döýí§‡gÏ©9È&`?{5Ÿ­èxé|bù4\kÚÈêmÚ{ójl‰Ô™2-ô¼nûOœáÕSœíõß³d˃…À)àâµ("Ø¿²¶2´÷ôµqqßt¡›‹ËÎé¡>s'ÄS’è¤xÇ1¹+Z@ÞCßÖõ:ý³æp—€å5 ?{vÙüI£Ò²‹êµúc—²ëµMÏ?0kêØá¯È)*­Ùë¹oA%d{ìýöÃó'‰öêz­9È7J«ð½rVò+¦×e׉t{…WŒ ¿Dé׳Íœ&m‰ì{¢åè¬ ^·Å³¦ñêñZUÇdgú¡ÅŠ®õÖ-¢!¶ùÞ£ ƒüX_+jê+j‘!4¢@X /b“»qdÏ?0YÅäÑï#·›CdÒ mzã‹#È6_6Mãéž–Uäáîr5÷¦P´eæ—öáüYgS}2ÔsÍü 0Ñs×òÓ²‹ÑKV×iM@Æìòõ"˜Se]cNa…§‡ò<ëD¦¯z…«F²lÂï«e;‰@wà¥ãÕãìNaYÆ4ebˆÿZVU7itLMƒö>Úè3}\, ’Ê:¥ ‰Îï¶ó>°ú¢¯ÁGs}£Þ¼Ä1aç3 R3 QÕ}oÝÂĘ6ÐÏ›–#Bü#CüÕϲ¿tß‚ Îïl?QXVM¤9„mÈð@Ì䮿%EèJºÙ»[¼W Wö÷ù‘#’X3l×çÕã´æNÊ¾Ý CBVï!@ý¡s™om:ÖÖvk\\deM#FÓØýÄG…²âª¬¬m(¯j°xWßܺûÔ"¿òÊJl^ìÝm'N¥ç>8üã˦Ҭ DÈ¢C‰Âv%§“úqv“î'YcW<39®¸\ì%l7xa¢X}¹»ºÄE… Ak4ê:í[óÔÖ7HWæ°457ç?ñRÆuÜ›°ÅœúÆÆêººšúzuç ÚH&&[ÔeºHkõúº†©ê¢.nñêñvQ@Þ²i†„¬¨®Qÿù Äo ölé”Äáaˆ|"Cý‰xïëíü&¿¤Š®XO£\ûçÖ?}z©ù]¦uXæRL[à7$¢BÌ‹ÍL‰ÛzäÒÿÿÏ„v>–õÜ_³íèå¿lØÇ¦´ £¢yŠMb:ë|?Ìã°°ÞwúÚoßÜš42œKÌÂÌAF³†ú y|J…h˜7¾³~ÚG~£VG¨&ûKŸŒâÖ­[;Ûrà0¯¶Ò ÊŽGW. øjélKkëÙËW¨Hx_A¬k=Ç«~–•_@~úõgFĈŽ]ËÍûÛ»HÿùW?éE ® [wL½ ±ÊÑs©ŸìØãêììåÙ¡bóôpœãÍ¿À‹1”²…º„º¿ým,”c£"Q¹7·´dß(â)y…Ť—…\B>HÃ<(ÖÙã(6qìhŠUV×ÖWtóprRã-R"ʤ$&ÄÅDÕ64ì8|LTQŸ·8òÉöÝP"ˆ‹ŸFéa$Ê0Ò?¾õž =èñK=—f0¢GnañŸß~Ÿ» <ÐÏ·®¡ñ“í{¶<*î¾þÑgP"l³¦¦$!g,°ºö®M»·[’ÿKì)+²‡Y”c YnVTбûû‰FB{ŽTA—´bþl.aînn˜û‰uð§~¾QPÎÂgÖ¬ú—¯=ù“ßÿ‰2H‰°h†ÐP˜KHÕ¤¤ÑlõúõŸ_­©«?~þr”ÄØÜ…ÁÄòŠJ0wƒMSg£À¸QqÐ/¾éÒµë#"# %eäOMÇÙâ[zlÕÒß½ú†6oÊDu4_;'gJòØ'Xáîê†Òpó¾Ch¯f玉‹ÅB˜ž~p¢¦3—Ó‘)-|¾{?AgÈ–>ݱ±Óž£'—ΙÑШe€”äÑãG‚·ýõ \ÅeåmJ›2!°]$+êÍÜáã_aLÝu}¥¤’躼¼« Ð}ÄDIÖ¹æ–6óK]iD&ì¶¶ŽæÎ·-i™lÙX8LH°"„Fü!¡9uñ2ëúùô«”a¥WJ*‰œƒ¡oj‚9‘Fb„YÏÅk™°¢¤Qq0A€n— ŒhjjÎ+2²"cÅäQñTéâq®..“ÆŽ>fô ðÀ¢ùÂ( ^lT„ÒóüiæÄ”.mܽŸ„R ·°HŒbþ´Éb?NÏA3£kFVd$p£b‡Oo`]3&$9{æ'ZÈ+4 680à\š!6cë>cG PCzôÚGŸ1¾øÜÚû))*ʳDÀ.¬¨ÇÓÊþÿï­mx¸ÆárוoVÖýùýD¥M‰ìf•®:w{2{¿8˜úÒSKB|†Pr¤ÃB‚Šƒ"…啃Q ¢ªúå>–FPg_ R%‹VUrMÍ-ȓԄƒŸqF«ÊÞ,«ª­CµD;7Nß„i–œª3²¢®AÝ(¾‰#¢4ãî¹i)IêgYL?´t!d'P(¥·$,–Dõ®‡ ì„.^Í„mÐδÎÕgJ°ˆZ1Φ=Ô*BE™E,wL¼)Œ´¬¬Òðåàkø…€ ÞIRÚ)¼ÙÇËÓAñU¶nÅ’±ñ±Jc]_Î¥°‚JË̺’‹L eäitìð9wkñÔeZ"`Ó˜®ý6=˜Î:O(uü(²`c&¹lFÒÂ)‰ú`ªôŸ<µ„·ÿýÁ–Òu‹&ÿáÝ]cF »q³ªN«Ÿ6vÄC 'âšÈ<³ Ô°Sƒ6ÿ¹åZ›'–M;òŽÜ›¥ý“½ç®æ”+"ØEÑÖœËè¢Jgݶ­|cŸì=[TføÙ:22ø¹Õ³p9ý·àî¹Z^q%±á›]¼^€Â WN¹E¡¾ß\; …oÌÝ'¯¬·‡ÛýóÆOLŒîÈpP—ûËŸ¤.±~¿±v.ñÑl CÙÛÞ!€êý/·#ªy{ã—¯^Žw"ä4OŸC£n—ó¦LÂÔî~ú’…ÂBBIöäó-5,”Âß° ›¤°à n%ÅÇ•UžÅ"i «uMM$„®m\b•‘ÉGÛvÍš8~Éì“àx㟣G/.«ê9 Ek˜8u½Ïb;½_»FXOsW‘èâq¢”h°"ÑÕîŠD-l’Ö-_Œ¡¸äLî›1uϱSì;»t-É !ÄÂ=V-˜‹÷&Œ²ó×W#BCŠJËÅ`E k—.üë»ÐÊ•m¨\A°0{ƒstø0Äñ÷oÿúCÆ8u©…GQWž%ö‡€ýÛ¥n?v™È¬P¼NÇE#â&œÂ!},¨mÿÙk¦ð`? ¦<³rFJ|Α…%µÅÌÂÒêqqO,Ÿ¶jv2áH´†U_õZýÉ˹s'Æ# ™–4‚ÌèaõUDE[?Ÿ½’¿yxáD‚œLcøÆD2çîæ‚ÇK¾AQVAùê9)eÕu€¾dú˜É# …B1¾q&NزSiy_H˜óÈ¢Iäw˜E Ëjð5ûð}ï›:úAc¸zÕ¼Ø:¼²ÿ]#€0ãGÏ>±jÁ>NÈ3à$ðô³o<‹hD©‹~-<$~³õÀŠ=¾z·jêê”À§ΘÂ%{Á¯±þæã³û UÔÅk†j>ÞÞß{ú1¼f‹ÖÐñÒ0Ît‰ ‰HîâqÜåÀ¶Z1šš$ãfy…¯Fƒ!ž~”ÂØ¿òÛ_(—$pÛ¨¾$ÝõãDavuA¹½u¬íg/>kÒ2—/½ð´:“¾­Y²“£’òòææ–È°P“ 'è¿ØZ18~º‘-Q—J ìJ£@iE%ÖhÅF6q—Z½Ê+«qOn¡2Ÿ‚›LØ%vΊøñ„Ô‡qÅõ‚Rô2Ï?0†ä¯a:÷ÉX6Øqó×Yùîäwö8(®€„w€¾½ÀÕÔÍLºÇè9–É-.}Á¥ÌóÅ-lžºiöd±™)°!ìœ!‡À„s¢W72È~§EÆô„ø~-ùx¹ÃiHÀ„Ðã ×!ÍÊÍ™´ÅÌ+9ÅÐ,x¶ÃìxztÉdÎWsKÎ_»±dÚ4k‡Îg®æÝô×xŠ-Q'/ç˜WáötÌ›4êú²vaP ŒY•²"v„¡RT˜PTX´†ÌÒÊ: ¼(²óDúÿýÇøÐüI£ÆÆ†o=rɱc³Á“òA.¸Y3,¨4µ 1Z9'Yt@ž%Ö‰Ó»ž¤oH¡&÷D}fѽ’Ø vΊ˜§y0ñAŸäç-LÈ ðõúÃ÷×*³ˆ‡åÓþÄ-óLvW­ž›Â]¶§±Šc@ ð€—žZе¿¯—ÐÙq—òæUDûvsŽ ñÿÕ ++kÙ¦Žä:ò(þ”aþË‹EÇ¿ÿÞC"Í–@L»Pe2ABd±Ÿ?Û!ðvsùü ZÃh¬Qß$·ž)€Ë„Õ"€ØÆÚs¦LP¾—¬¶·²c¡ƒ€ý³"æ/81CÂɼAxÆÝæùätVÅba›ËDhÀõâÐxºóg^±3Ä:™|I‰Ìa”9VˆÀäqcø³ÂŽÉ.I†8wì­ƒ@FluéOúµñþìxŸ·-‘èsHeƒ‰€DÀ6°jV$ô)œ n¥ÕßÙoÐöu/ALú¤y¥5.NŽZ£o¡>iÙFÁ¡€§›A€*±ÑQÈnK$‰@ï°jV¤ ÉÃÕYltWr†`À¡ÿîæâ(A®kÐy{6fËC" H† ÖΊøÉÎáçí^Û gÅ‚3$†ÌØA }ˆƒh³—›smãP· Á·¾ûdÙ”D@" ØVÍŠÄ‚ mT¶é¹%6it’±ƒ8€†8úä) ÂÎÎÎAÞ®d|!ÅEõ!Â}2M²‰€D@" ¬š>[Y°==\½]ŸÏA‹ëy £fì  ¡ö±{ïÓ*þ5®Cä#®Gj¼==úá{Ÿ#Ù‚D@"  VÍŠÄ‚íâââæê–¦!–=QÆ«z £fì  Ñ‡ÄH°«‹k¸Æq(ƒ\YÛ89!¢o¶ª’ìŒD@" t€U³"È0jèááNä²/§­G/uDSïzXvt—ñ2jÆà`2}2D5Â4îçåæëÒ64AÞvôÒð`oÜQö-Â}2M²‰€D@" ¬Œ™­õÈEÜÜ\===¼½½FGøx:·¿½ùØÐ!FŒ”ñ2jÆà`¢þpæö^©öòòŒòqrsh‚ kܧ%FZD€Ù쳩ž™–H${@À*X‘|pvwq!4«WãšíðÑxûùøŒ vook~mã!¢bÙ·£cŒ„oc¼Œš±ƒ8¸¹¹™°¢F]¸)ªÑSÒÊ]°*5Èj„}4Ìj"=[ÛZš†ÈÎmÓãCü}5F@FNGB ©+‰€D@"`OXWÄžÒê5¾¾êÙËËÃ××G«ÓéõM£ÚZs«š7¾x,õúÜI£’bÃ}5žê*¶žf>;ÎŽœÏ¬¬Ó¢ÏŠ t ò÷õóóp [àšºÆ°ÿîVU]ר”7G¸¥¥5Ú¡ñ¦®ÕAÖ¾óI{mçs9ùv~D r§ãøá! Ü Â`El»ÿ 4™H$»DÀZX?Á9ü¼½®Ý(6DWõñRàÆúÕÃÃIIs³kkÛ­ö[N޵•õºŠ†ú/¦n:˜êçíá§ñô²HKiÄ&¸–Æ"NƒPÖx:µÅxÝ òñ ð  ðp õXÀªº^;**B¨¾e’8{{¸–Ô©A6G˜ºNNÚz½®¦¾Õ†@ö/+¿q/¼œq|õƒŒÁ.Õ {»´'ºD†ú‡v†0U ×5øžyƒöÓÞÞÞ Õ¦ed••×Ö7huzrì{È£3Nµ£ƒ£—§‡¿¯OdXhrb¼·ç ýºÑ£OÛ픉c3©˜¸qƒ:q=ê¾,l ܵÄV‡Ä‚Ð":4øôµ¬‹™7æO­t†|Œi°ªikC­t‹|'GƒÉ§Q«knªovhÒ5WjëËDýÆ6¾Ì˜ 9;¶‡¸¶û¸9x†ìãëã%  ð÷óp0+5¸‘/`TpS'„Ñ—ùx;–TªAî aW­¦¥¥©¥¹±Õ©E×T¡­»…u#ìÒÚ:ÅÍÝ»¹)¤¨(|ïÞ£TÑ7o/rNŽí.ŽaîÎȆ¾a`4‚ìÙÂj´í ÍÌ‘WT¼yß¡kÙy·ÚÛý4þÆß·×;e—C0~Î9•WÔ^ÏËÛUÛÀ –”÷àâù1á]ÖìË›r"z†æío'þ/«¨ÍÌË«º=qk–,ˆÖ³Ödé!‰€U°"g½aÁFƒ¤ñÞs2mÎÄQ¸åQfÄpËÃ/rXÂÙœÎe½'?bu>Í-­­­·øÚ6J›KÜ&.ŽHn`?>cåƒâ ”«"† êqAÁ ÄÀ[´ ¾k’ƒÛ>®& wpss‹Í ìraÖäÙ‡Ž;¶;LÍÎl, Sp¸0ŸŸî"L]@Þ{2="8«®®Vdë ~{蛚>Þ¶ûDêåÐ@ßÇ–M?*F-¾µõö¢ÿÈ !ÇûO_ùWþ>wÊ„ÇW¯à[¨íô¨Êí‰Øu"5MND S +÷ï/¿5wÊÄÇW/€‰Sž.¶ˆ€U°"#Ñ1¸áqwwOˆ=‘‘{ø\ÆÂ©w~è³±’±?H,íXc«iðnlÔbiÔÒÚb#Ù01º½`¨!~ƒ @H†P™aé™49 `²*ƒREMÃÌÄXp£"@Òâ§P°««k¸WFy­d»AøV ^£.öÌâÔÓί»¿Iã &½C˜Š T^S¿fÞŒ®¶»Íeò ñ*UÕÔ¾ññçÅe->or¢ú÷‰Í¨¯: )D€Í¯5>÷)ºYöݧãÝì«öMÚQY]óæ'_—•ˉ0Á§û—¦Wjœ8ï;ÝoJ–"X+bÅ”ˆÕ=$À/ÔÇë³½§GDÇF†(Óp{Ù6”D”≥‘Æ`~­ojniF·Æ·9_%”g¥¢õ' ý3¹Á €0?Ì«9/£Å”á䕃X¸q˜—Q ‹ÆE1Îþ/¿Ú)c¤ž6pÛªÅÕ5ÞY¹®MM)=ÿ8ëÂT1‚|fxhPDhp×+PÛn‚±kCcãë6V×ÕýäéåêÐvÇÕ‡=‡ òk¯¦—?Ù÷¿ïüÒ Ï ÊïÃöESÊDÀM±BNÄ=‚¬LÜ+Ÿì{Ù8qRbtÚqõ¾¥»¦píwÛ§¹Ž›——{ÃGG…Ë.øß {¾·~‰úë@,ÛF±‡AªäííÙ#â‹lƒÉ_&¶ÍŠ0 ¨‰«‹‹»;1 reAÄj >.Î`bàf¾cßd"Ô×ÕÕEù{çT´â@A@IDAT5˜€l?ÿëíÿö_ŽÕ5ž…Å#ŽŸ½õÄZÐ`tÝG˜ò?Þ£ñp›1.±S„Û Íš@m‹—b%ÖétŸlß]\^ñÒÓr%îtùRúþû½mÙñôC«ûö '¢SÜïù÷1q[w>½fÕ=·'°O‡a¢QVY¥F”5ÍŽ'z1Œi|}G‡]-®øã»;Y<ÕD†+¢$\Á 1òôR"ðNdË»cønå`t@ÄÈx6ȇ8Ô@Áþá#æqwv V n`BuI“4w„q~„-²¹¥°^o²= Ôú‹4þâÿ:´µ¹ì=ä91ÙmîtæW" n È^n®ÓÇÄrg£Á 4Ú/ù]ÁGâznÞ¹+™èkÔ¿Flq8ýÝgðyxÑÔwŸš;uRltd>NND‚iÞ”2q ¦O–Æ×æøÈVäç£ÉÌÍWOkÒ,h|}}µZÖý¨ÖÖÜò¾w°p\<#Imï©,op#A†>¤$Ô[Z¡>bhâlÒma6ˆå/f.~nα!þØaû±d³=ÍôÀФŠúÒáæææ‡öâ½9Èö€ðÔ‰.ßz¶öå‚î/oøÄ w@º „¹«9ØÛcüȘÐà .®©«O£ÆÙÓü¨àÃÐØØ¸ûØéÐ_~‡Øâ(¸Ï tàìÕ/÷üÁ3wýêu¿cr"ºU¯KŠ‰Û´çà÷ŸYßëFdE;F`pXQdhÈΚZVüI(àBq°¡aji1XOÃoPWÖÖ—74nØyò£'±› ðñÂòø.á‰RßN(±)Ç) 6÷rqáëäçdX³ÍýYÃa(B”“S½®©º¾ÞAvò?*123á¹ùÆsü±Çõš;Ÿ75D& {»9 ˆ áèa>À•5µ|˜ÕMÙb4¿CÊ+*óJJ×/›.Í«»3‰ ´hÚØwª«o`—hwª|e9_ ѽP&®Q§DT÷>ÙB?!08¬·ZXx¤^ÍX0}Š20E˜a42ú%2*Ë|uú¦º¦–&½®L«½e°ºÛ(IiÂî‚ÿ99:¸89†â_Ç3s ª 5VkX‘¿¿wE˜ÎvumÔ477µ´4¶´5ë´d#Ì6hÎȱ—•‡×Tyhµã6nütæ‚Ö»Ý`v0lGÁï´éYÙüA(kÓÃÈ΃¿Ö.^ËDvïâ"96wbâ.]»>sbÊ€=T>ÈVVCÇÚÞã§çN™„ ¶/ˆ=„Œo!Û¨¯¯ç+Û§¹™R|qpWPjÙwB­ÌBM†Ù/VA(ÎQ ù“±®-ŠÔøt0:£_"{CøÒ¢Å~;¶yiCëjÖ^9fÞ´hjLz‡0m|€ùÛúÏM~„0õ¼b7+ªÅq¿DêÆW¦Á*Ð×»¨´ ïõÉDð YRQ‹\9Ø_äÇø¶Ôê›x¸»Šéššùõ¾  ¬ªCºð`Ó¨AldiniU¨Û'DñhÚÄ%&¡®Eãl™iimSú#2ûõl˜8? îúõ)²qE`pX`­Y¼·Z‡Îœ»oÆT;Ö'˜–­$X¿ Àš‹$üB³6¤ˆ‘²`ƒ¶Ò $C0!$œI ÝÅ »N M„¯­yhü§;·´„ܘxíjþì9 J½F˜.;¾¹Þ°»Í¦±³û¬¾QëïÛ_xl¢.:ÏúŠ" y=»(Ö[÷>¥UuonÀ¶¾…—ˆŠ7‹÷ Ï_ÃüL?Wüâ/.¯Á!Fx<ÊŒu‘_ünx 5z|¦Yƒ|ñ‚£#·< · "á.¤DšÃ+œ©bñ¡}’©t@IÜc³„_ÄZ&Õ»yi˜8­ØMÔÍ겘#0h¬LׯZ†ðŸZx‰5'FBƒfôKäÝÔ„Ñ‹Á3ß>BVDu>Ðv<1 MY³‚//¤hüÈ .{-´§eQóPA8:óçߨŸFG·O™Ú;„¡D|h‡G†ó¶õO oïoĈ¸ËÝÎé´œwžÀ¥<…a=+çŒ_=wBaiÕþ¹ ëcQò3óoþùƒ]-œÌÒµíèÅy“ŸX1“üíG/î<~ù/?}Ò]µ˜¡Áùû¦C¬Ö`•]·xêÜI=Ø—SXv*-gÕœñ¾š¯ݪˆ%”½—×~ËÀ,A’—´×Mõb"Lž€yÅÓÆ\»È`¬zãfå™ôÜ'–Ïœ<6V”ž]+JË*ä.9ô8§°<>=Ï<~ðø’²êz¢o³—UÝrÒȈaÁþS“Fþý‹Cg®ä—W‡ø¼ñùÁ´ë…ŽNޱ!?|béßÝN•]'ÒЂ͞`rW0'¥MÜ'N3‚KèïÛ›\É)VnÑá76¬¨©÷twÃ[æó'fä—üSÆÆ¦fÜÀÅÝŠÙ)Ëf&óÍÿþ¶ãÇ/e±ŒùâCóeœÒÔW&˜¸¶vP¿§YûʧȶˆÀ`²"^ï<ùè+|òÇ¿¿ûÈòÅó§NVÿ`Ék?$ ß;⫇W‚ñî]ŸY¹91âkW"³w *µ†Â_{¾±¶¦yÛV$F.ÿûWÍüÞuÒ$Ð`vaÃoâ3çA‰øèÚ‡{\±óruçµBDôîÖ£þ>ÞëOÓx¹Ãr¶N œƒ…MZvѺ%†Ï×í¥wĉ‹Y\¹I¸ –dý»~î¿õÅ!ÄhRXÈœ¹úÁŽ!¾h(àµ:´T&$µäR‡…SF«YlLcXláî^„X悞N4䯴§704i°—=šóöK*jÈLŠ»ã<‰tAiÕÍÊZœô@}˜¥3“Ó² Å%…§'Ǹ”õÓÿù8fXุ(♘0õS†GÊʫë˪ê¯å–<µj6€¿üñ^&Ùœ˜Ö&‘ž]lr®¬n‡ß¶—¯ ÔÖüêוMMÚ½{Øp÷o¿ýÛ+ÉÉÝAÛvœí;qº´¢ ÅR"û Dâëqw6vKÍDœ°nÉT±U Ó_¿ò9¤VdHœLã‡~°¿ 0a1Hl±YÁï蟰 ZCª„„`fJüúe†…mÔðaزÔ5êH(בTì‚Ñ’°("00—@ž>ÙsŠÂÿùÎöÿóâƒÿñæ—) ѹÅH#4˜· ú£>ƒ€‰à!§¨üÕO÷-œ:öÈ…ŒŸ>³Ò"™»«ƒØÚp¨3{š-t"ÌÛì)Ê-øixçÉcGì=•§É/®xÄ(Ï#óÙûçÌ8êì•Ü«¹%ˆôަfþî{ëÔ?M•¦H ¨çÌœ®½‘ñ¿xœòêºÙãH`©Oâ“`r—[êÕ*š8rR­ž7adTèéô\Q`ýò©ù'/g§gBR+kD>òH&™"Ÿ ÉáL ŠOQ ‘N¥g©Ûï~áÓ=ÎZ÷Ÿ%Kڃ̊@ŠÕå©WΟ6 ·ZУ·ì ò÷ƒa8Ã5yHúÇ1&ge‡æå´ëtßýöÉuO6wîp¨Ý¢Â/_ÄIñqßxl­­Û™à)ÖcãúðÕëzie-«o¢QC;!¾,“H#HŒWN¦±D%ÇG!·À+½ò Ò,„—®(9Jâ¦ÑÔ2$rü4^Ϭ6ØÂ³æm>t&aÚv$• ðG7—°/ž–ÄŠŽ!0v-06ÖTZn±å¹Jã_Áý§¤V3Râ|¼-+u;ÆÕÕ4³ç×=šóæÅ&²ôœ"tXâ.Ä…oÐaÆh ¹ûDqm¹%X Z=t¬]:̃g¯nØuêrVÁ„Äáæ“ƒ“ xðÑ ™ïo?>g¨¥3ǽöé~“Â]ߥ0þŸ¯?À§ÈÜpûõÏ BG6kB¶#•–ÝÖK‚Þ‘ [rrt ò÷A‚¸lVrò-¥ˉ¯þ¼[®'síÁgE_Vßpâ±§uX#K À{5®ñKdë›ðïaôŒüˆW+ŒP½ évƒ£ ¿eÄÒ+ž:ulìFÃ6¥;[œÄ-#a.Ÿ@@þC÷MFõƒzëõ®å£>&S-9Ð5µ F!3<ØÏÅxéÑž4¥v¸lÞ9&‡¹àVD™Ç–Në‘y“I³Ä&úŸ› ɃêÁMÑK ®˜aAØaƒ!Iº‡E3»ÌР­˜=ôÎ^É#3ÀçŽ>K áìÕ<§Gf~ɵ¼˜o²=g'X@¯0ÈÈŒSŽGßÜbá®hèö™ྷ¯îúŸº 1aó§ŒÆlœIßFF†0$ˆÈ#‘¡x½«y!¸7¬…‰Q°êLŸÐ¡Ñ¸·qÉÚ®h{dMñ7¿Þœ‘á¡m\²ssÄëoºFÞ1È誦}ÝC>Á€„”¢;#C È…‹ =˜áu}œ‘FP}ò˜X‚` Äa¡bï·ºÁ‡MùÍ«ŸŸLËVg’>u®dMKÉ%žw·›3iŠ3.…9‘Ð µÝ‚Ðj›H YªCy®ÅT;’ ýîê¿yE%ÇÐQ#†JN/=‹xöþ¹Ø/#-ãX^cð®”D\´ãØ¥©I±"ù­u ±ÔA‡v,zXàËg@m•ò"Ý P…`aLFéJ·ÿ÷‡£cÃé£Aƒ Œ‹‹„‡A¼Ìïš4ØÅ%ÖÛ\|éO ý,ú¾ å59î›6¶¼¦á‹ýçØ ÉçGȺLÊtóRÀÞͲØAÀºXÑ]sÐpöñ‰xùÕ¢¾Ö’ŸßZRL"âÕ×Ýb;ŒAïžÕv€ßñÏ]%8 FOwTft•­à¢ÃSÆŽ@Bs5·˜°!&CÀ¨…‹Z-" Úi±Êz¸»aó»ïôhJÒÈÈF]b–dôAûN§#Ø;2"« S·{ìŸbW?mÞ¶Ä3è‰Ì[Pïx0<¨íMFaå—þåÉe¨Æ ªÁ~ˆx:Ü$Šn?¸`ê!$çQB!@RßeÃê‘ÆXû¿~´¾A§W‹|¾·~ ›„× æw•F”ýJ >-âÃfÆ…SÆ ˆB¾õâí¯ýê9‘D³ÆŸH?¹bæ£K¦Õ6h%"|º]Vþ/èîÚ„ÙíÉ&$6‚€s@`Ä«o¸ÆÄÐß¶ò²¢¯?ßDÄ4yt‰Œák¢ð‚p°YŒí]l¹W¬‚¢‡aXÍ*Å^$ófXÒÌ—^TrßZw¼Øµ¼»õÿ±÷ðQ××Û‹¶jÕ{Cˆ¢‰ÞqlÆà†í¸Ç5Ž“¿8ÅiþœÄ5®qb;pÁƒM¯„h!j ÞËö¾ßyñ¼HBZI»Òj5ï·,³ïÍÜ™9³ÒÝ{çÞLP"lxp¡ÅYH›\‘(âŸ/«ïù™’î»o+yØÑ̤—¢—·@H„“Æéñ¾„â¡úÒû`l  Îèu{¶j¨bz4AÄPè¶>òÖÝ·|îâŒÑ¨‰¬®H“Çž(Ä#÷‘­%1& ã4š¯‹:øæ ý-$ xc·=Ò ^#ÐÍÉŽ^Ë¥ )ƒxì†<þ¤()¹ö/âØíæÇËï¾3êµ7h(#¯¬&â!Ì#öQà<<>âÞås:ðö¤£¿½ÿÝð„¨WÎó¤ò¯>±q{Òôâä ‚.âŒ!½øá÷È‹Tq…e5¯}² È& _¿åP^q â=ÞsãlwÜþôïo AýãC+ õyþß_ã¨àã·.Þ¼ï$B' ¤5Bc#ÉÝæý'³Î½þwâýÓ#dä<÷Ö—H!üùŽ£HT÷Þ¦½9ù¥ˆ…ýùö¬åó'Ait4·ñ†¶ ƒ‰ |Òþïµ8ˆ/Ý<ôè£Í™§/”IűÐ)d#v-S¼‹ÕyO*-P,»Ñ®yê`LÆ^^^qÏZýöm0±6<¤k2:ùù‡W¬¹&#ÿRõ7{N°#UrO$Bî»kØš( ˆ3Q<›ðDÁ–ï^¡Siî†Nq5$a‘ Ö`¶|¿ÿdh°‰Þt­)xÁSAqfŒ‚‚ !|h92‹A%÷wˆBT²=“²aë›t™}âÜE’~î©;¯EØqÄ”BdˆEŽa_¬^±m0Zä wÁfÐÑœôáX)¤®Cð*DìÌ>[²töøãS‘N¸¼¦‘¨‚”ü®e3ø ÙKð•ØzðtþŪå &c¡€Ü^<ùè>GZ¦¨®ˆ~( »þãê_[ü¿CM„¥Æ}Ä(Ÿ7y$,¤X—š†$£ÃðÚ p#P(è–ÐdX“Øü êã¾XY¦%‚¯´¦!1*ôTþ%P1ŒÕš[c—9øH/Š€ ¬ÈGÀR±€O&‹|é•Ðß>Ç3¿ñm/–ßugˆÏaný>d¢øÓÃ+î[>3”m>øåΣØÿ !@ ÆÓå&3£H€k Úïî¿qaÆh䘟WE…ýüÅÊŒ1ÉîÑO](MŽ oÍð•þûn‚ZâjÒú}º~Ñ¡T,Ä8Næ_ ‚M.“Ëc„FURYWPZsó¢)¨*ù·ÿn?<îWk¯WÈ$È &´vÙL¼€À¢ð¾53,G)“6¶èóÆ;þóU  ÃƒX΂$ÌϲàÍŸ W¡ãç."S,(ÎŪŸ\¤±îÈ.¢'TÎ:S ¥à_þóíά³ø&\¬lHj%O`QJ…—åÖ:Ðâ/w1áU©«5@ —O 4ŸÂK…ª›o‘¦O„I b³ÕÿóïÆC™¡¿yNÓë¯9`¾p© ¶¸Aô¯Ïv>]´bÁ$¸ë"/, ¯(à" ˆî73ƤÀ'w÷Ñs«}ê˜÷ñâ$ȹÃãqá’ÒQš{ý¡Vƒ,u‡Oâ()É2®QÌE£#Zét6V»)çàý“~Ëb†*¹_! +Z0e$ •äª&¼ .º{Ù,hzdR&-ì˜(sáPÍá\?‹I.K¼…`EÉ[@ƒ ŽºaNzdˆ*+·/°Þ›L†vÚ#Bz@žPMÀÆvÎ…çùAÔ`'%ü OéEð”ùX*6 %§Ä¬û¤áµW´ŸoÄÄŒ™ËV­~ðaõwr»KÑP@ôa2f‹k°GÂÂVs ‘”iYaÇ)­n€3o›2€ÙUÛ®‘IQP`@QΦ¾|›ùì°¸ÝÙy™§ °¡~¹3û±5‹:—æÞf(•‘,å—w^?wîž^Ððb‘@Zß<¹ «¤œC[6w=iõì½ËP_$ U ¹3:%–­ó˵ױÂaßÄ‹|„Ñ ŽöÐöÁ‡ìÞT׬—ID„ׂK±Í¡d‹4o>b €‡ÁôÆÊ¤Š€ï ¬ÈwØRÉ…O,{æ7AÓjÿòggs“Ëlnü×kú-›Cû;¸fÔT}3™±©q+L‚Öç ; “×Y:{º‚'õW»²ÿúßï`C™::99& ,Ç}ð+š2*iwö9(Üï£<+=–šõ[2QÆ>:*…q‚i'­]“!ø˜t;kx¾#±]·ÕØ à(°Ž±=/{ÔDzMU×mQ­wu-–>¥\ ÊŠ®† ½OèÙ¼ùñ'5¾ñºvÓWxŒj•÷ýLqÓò'ŸB>µNÐ[nŒ\0…°»#’Â>qÛ5ˆUÃçñˆÕ gÈñrkÇY}M^ì—žº”•~d%ΊC Q9t”ƶ¢ŠE€"Ð-ÔÛº[ˆhŠÀð•ʰç~óáG¢Ôáäާ•Þ´¬éýÿ8 †+ªÒ!—–±Ïá³Â:±7=)@i_Ö»ˆ4éµ4Oz¤u(F€²¢^\:5" 7>öãOCžzšÛÁÈ©Ó5¾ýÖ¥–4ý柳‰wG/ŠE€"@tPV4è–ŒØ_€Ÿµúεñ_}#¿~ §ÕÔÙÒÒøÆ¿.-[Ò¼~ÓܧT_þ2I:ŠE€"0” ¬h(­6«DDDüõ…¸/¾’-¾†ˆ‡/6N«•.»¾á­7lUU>蓊¤P(Ÿ @Y‘O`¥B‡H(ùâ?b7~)[°ÌÝÑÔÔüÁû¥7.­zêIãáC8`åï˜øýû ‚" 3„°…> ¤Í)ÀC€²¢À[S:£C@+–*1BämxtõB$=”SˆžG&"éºÎÀ&@†,„š6&yã¶,±Hpã܉Á ’^UÖ5ƒB±¢«ë›?Þzh\jìĉàX'Î_ŒÔt”ŒÝJ d~XµhÊ·{Ž£XÑœôáèôšŒ1 §ŽÚ¸= äætAÙ²Ù[tÝæƒ3Ó‡§§% Î5ØÒœ‰i—.ç(e yÅ•›÷„žI$üﻃȺlÎ$$q:cRcçMÁÒo eÕ ßî=‰`9‚AJ™y3üv´>X•Θ±ªIgDä*ä}#GõEÅ(+ «Lç8ð4Õ-«ñ²×Õ0f1egã´™Ëb±œÍÅ‹(4I‚èaL´ ,‚§Püô3™ÉË.àݰwËjuêuª ¼`¶³ƒ UT¸ºŒ™i’±ãÄãÆ &L°%%×Õ×ËʸÇrØÞ»- aøÍoェ‰XV…D$TÉ¥F³Š¢ó«ž¼ýšª-ØÌÍ §€— ‹XˆÔ'?>W\.®>)ÖAž~~ 㢞‚ˆ×J®kÒ¡ùëæC+€\¡¶F-ê›aõãpƧūAÈ'ò›{—A9ô¯Ï¶#×,h¶É3…e'°Ã|ÖÚE(Ø ¨üú§Û‘뢮™Óª]–¥¼¦ù–ϟÝêøóe³ÛA7ž*@¾ÕçŽÅæ#óçaûnlZ½élIUæ©Â¿½¿yÖ„T()‘UÆwÝQÉŒeE¼¸tjþˆ€ ,L¹b%^0^Y/䛲²Ì§s,çÎÙ«¯8ÃM’­¸¯Nç0¹õnõæM>íä&— #dÜ8ñØqàCÂÄD°T³ÙlöVSZ'Mº¼u*¿†&P"Ô‚ ²F%Ç  ŠzE¬]Høzàd>nŽHŠ"ÕÓ^åâ´æNbLØÆíG3Æ&£ âËr…v*ù‹Ga}%BM(HF$E£½xO\„;" 2ċ˅ƺdLÃ#ÔÁØD­”!-Æ)ÌÖ²šÆñÃãÀŸP *(ä_#ÙÝÓG$âŽ?_Z½ñí/vƒÃÝ8gü´±I@ÕŸGÛ?c)œ>6yêèÄ#gJ¶džÙ…³­¶FB{ìPV4ØWް"€-~Ùx‘ 8ZZ,ùçAlKl••öÊ (~8¬Óç³äñ‘°Á bã„1±ÂbE ‰5OR–’Æýÿ&m±Zßþ|W]³î¡•sâ#ûdfíÿñûºG|gŽO‰‹þß÷‡ßÚ¸ó—k¯c}í}Ý5•0PV0KI'2¸@rÙ ©x±Ó€´½¶GØàͼZ-eØQ!+ç Þ3Æ`Œkr¼ä|¼4!‚¨(Ýf…ø¨°xÚpŽ÷¿ÙùcRb–ÏŸD: ×(,Vû¢ŒÑ„[”TÖ%F…,Ys&¥µÏ„´øÝÙç^^ÿò¢?¼jœ²;J‚¿6!= VŠw J«á1VøˆŽðÖϸZ¿´îh³P‚,´b Ð]?sܾãçÿøî×èúžgÁ´vEz/è·.,[ô¾(‘ÝnG$‚²š¦‡o¦”èªH‚,Þ³lú»_íVk—Î$jѫ֦(W"ÀÅOÚ•wè'ŠEÀßxcý ññµ·öq ° étºÚÚÚ²²²½'ÎÈ•²gîYê¡L¸àÔ5j½ÍêÊö‚“SV›‡Ý½<— EZаÒH, êÐŽÜkÂ;§Eo QýÔµûSOÊÿß©8è®åK …°qÜb籜بÐG×,r¡DE¥Õ¯mØÃ4"îOi¹#™9EßíÏyæîëcÂÙ¯[ š$›ƒÿø]·öbÕ¼õÓdžü jö«å ƒ¡ô+d·À;.±@ïÏ»‡µ"2TÝ÷³Ðð‚sÀví¹dË:å=}ªîôÛ 0¯€9¹wíþÔ“r³Ö oMÜ!žtäp8ÌfË–ƒ§AáKäI“!^(…ªd8è†8tú=B€²¢ÁE+SÄ;„~¯€¡·'¬p \eÒ{[p{y`µÚê[ +êgMÖØ¾ýÜ 4+=5ÿR ˜+ìðœÞ tŽýéêz—"0D€’ƒ\¹êšœ ¥Cdâ}Ÿ&°‚ ‚ü7 û÷õ]ZàNd6› v4Âï¢&}䎰B$§Ó.@÷û´LèÊŠº‡>¢8„!?ŸÏ#à¤X°ãH.Üz|ÚÞ˜PÚqøŒ¦¦Rd4Ö?ÿí½!µÐs ~‚ÑhFÈoµ\:ÄãuÐÕo+ V^Ó©ºèê8Ñ'W @YÑpС†¡D`E¸"å’úfýþãL!zuPªo1Œtšù\æZý?ÿQÿò?]> ”p‹±Z­F“ ŽáJ¹¤ëQõè©Ó骪o GózÔðj•‘¦×d±¹¿Ð<ßq,ñjM|}ĨîüV+õ.ò5Ô#ßç'x):Š@à!@(‘@ !ª4tE±RhürçQPOŠ ¼ùzkFƒ„Ãÿ±!êáË’ÆÄð6‹¿/[>ý9€Ãÿú¯5þ¸·újeE6“É z¬ö–XÐßÈ+"‡Å…Ýq]Â5õEþþÛŽä¹KxxåœÌJ‰ »ýº©î÷û­Œþ§Ž^7ë·qÒŽüª+òŸµ #¡ô7° AEJ$i½Pˆ”‰„\Λv`ãïïÑ ’þ€ ð  Ò‡%H¥ÒÈÇÿõ3$¹¯a÷®Ê‡t4#‹ˆw.89N«Í†h(xG(yŠ]¶gCE4wbêÏWÍ2*¡°¬îÛ}§ÜåCñãþeÄÏDÃv7¡Ýk¦ºõšÉä¦aB‡»_Ð$éM÷;¾.ñNÃÐøêÀOuE±Žt^"@Xvw™Ld2™"l¶j“Q‘qé‰'YøÁp]šDÀ—‹EÀ TR¾z<.®æ7Ï O‹åtNÅ=wE½ù¶06–mØë¼a ä€³°Õfõâ¦^^Û„PéiqKfŽÅØ£B*j›O](_>/1~J«#C”y%UÃb×̪jl1¬ßz¤²¾ÁLN[0eª!ÕoB”æLa2©ÜzÍ6Ðöˆ„è¼Ý¬Aª>ýñhAY-îCæÚ%èâ‡Ì³­ž‡8éº8sBÊÒ™cßü|Âo>|óÜvÍ{ýÑår@À0ñmïµÚpˆ @uECd¡é4)#@Xøöx\(HÅâH1WÂq ýóï|Ðô¸>@˜(„üÑ1a!ÁjX0”Í™ýÞùÁLÚ[YiùÝk͹Lüñ¾_„yWÕQÛÈ8¥%D°Ã#ehÀ`\C*•ëgŒ¹XÕ°çãd–yºHg´Ü{ã h•`#+®¨Ÿ"‰¯^4Yk09S̊ڸ㘠^ßì½Bù”}öbaY-BPBÔ¤5B2Xº KCŽ^äuæ ülX\8+­ï…V}C‰ú.ŠJ P]ÑPXe:GŠÀU€kü-@†”J¥'•ÖfÃFƵÈm­V·áÇ#Èœ€m2X$ ’À³xè\0Œf%+ÂÄ¥|n‚BªVªUÌĀІÀD2zLÌÿÖW=þˆ­´ÔÙÜTùÐ/¼(›;¯/pa!pÁnU }åÞÙâð̆½IÊä¾D$¼aö8<ª®o9STi³;r +$bÁ¹’j³Õ†û.Õàq7o^0z' ª´¬(Ä 'þIÁJ&›/{å–ÇF“¨Üç.VCÕ´b^ºB&)­i„Ã:2õž.(‡ †® ‘l«¾€ ŽÞ°ï£¢üÊŠüvièÀ(ý„¼­áVÍŽêÀÖ@þª†ËgÃô€cD—Í`¨1:x•x4B‡^ïh¨GU$hDGC°GÍü£øN™…¹rZ4؃ƒƒCBB4 n˜;RXÍb>\WýË_˜sNÁšVýôS¡ÿ÷¬jõ¶B/ d;÷îŽÑêîƒÓgSF%’!](­Q f܀ИŒeŒÛ šÒb0Å„kT2"q,lj¬‰%F=êR8Sb±‚g‰RR”cÆô¨íÀVf‘%"®W0œAKJDEzïâd~ÄßÿYÿÏ¿k72I|›?üÀV^ñç¿rE=>÷NV„ 年ñ0«M%GÁ âày G""ö/ØÈv=‡< qó–E·<óú†]PÀ±úD>ÓªG×Ôщ°Á¡S´ QÉæMŽ˜èb¸FB©),¯KKô¦ù ]7Æ ˜VšPV44×Κ"pØþ¡Á‰*Xÿk¹\uñ4"j¤ž#ky™=/'¶8B¡fåJšqFCˆZ)Ѐç8"4C`BÐáeb;CN§ƒûaÿ÷¬06®á•—°'vl¯¬¯|å5¾RÙiýþ¿‰ð†¬˜m0Yà4¬D’_!;¨p]=Ok0á.9¢ºï¦Yz£Œ™ø ­œŸŽiòÄš¤€³ix±rHá÷÷-%xA,ºƒQV£ "ÐAÚ‹­$®›>¯vÍéGŠ@?#@YQ?N»£ø)Ø¥1"¬ Û?ꃑÐÀ¬Ã/ùË»ÛiÀC¸î£E­n7Š›–+F š lØx p u.”ɹ3R­ Ô·ß!ˆŒ¬ýÝo]‹ù䉊ŸÝõæ[¨è.šôó#ð¼:íL©Ý}КvwzñQ%o/¶BhŠ€ï ¬ÈwØRÉA†!FÄ‚&nõ,¶X,8’Ɔ{é‘%B·å{NM5¦‘˜¸¨»ïá ÒFø9.,+`E ‹P!ð÷=œ‚|ÁBÁ»ïU=õ¤³¹Ùv±¤âîµQ¯¿)9ÒÃæý_m渔ñ©^ˆ´Ôÿ#§=R¼‚eE^‘ ¡ØòÉÞ TD¸p0­Gæ3Gs“é«/BZE‘Ï>‹´ƒ p#\„µêŒ˜7r³Gs‘ŒóáGU?j//w44TÜÿ3xÉfÍî‘~«œÒo}ÑŽ(~ˆeE~¸(tHF€ìý Ћ2ÄZÍØB×C¬{ë ©ÙÌáóå×]>{Nוýð) £"Pô‚ ¹OJŸû¿uU¿xÂ’›ËœØêɰß<§\y³{Z¦PüÊŠüaè(~Š@ïØ€µ¨ÐôÝ·B4–H"ñìO~:½~?XýïÿÖüöYã¾½ÈCV÷·¿0'ö~¤‡@»¢PºGÀSëx÷’h ŠE€"Њ@ý«/cãGQ}×=‚ðŸÒJ qxxàˆ/½¢\}+Á¡é?ïÕüî9—‰M/ŠEÀO ¬ÈO‚ƒ" 2šÆdøaáê»îYyipS {æYÍcOyú¶ CˆCÇd%£E€"àP š?¬E @pÙí ¯¼L&òø<)=†ÝÉÊÿì^aLLÍ~DZÙLÙÙ•÷Ýúòk“OÃ[Ü¿Œ&ËùKÕÕ ZÁl²Ø÷Â[Âû]ãä…ˆJ¢‘yµhý>0Úa"@YQ.*E` Ð~õ%Ο£wñÈQò%máûj0þܯüškù¡aÕ¿|Ò©ÓY‹ŠÊï½Gþ·8JUÇL\ãËk·g/*¯Ã¹AR†Ì¾ARqçá&ûØ_5¡k1X.U7í;Y€j©ña×dŒBÞ¸®õ×h?CÊŠ†ÊJÓyR|€C§m|ï]ÒKÈÓ¿¦;V×€K'ND*YæÄ~U%²çV=ñ÷¿ä„„vݪ‹§ˆž`¶X78s"¿,\£\smÆøáñ D]4tšuÆœ ¥»Žæ½ñùÞÉ#ãoš;änt¡ö[(+òÛ¥¡£ 2à>ŒX…´lá"iz[:ˆA6‡þ®())æ£õÕO>fËËs™Lõ/ü͸ꎰǦ4¨ˆSªYkøtû1äy]sMÆœIi$YGÿNÈç½äÍ4bVúðýÇó¿Ü•]Ó ]»$C)oËâóîiCêm=™N‘"à{le¥-­ÉP‘ò,äɧ|ßa€ô  ‰þÏAÓg0óqØ[Ö¯C¤ÇÍ ”ÁÇõÓ'Û²[ôæ§×^?ÊÈ€¤D,,˜æø«µ×7éMë¶fACا´@è ”õ=Ú–"@hC áõ×8v;>¨o»®ÄÏ€Ozä˯*–Ý@š8êë=oK(‘ÉlÙ|àtM£þÑ5‹’b¨kb¦®^TÕ ýv_x!%Fƒz5ýgð”ùÏZБP+¦S' {vcôE #”uĄޡPz†@ãÛo‘š‡ΓË{Ö˜Ö¾Œ€zõjõ=?ãx|® îÕV«Íh4î?U¬€/ÑeICèÌsß},P!4s:Uß @½­}ƒ+•J`•йù…µu-:½Ñdîh§)»8íø1LרT­ÓY]}So=ñÍåpƒ¤µR>6m˜Ì÷á—‚¦fLžní0Y,–†&mEƒöÖk§¶/ÑÕ¾T˜õ¢ŒÑ¶eµhõ)¯V“Þ§x‚eEž DëP† >¸.VT~·kßù¢‹LØ•\-—vö&íð~‚NÁ”©žëb¼‹ýÕ꼋·ºú–‚‹·µè>jÊM‹æÆGGùtv<™Gg顱Ùìà©.ÕÂÕ‡ð}:*޹ö㑼‹ÕÓ2ä3FVc­/Æætºêë›[´Í-øVÍ“ÅLÜ­\N'ÂdE"±P( eAR¥\®”ËBƒÕ¡”}1¤Á+“²¢Á»vtäŸ ÐöƲqËöçÎtöÆx(»¦º ãÆÅÜô‡Çàã“1 ´P&g÷Ѽ¿¾ýþìÉn[v=Ô;(¸ÑØÛd®kÖ+‚,.Q°Å܃•A8¥@D"áaE`BÅeå§ó ò´¦Îf'*F—„os Ž™Ï1ò9V.Ó/®ƒ#tpÄŽÄâ’[œ‡«í , ’bcSâÆHMŒ‰îòY™²¢€\V:)Š@o€~{-þÖ|oã¦ÊÚúnÃÞ ~ÓÖ“žÔ÷ߨ”t“óÕ®ìŠêÚGï\£÷8°PoVå*m§Õf3›Íz“6¾«Ô*·Õ ™Îh ‡S( ðYCG¸÷è±™‡š´FÏÊ+LåW(…5 ~½”«åÿxpY]£Scp·8"Jc /&|¿ç@ˆJ¾dÞœ™“Òy¼öAà &rÇà݈ïœÙêÉg.žT,V*TJ…P t"@¦áÁw€V¡PºB§5ìáß¾jÒjŸ^{]·šŒûY Š!T˜œ [0»+éñŒ„ÉIŒ}ëó]o~¼ñW÷Ý%̯PÂ_á_l6[-6{˜T÷~ÈŒÖÐ`  õ'PãªcjõMoòYemCœðÔ˜ ã~™ÇÞùWÀ+âšEüJ5¿2Fxœ.^£#®Ô8ñ“ï´Y99¯½Ãb³ž+,),-¯¨®¬ª«7˜lW´ïðA&Áý.166!:*%>V£îkúš=ôÓù‘î§ÉÑn(Ï ”Èd2}¾u{e]=âãuK‰à¬Ðôþ'D|ðkuêˆytõ—×ÿðÙæÖ®X6 ‡•Ó—1¢aí:r¨Ýf%XpA‰1¸À™†â²ŠÒÊjhj›››› &33¬n+É ’ˆàì/—ÉkºŠy²u*~­çÈã:C—ðJvÙé¡ß¾ü/èÞ _!hRp*cyu i„«qMB®IÀµ¸8<—‹çäðm.‰Ù¥49•F‡º¥<ê@YüvãyTÒí×O'ã9r¦èÝ/÷€cé—ÏŸFÚæ_ªzsÃNÔ<•_ ÍÎõ3Ç];}l“Ö€øOøò Vׯï^Òëyù[C|1 ‹Ý¸e[]cýì ‚x-Þ!_(»è¥ÈaK!Ö¤-/>þNYn[`Uïvä-i8X—.Ù´CÿÔñÜs3'M@†oIö–ª+ò’TE`ð!€} ®Ö„9])iZ>fÌ(¸rÇŒwùù- â"4 =Øçp^¥ÜËÖ¢{˜4*ÛÞÖƒ9ØAŸxŽýç_ªþfO"¢È;¸Ô}ËçL‘p¦ Û$ûÈ]‹ÐQQáþ”mí[&…ŽÕ@°Xe©ƒ¬f«]ó«}ÄÙdl{ÀH^­ŽÞŸ6&+ÿ›aRÉ¥!ª´Ä¨*œ7š/VÖAûSRY_\Qgw8G&GC»ƒ/ ærÇ’xÏ›2qDbÖ™"( Anf§§í?‘_XVC& ¤‡WÍ‹ Ùvè ´wl[Èì—ªêï\:§ñѰ¢¦dzDbºë‹²Ð¡Æw£¢º&;÷ÜHÑ¿ /Ž3uگžÞñÔŽwÒª 6œó‰‚„Šàjb¦+BG‘¾ä!#ÄAáø¨ ÃöŽ;¡ ó"欀"t¤LŒO QGN$7Ñ*8:ƒËó2qõPÎo¾X^ ¾è‡?8^ž- 7-P(ƒ¢.Âï&º°ùl¾ùäTk–Ê&Ý{Ëø‰(G‡©ÿö&XU&¤ÅO•´ãH.ô ¡jèþúGáø9&¿¬Z! S+"'«4*™¨³Ð£“£¡·yzÿë}Ùy%•uMØßûj/¤IÅ¢ùSFÞ87Ý]Q‘Þî)ƒ]öÍ ;Ð5l[°ø€¨•V7t¬¶þûLŒÊj·§Ä†?~ëb¡€ÿñ–C‡N‚ãŒLŽy`Å\©ÄÓÔëÀÍè06¿ºÆ ûÛã·dŸ-Î+®ÌÊ-:|ºš¡QÉÑà¯0t´)£“ÀdR ôIQ¡jvð¤ŒTÁJ¹ôÔ…R¬ÂMó&âû3uL2K•uŒ»ÌÜI#ƦƕÕ4Blªl[R€fqÒÈÄÚF-„H0w¦ lDRÑ/Í-nWyPÄß0OgžÈr,‰¢c¾˜KÌÈUå‡+Ïá½X~öS›¹ÄhêŠ\.O,,>övaÖKéKþÍåò"¹Hª)Ìzµðè«Ã2~•8á>»U‹†Y_Þœ0þ^MLÆþus"’¯»è•=Lž²ü3P%P"mÝÙcßܵ²·Æïp ÌN¹HÀ8B¡ßå­£º"o-4•C|[~}3$»»´¿!U²SÒÒ’ÉŸ¤œ°`%¨Ouc˜<2ïp¶mlÑCë[ >¦%D+eÐ=ýÊg ,°£‹àþÕ®„èP<ªkÒ!+Ìs¿ºëúiãR~Ì< ™îŠŠŽOÑ Âñ¾tÖxx;ýxè4Ê«ax™9k—ÍÐÌ'ó/àÎÊ“î_9ï|Iå7—ÍyhÞíÕêã;øˆ44¹e0`=uǵ{tp>p2?9& ö/ÐÄVVTb4fXÛBw„Z%˜áÈ}HàrZãúq8r©7ÛLŠض¸•£>iÈðñËß½ŸJ;„wðc…˜åUÕ*^ŸÛæeåÅyÀ|&«š«CfôˆUÓW7þÚ7#R®J½Áé°ýú¶¢ì%OzX bÒäé ö¼?©<ïóˆaK¸Å#lêݺØ x€ÏoÇjp‚A¨‹Rã#n˜›ž>"á«Ùؤ¡”‚ùî2íŽP¡rW×å£O]Õñ¿g8ûW‹Þ4:%+]a°B†p”#“¢@q31: Ęƒb^1üÖ¥…ŽÖƱÃâvgçež*€™ þ^­Y$ô8ÕÆ„´(¨>ûñ°H @TC•â'‡³+úlð‚]¬Èl1ÃËØÃwØ v«N5µu¹`<#çüv1Mìtø_[ü î·ÔäV„ Ð÷Xu|T6Z(VÆŽ¾=:m¥¡©Èi·4V1ë«aŒ Ÿ›·÷wšØ™ŒÞtö!¡©2ÌÉ+㇖è´yé%Û¤1 1Ab‘šž©®È+kM…Pí¦­œÖsCMS§Øx¼s%LG\0š`3ƒñ‹|œ42 ‡›Næ—B!n”ÖÀ¬62)úþsÿñ‹5p^9|ºˆPÒ¤ÝûùÆ{†¹¹çóíGaw›1¡“ó5>E!t Ly\^Çj0«=wß Ð…TÔ6ÁYx󾓨ûQ3D­€Ç1„¯™þ“ïE»±ÌÇU‹¦L7lÿÉ|œÌ?xêÂìôáKf3ìgTJ RRtØax°°`½Üg- ǤÄ@µ¶ÿøùYé©ðš_¿%”hú¸ahë^ÓÃò܉iÃã#3OVÔ5E†¨Ú4L6öãjøb×)’I¤-ÎVæÝ!Wæ;#nÌ.§M®IåñEÐÑ™´¥ dÐô{£¡<Ó¬'?ª—5rŽYWér:ª.|ƒ: e+ÃA¢Óäñ…µ%;L-¥(œ;ð§Ü]¿nªÊÖÖ1vó¾\àCÅÖ©;õO–ÚÒSÂÔÉQa}‘æÓ¶Ô¯È§ðRá¿F¿¸1¾6UQ—#El>Ýw?2Uø¼a÷ß&Þt`ÃGà[ n_3­IL•¸íðè„&›ˆ4[¬›÷ŸÂYîy“G—Š"EDÐ!ç±såRÉ…KU8Ó4kÂpìŽðJ:gîä~»¢˜Áº)*:yŠ_÷ÛwûNÂÙåt«· 4X«ÁT„ñÀyÇ£ž÷ëú&,GðÇ~G(8»tmà#“bßÉVCdoú ¼»o˜…aC=õËE€<^dü~äfv"Ðí±ê=hã „ÃbÀÐöçGV@°XrfÓï>wi/l¼PfÛº?…ñ/<ÅÁ4,ñSw^ ëçßý:%®+ë*‘<(Þ‰aꢸÐà‚rU…}l¬°¯Ü¢ãÄó3ÿŸH:zþÿãæêS5EÛP(Ê~CªŒ_ø@ŽY_‰2~tÚ5´kóöÿ!eòã©Ó~ Tšû1*Tæo‚¹ 'Xߪ ·”žùxêŠÏ‚¸âÜ—yU;1ž~4:•û [\r¨aâTA1!j‰D"A9(èÿl9Ýš²¢n!¢(Žaû^g ã˜)›3#4%áÁ•hÈqz±H€#ÙÃ" L8yC]³g”È8ÞÂeö¬76ì‚féì ä‘û;²á#ÔKó&¸eÑT”áa½õ@ί^ùŒD܆A½E”:Ÿ¢ ~AÈ~Ĉ²¢^bJ›Q(‹€ö«ÍdÊ[nD~'/<~KC³Eð¶f]nIw¹sÍt˜¥ÆÂJ…ŽÐX¥©Ì¾#ÒüÉ#|žX$|àò]wEE»§3/ÚZôF¨£È/ÜN…4"#c"O–X3?Kï&Êäs½vž‹Lˆe6îóc)‘ûÍ+Ë®N²uœv3[îKAÀµ•0J,§‹_ïH(³MÈ>k9[\r÷Ê›Æ ïÄ8Þ—¾úÞ–êŠúŽ!•@p,ç ¬…%˜¤hX’d\[( zªfÀiùž"•OÇ&¬¢¢Ó§¨ÏrÒ¶Ój±/wá°ÁÅÛý-÷`B}#CíMDý3î.{+‹ÅR©T.— ¹TÛxÞ² Ø:-N˜#ÌUñªÒ°KöÇu„ ŠñíÜ~Ê|ã[[ºõæôÑ#üêïÊŠíkGçCð:ºÍÛ‰LÅ×y]8Ø È.‚w\·Ó<*½;x› ½@´¦Z%ï>(ŠŒ”H¡P £ÂÇÃ¥ÔêM¼ë´BëL.Ç¡àÕAu/ÊéÝhI¬E¶­ÓiõŠvbqºÍ‹ŠØ² O?-èÓ£Æ5ë¾ KˆSÈe~²j!eEì2ÑE€"Ð N³Y¿spE"Ùâ6ï„NêÑ[„€ÇÁÀêÜ_ºmÖƒ¥þu¤š!¬ÜŠU* Ìž„;2£Qk2Wlvßè î5‚p²^pÿ ÷æp‹>³ói÷;½( %š…œ:öíõ¥ÌŸ^c%?lÓÎ+,šbZ“"@èO {2]fÓ š7ƒ¯ŸþÐG}aÓ%—TÈ­i1Âg«J­ŸÅbîà…ñÁjH?÷ÞEwp*+Âa4¸]clØø]\Þ¥F]­Þ*åjGKöÁ”Ÿ›.$tñÈfnÊüìZT˜¾zsŹ/KÏ|„tHÖÑ\}a¬UãõMEè‡ÑŒÍ%ªˆ -5§ÜH(Q·»ÓÚÒƒLƒ’®>ϽGdÿ@Ú“¶ÌÐTè~ß[e)O «êÚz„0eÄå-É}‘CYQ_Уm)ÀO泘_Á>º¹ ïáèƒdd8½`Œ>ê(0Ä’íá˜à¡¬–ò¸-NÉDžÀ˜]Og¹#bC¤:€«§|Tƒ‚ òQ®mj9t¾Ðn3“ìHf÷Ñ©.]ý¹Ö‘»,Æz”•qÌ‚qŽ ™=¦Ýò͉ïïç ¥c¿Šsõ8ª´çƒIÒØîŽPìžäRÎ ©2næmÛÁ¥@°t¶øNø{ùÒ9B.¾R„ÔÊOX‘_P3/#MÅQ(^BÀ^]k9}Â1QÒt&ºŒ®?üþíÖŒ°omÜùÒº|ÔK ‰Å'>s‰¨/A|'¢FX^U ³ÆÜUžT, €ÅÆýS ;=ö{ø\W54}½ç€ÌY²Pöz²(«”ÈóÁ£ÿÓÛÏüôHJ„†íîtHÒ¦K˜Stb”³í1}C¾ç=z^³À:[,à"L«_¥þð¯/çhÒšŠ@?  ß}ô"ïÒ£‚&-%ºÏôDò”±#‡iZ"D ª®oIŒ …œKU ¬Œ íÚ³MP@Gížâ#ä¸×!åvÕÜ+ô¢‰{ó-c¯…–ˆ/àßW(„ŠƸÿ¸O¶®i·½cÖ˜{‚F ÐÜψ¦`0š>ݼUÃ+ž!ýPÊÓu;©¾Và^‘w¥¹ú$”Iɶu¿£‰AÒƒ„ÆÍfÒƒÛXQUÁf¼FÍýëÄ¥ï#ql_‡Ô¡}}â\Lˆƒ:|àg¿C•¹A-hƒ;í•"0(0ìÚOÆ)[8çjFºu$|Ýà¦yé×Í÷§#‰¬ë­p8œÏÿûkDùC^z$Ö@š¸€D…ªž¼ýZ8Ál?’‹Èv»cBZ<~#‚ !Å,²k5ë ¿~uÎUÍœ0üÎ¥3ÜûÅÍõ[åWàw(RÏÞsãl¤w]·å`nA9L3Æ CýF­áïlš>>5·°!Ñ/B%Ý87½°¬æµO¶!-ÚøÔø.šÜ¼p2IjëÞ¯¿•5£% "X"+$™ÉòåΣˆÍ|&þ6Zßi}1ë`±+TÔ-YXŽïzì©d8ÁiæàñSf«e®ì ¯G*r‰â¨Žš5Éý~ÇØÖîwD:©?‚£§À-Él¨!m¡X²[µ»ßOO™òDüØ»ó3_ðâ©´F{ÜQãmÁAâ„ð¬—_-Õ]ù塟(ËØ*ª­ùŒ—¥0)A”Ľ½ü¸õÿòšÆu›"xè²mÍ–• qŠñüPNÒA¬˜?éĹ‹[æ¬]:‰NWQy-ö³M»Ž!wý© ¥¨ŒYAP@V²ëfŽ ’ŠœiïãùÙGªê›ÿøÐrd+Ce“ÙŠøÚ5 Z„vÌ“ ÖUV݈ûpMÊ>[<~xÜÄ´”uf0``àg`N]7AN.Töÿ Z‘¬H$•I"%NËþæ†Öÿï•b¦˜¯ˆëL&‰ÔE|ÿÚÔ Á™üòê¯FÌ3xeâWÒTyÔÐTÛªþÙ³ñg"ªAE„\H‘†PŠÏܳ n×3½ýp®Ö`|é£`hCó§×^Ìkx½óÅnäùõÝKPL(%6 YÏ´&¯Ál!´à·÷Þ@2Q¨äR¤w‡¢éÕž¼ý4ï¶ÉÕ&ëW÷±‘`;‘J%ˆ¨0Í&K¨MWgv½´nëªESçLJƒ-ɯìÅÁÀ—†3h‰„G’Ч’ã’ ²¿z±£>Š‚F¬ bR‰Öns‰…ܾfÏè8¤ío·…‡Æ!²ÏsOôÊU¾Å»Ëie³yt¼Ó.=[3óÓÅ8ÿÃnÞ¢DNï¼eþë¹€;<*L©TÈdX8†ùÝ“²¢Žß1z‡"@`08B€/ìœÁ5'¯¸2cL (jÂ%iÅÔJY¸F³ûhžÑb…é  ÉQI³r‹ Â9š[ô—GoFÃÙÓ@‰ Ö·a­9AK*ê#4Jrº¸¼Ò`ƒKmUÞÀ´é±[í>zº¥­s.UÕ#+6ž´„(©Dr6*9:>2dËSàa„¡k Š¢q©q#“¢Ñ°Û&dÖ~þŽ]EP‘(r“Él¶XlvWoh°‹6nÏÚuôìâicÀì¸>,°8q¶óH.òÖɸ–X/X¥Àæ ñŸÍß|ÓŒºÚÒ¼mΊ}N×°3æ%é’¯}mßó ÑG'_í«µ²šõ­W®:{âYœš‰0N£ V3—R© ¢¬È+S!Š€p4·XÏ a\ ^ö„ƒÐHjv±ªšäd…’v40$ÂH¶fæ ¬R&mlÑ¿øáh/~¾j6ì³ÅÈá UR^›±êà© `-‰ÑaÐ-•×6B ¶½ŠÚ&È|på¼`¥&6ÒûžìsÐ*=¼jþ¢ŒÑþÏ7r©„¸#`ã JKŒÂA¤ÒêÆ©c’ÙGhTÙyÅv»ó-ÇMOš°mý¼@7Éå8ج‚ó Ââ`À<£Éhµèš­~<ƒ£Z! ÆäA?òµé9¬˜ÔaM:C³Î„‰ˆ9¶HE-«U* ³»B[„s^¤ç²½ß £±ì@céžÚ‹»ëÊOµ8 ZN}Liøj§‹;^ºÅ#ïOÃ{ìñù–9µŽábž+A! Q)°f!!!!.aú+"?˜Þë³÷’üâ;ÔûáÓ–Š€o0=‰?u![:}òÕzÀ‰¨‚àj³ôÈáŠ2²Æ¢rD“D|hÁ”‘( ˈ ´5xÁjv÷²Y¡ÁŠYéܼðú§Ûǧ1KIÑ¡eÕ Ø×‘ôÝ/÷ kDß»±ôIV»ýmE¿)qá·,žKJqEí§?Áåð"š1>&<(Ÿà¢Ä¶ ×(,V;ˆ´P¸9cü°n›°mý¼½®ñ§64jHmŽÑ‚§ B¡Ñ(µ™¬vƒÙ)°jU-MN((˜ÅœW+¡ãq\|ŽSÍuÊøN‰H$W(d D!!Á*• ŠTÙ­ú¦òƒ ¥{ðj©9“°Æ²àØ3LÎxÖ^¿»"tN#1cg¹ÜÁ»$}‘¬.I¥môEë¤fg¬ë —ðCd0øÊÁ„À‡BCCƒƒƒñtv®ãL(+êˆ ½C pŒGޤÓÚg¹° dª‡]yUáDž!íü»ÏÝã^ïÙ{—Ac„È:J9Ñ×Kf,™5>H"‹~JÑJZá >þp„ë,©É¾ÇE†üãÉ5¢–³ b]½H«7ÁY“¤€Å{»®q /VÇvÛ„­ìÿxIÄ"—K†¡"º ”%8’¦3ˆMF3bÀ0 $F—ÿü.]Œß\PC ð¨»ú/Π%%‚S@]HðÅ#‡ÍØTq¨ Uewt¾Âñ.MpFŒx”ÁÓÔbªolªÔ Ž™VŸå¶¤ˆ!¶µ˜PÙZà9Ô〉·'ÖÙ“ëÉ.OÂãDHxÐÁ^DX0!°"\0|BQ„/-× Ô;™”õ7ÚŠ"È`etEØh‘ñ{ÂO”¢Ó9#u·¡¨±ŸuLG?¡« ìô>n¾ªn÷”eZíîwñ±Mº6€°`S‘JË s*MÄø_ËôAF£‰ñ4²Á¬…ÃŒp à8ûÒ5¦† j0Dh‚çhNœ1>æ Cr(‰‚@‰úmguØ-KžÔšwøÓwš+¸œ¶ŽS“§†$,‰Ÿ7+Péõúúúúšš'§Î×"_k47[•¹–ër-׆ðK£y‚ r~cGQ=º£s„VÚGŽŒ‘lWó+{ÔÖóÊø4/‡{„«•ðtPB:9|§Kht* ÎP£ >…|.Ç)æq‚…|…ˆ?o  %ÂjÁ‹ˆq&j%³øèo¶3eEžhMŠÀPAÀv±ÌÙÜ‚ÙJÒÇÀuh¨L{pÎŒ!F`E0¨1gõÅ` 8÷dƹ>› Û0¡E˜Ü`$F˜FŽw&jeë±;&D£xÀ½Û-ˆ î“j>Z@§ÃÖ\u~BP A9ã`Α5é¯èMªJ b^ $ò(öàÇhÁHøfŒº.¡Ð ·X ÕÛ{\®%áŒe‰c æ—k¥ ^Œ×(ã5uë~•ÞÚìŒnvD78 ÎÇ ¼²Œ·ÍúŸ‚ßÀÃ[£Ë5_‹è‹®KÏA,¦VÁ\Œ„`­0aªy Ÿ'Å7RS.saú`®à@Ð AQ(Phµxö—íÑô)+ê\´2E`H `9{žÌS<†q ¢—Ÿ#€½›œ3„vÉ‚¤+(‘Ýa%jÓa ƒšAWÄd8aÂy *Ä\ÌÖë#—løÚêãÄ:ÖX~ÐigB^µ»$Êø¸y! C 5¬ÝSòÃÊ€|” KÐjµF£Qb6ˬð”³ÃÒi°9,©Ö™RoIqrÚB*8f×,äšZ_Q€} š»Klv),.™“Cvp‚6 yü0GÊã9\œ:«|áÑ$ÑQd[ó 7²»„ öÄJûÈrÛ8G&âÉ…Ðÿ2™&)c‚¸˜Ui¥DDEØ.Ý^«{5j²Í;…n nRV4PÈÓ~)þ‹€ål[îɘ!špÔ׿ê#Ã6C˜86ž6-ÑeûÙÕÛ ‚'dÓmMýÖšû­5¹éÅÑÃc][sò2:à°^©jíI,®·'™Ä£V¯y&HýÓQÇ« ƒEÀr ±%ét:#“É„P#­£×sÂêiu¸lN¸ƒI.±Ý¥²:9pÞ&ï "G É\ŽˆÏóÛ81¾$ˆEöF³½ÄšQdÄm‚yNůRñk ‚q;¡wíos‰¬.ldzgÂ,59â´Îp°1!×çAT(1jBk0/4Ç;>b‚„aš,%"¬ˆ!‡P µ]¿~ò‘²"?Y: Š€!`&º".W»…1·»DAá!ñó`ÓÄÏ—k†¿±~*xB‰ˆŒ\ï$ƒÁÀ# 7ÂÕjó·a®ËkxÅÚAa$„ˆ.‚.P@€ô±Ec±è-vƒ#¸Ü>¹ÄÂÄÀÈãØÅ\”O®EÀÁ±ò½àÚ\+'Èæ’¢i2r¨qvLÈå«…™€$fL–¸@nðÎ’²$dàå„¡¹Z[0o!.æCdÖ”]^}ú?E€"Њ€Ól¶•”¢(LŒçÉÚÒIRlH‡rþRuuƒ©âL›k0ÍÇ~ŽHTˆõ€ ##Éyþ¬ˆÐ^!³\ÈÙüMSù>›¹7g¡D£‰›Ûj›¯Ý—¾Ð–#Bbˆºæ$(ŠàüEÞÁŠpÍ“ƒ¹;BA ŠHÏ D„¨ Q B “P.‰Å¢n¥Y Z‡ 'í.Ý¥t¸”ÌÂky‚ ‰x8¨AÅGˆD#ƒÕ²U¯Cz³!Š÷€Ô:%ãaXÏew"vT(@‘ÃΟ ”ùóêбQ{yù})JI€îi—}@€¨œn{Öù¢ò:¨4J‚E!©\›¾¥Â°)¶ïƒåRuÓ¾“ðFI»&cBŸ“-ÙÛKZª6Vn,?ß²¹öÊ<{±J7‡8M+ÂÆõH¸'cu%o Ä $ÆýÂrbDôFDŸƒ÷N)á æ:J„ƒo¸ ‹Ù‚("­ký¦ù$y'¬ 2É€A‰ˆí>R„á>©Ì6toÂð VFÅÖñ"¨CY‘?¬EÀ°•·éÆFûѰèPºC{žÙbÝ|à̉ü2„_smF füØu4ïÏ÷NÓÜ ÖÐ0gS˜ëXÝé*ZNÛLäÍå‹äšØÙ„ )#ÒÁ ºæ•G` ¸Õé!|ï ClÙÄ;Ûµ;1‚0¼EÄ‚AÑV„2x±„µ“ áD,áC„Ͱ’Á·@‰`#è!a¤U»æìÙ1®BWß§Á5:ZŠEÀ+Ø +j½®–èÃ+½P!^D{!ö×f­áÓíÇjuk®ÉÔì°Ð{!è9£3Ùawe×4h×.ÉPʃ°wij¥*+¹ù÷<ŽÕP}ÅCž@¢ç¥è£–Ýø„&6åŠÇýòíÀBƒåMa݉Øá.xŠË}Ph DÞ!åVILxÔDsp¨sàºJÔjšcºYb„ ¨æNŒÐ\¬(·06\ @¸@ŒÈE>’~ÑÊ}`Q€oC`GgATX]‘ K]‘ÞhÎ-,¯¬kB`k$¥Wüæ<èß눲7:,x̰Xydð Ÿ9oÒ„}²-[k°<½öú¤˜°A4þ^ D`þ”‘ÈëòÖç;×mͺÿ¦™äØq‡66€ù\žH91$nfHìtUDú;_î³9øª¨©B‰Ü§ÌÒÜgù ä#)´k‚¤!Ûœ­º*nDÈ;b)W;±¬4BŒðÎ*¢P Äú…§¨Ïö`ÊŠlAét(}E€Äo„A¨¦£,ü2-­nøvï‰ó%Up[ V)dPŠŽ5×ꆖ‚Òš&ÝYü¾™u㼉ñ‘!þÿ«ËJ„8Ö›œ®iÔÿê®À§Dì÷ äÉ[^Zÿ÷ûrn^Ž»ÝzE¦Ýzìôi˜4ùú¸a³Uj öu¶¹00¾Œ ”„`²DE˜;Â×—{/:¼£-yoÃÈ!òNª¹7 ¼2eE·¦tF>!à4¶E4áµå,cÅÁ=™Ø. UËoœ;~tRTÀ¤Î sDJµ³%UOþ¿¾Ÿ1.åÖë¦!\ ;}?,`«ƒ/QIyMnI g¯%j·˜ïª…S6nÏš4"e#÷ È W´¼Ùª¥™Àãzâî>µnË /D¯nD8k2#;J Œ‡}G½:Và;W|‡xžtjŠ€‡°¬ˆçÆŠð›´Eg@6ûòÚ¦猟66 Bz(pUÉ›>6yêèÄ#gJ¶!7gÐ|@LaØi§ÿEÀÕª+âŠDøc“Œ”©Fßùbw]³î¡•sfŽO HJÄ. f‡9>|ó̳ÆÜûÔ×—Ýáa°Á—¶¡I[Ñ ]”1:°åj˜`Ö˜{IUS‹V@®VÞ§xˆeEE«Q† ­d|qLs!Àf³a[Vymó=ËfÄGvâlÐ`¦˜/f¹~ F8>dسÇVRì žPá(·Ñd¾p©œ ‡ð=iu0w¬NÞÅjXrŽtRý†eEý5íˆ"08àIÅÌ@™¤P:üÏ•¢²š¬Ü’¥³ÆJD– óŬ1w ÈM½›Nž¨~ú©–/6"‘•']`<8[„ðÈuÍzø¼ã'­²æ¬ Â)}âëe Hé¤Ü ¬È Z¦P8\i›“5àÏ•­Oý¾DCÌ:D%ðàñ‘Â^SSóÜoªù [Y©ç#‘(œß‘2Bo²ª•C—ÄÔ ™ñ!Vä§ôgZs¨!@YÑP[q:_Š@7"Ú¢Ý8š[`9‚¿FAyݬ ƬÛÊìôT ¼î¶‚ì M~Pºò&ýÎdU1±‚¸îma0A)¢f6[-6»\:˜,uóýëÕcdFC®7XúÁÖÙ«1ÒFƒÊŠÇ:ÑQRú 6ч­ºŽ§ ʱÍà~¿ Àß:ÂÜpð®ÛŠáàÒ[nn|ó_.³SæÉåÁ÷=öìoÜþ] h­xÙ#%@ G‘`€©ô®¶ô~€!@OæØ‚ÒéPúŠ€01ŽˆÐ;%͘ˆÓéj¹4Àâõ#Ì䓉„}?ál++«ùÆÚ†Áå*n¼)ä®{,–æ²2Ç_x“%‚ªFÚ; 䈀‡ÒjŽPVÔz‡"0¤ŒMæoÈ:Á`-eJD ÀhúÙi25}ð~óú86‘,;.ì™gyÃRu:§¶ÖÃo.F;‚¶ üj ä ü¹Òú ÊŠ|-LœðUJѰ$KA±µ¾A_X V™|¨;ó"¥‰Ao$μØw{ÔQ¿c{ý«/;jjÈ÷‚¯ÑhRqÃCXO¿,mû?å{#–è„©5™æÛ»9’æMZƒPÀïçtr`‡”õô[DëwD€²¢Ž˜Ð;¡Ž@Ðܦ‚bœD×ef;•‘CŽÖù;]Nx[Ãb·•žfª²ÖýãïæcÙmHòùª5·iz¾D~‚m‹Îø›7¾`N“1&åö%Ó‘‹½Ùu¡¸¼6+·xé¬ñЫýíýï†'D=¸r^×MèSŠ€"@Y‘. E`€P,YÔðþ'ÐA4¿ÍyË<ÿèzB‰z4‡N×ôÞ»-7 îi(25ôÿž%§ôHN»Ê îx]52~xÜÔ1)à}NäI  ÆPhíyï83¸i¿ü¼ñí·z&.a||诟 š1“|ôÿ÷ãSo^8ã\¿%3óT\… ÉÁGâND‚›CIÄã2FòîA]Ï j!“x<.£_rËòæëp‹iJ¹Àűs9H²¯¶¯T•âŽa‰Kâcº3}:” ¬h(¯>;E +x"aÈSr¿ÞßU%ú¬óÉÍ/¿d-, x —Lfê;×r…ÂÁˆP\D‡SЬ3ÂÍÖ48ÍœºëèY‰X8*9º¶Q‹IáÄ’Ë+qŸ ô@°¦¹ßiWŽ UGh”_ï>n±ÚáŸÔî©w?ŽT)n¶ñNÚÌUJ©®ÕÛ½ E÷üñ3¯œ>ŸÄugbÓ‹"ÐêWÔú™"@`FE “شЩA¯᯵?ˆ¥Dòk¯‹ßôMðÏsTȘÄjUõÍ 8‹§ÉÌ)€uQyíªES ø‰ QI‰údëáØ ÁnÔaÁŠ·?ßÕ¨5tDÉýÎ-‹§ÂqNÜáÁ êK`$w±–åîXƒíÝ‘ÉÏŒL©n£kZ›-‡ëøB`~bߎÌʲ«)ÿ:Ho<TWðKL'HèƒwkïÓ´=hì²Ù’g;še³ÛIuѰÔÐgž•NœäAkÿª¬”½ûÜ=옠"štù#ljˆBÔ¤3ÀÿZ(`¶ ä<Á¡3ƒÉ" X/"ägýË#7Ã- Œç¥§n#¢îX2/Rþͽ7 €Cþ¨sÿй8˜vüÜEð­è°`RÁwï"oIDØš´”³-ºO /í(¯²ÃÅ›ËÙ_Y†W‚Ru{Úèå)ir‘W#øn>T²/ ¬È—èRÙ¡ŠœNàW«5˜5Ê ¨þ"GVs>+µ•þ{ç߯q¦}¢w`ïM”Hª÷.«X–m¹7¹Å%.‰SœK.ÎÝå.wiw_’ËåR.Ívªíزã"Û*V³zï…M"Å*öŠÞñ=‹!W0HB’øî‚»3³3ÿYì<ûÎ;3V; y02ƒÔÙkÄ€$t¯ø1s` T§ m-SüNà+.¾x‹D!YÊÍGNvýò•’¦~×c¡F“ø¥/Ç?¸AðY_ãä0£ ã,Cæ?;dÐà¬^w "`?[Õp¦ª',-—‘¬-ÌNœÎ(™œˆ¿Nké·>Ü[&pY¼7T½¾ïG'ÿòÌñ{&?Z?N<Ã)-^¹hã劳Ü,f§ó­ª2ü-ÎÈz¼dúªì¼SVŽM>é*ãB€TѸ`§‹˜%óÏÆ':z +çN†gîÉòºåõî;ûè­ gNÎÞærkWìMí=è‚Y9g2@ì8V‹¯†!篛¾½ãÄwŸ¿+ËŒOˆâ72LP0,ùòE0¹TºÿŠkÚ¿öŽóƒ­’‰ª{ÓÒO,¾iÃW_i8{mÁ@Æäã/˜À£F'¸-¯ð®¢âŠ®Î¿U^ÜR[mwsóm¹Š¿ •Ýj÷O.ÑÊ8+Ú&RE§®©¤D`,@î4¶õÌ)ÎY¿l®—Ÿ‘tµ½÷쥦{WÍ™99 ª¨ª® ª¨²Ž{A‡N‚¬ý&?3I§Q¢Sæ¹{–uöq]iB¡(@v±f훟¿ÜÈ-29ržX¿ê\¹Ú±qûI¬M˜u‹§Îšœ}µ£÷­Çàÿ ´lÖ$ fàSn½¡í[ß×öéUè#„Ú„Ä/=½Õg´bÜ7mãL»o¥IÉÿ¹lÕKó¿[]±±ª¼ÅdDRøüÙéc¿>wòŽ‚¢ÇЧ#̈ӧˆÑE ¤žõè**å–1 ÐÞÍu„ç¥ñ×bû°å¤%¢Ó¤²¾§ªê[sÓ¹¯˜gNIΕ«?üã–ÿ{{ÏÅšæ¼ô$¸ýðчÜ9QVWÝØ~÷M³Y7¿GoÞs² Á>9R9µaí±‰…£ÙöKDW‚&‹U Þê`Ÿ7š­\þÜô9;îìW«Ö-Jïï]µ¹\ïWW=¸å½'>ùp[mµÃkLºÑ”)|t [QtÕå–D:Öó…•øŒ²}v|fQö³—»úLMm=ðba6¬¿hZÁ¹ËM:»OT/¯ûöS·ö†>WÝ7#˜BE]+´Ô- §Âæ„4ç•æ•dèMÌxïÊÙeWšOUÖWÕ·á+ôSˆ*F6sZÖ7¿¬*imWžLXv$¸PÔ^Xò0î‰ôêÍ:E^ò!¬nÎ-À_MoÏ›U?ª¹#”ît{+þ’O*ž2Õìq+½óXŽ{©)£A ·Ñhd‹Ò$D J ¤%r6P!|þ/5´A‹¤zG¢Íš’ Ç£-/àìL¯¿3¼‹v¯ÀŒÉ0üüãã·Ü³r–Ád­¬ãìI6$›Ð’ÁwëT …×R8 ÃÁë[~´ÿ<¸LÍ Z0§°–Yҋϧÿø;ÒÂÜ`Âvo¹…D€éñ7ÚWŒØôQvèB\˜„%Ÿ‰öï‹Vìyð‰o/XŠ¡û,ÍN‹ù7çN¾gØå1Ÿóúh‡åZ”HD [QDUe†D=¬TÝäœÔ²šæ·wžÄP²“õm݆¥3 ÙP£¬-4 ì7™IñjJ‹úÎTŸª¨_³ $5Qsþrb.?§«ÐËÆ¢Ç Šj籊ӕ 4·îÌ”„ÄxÕás5e{O]jhíþγëáWT™¼dÆ$8€{ãB8ÐZ$-*P,žë—«ñúÊš˜ßÐÿ¨U}n̽r^Éxåg|¯‹²£RÓµJ £0B¡0ƒÑçJg`HÚ¡æ&xd¸Ú€VÐê8ÇcÛ6MMLÆ©Û &ÉDÔ’Žï-ΫS]†“&¥EˆÀ³’’ø ÏëÛ—NçÉÀ úÓ“UXÁ:YOÞ±xï©* ÎG_f¿oÕìÁƒð«áS(ÌN^8-¿¢¶WÁAȬUó¦`ç–E¥ïî>ýòûæ†[æcJ"t±í>^ùýW7O+ä†ú£ç.Ù»ˆ)ŸTôîàŽ.aÃLŠJ‘}çÑ‹ËçLaK•Eo¡Fsx’¡ì r!¦~`A"£@i-ÏÊÁ_£AÏÞ/¿`ç&dˆ+ïîü·Ã{zêèC“K6L™š©¦qˆAFÇYREÑQO”K"E`zþ¾èë3Z0i2š+ßÌߺdþ|LÊNÁ–d·ÚÌ€ä{6$üùaû_Ù° éCHa¢H´[88·$wú¤Lt¦@'±9—×.,]:sD,U$ñ“ïØÖÿ1¸H$I¸Mœ,sÕ÷÷ŸªZ½ 4Z‹4Ò|£Ô˜kV¦ X°4±ëÄËÑÄÿÓü%âòºËqŽ­âr/ç}ßk³¾zñìËέÉɬdï©}´èt¤ U©5Cù"QNjhÈé‡+„ËNè7¸£ )øÍŽÍÏ=Üu£ñ8T g%’Jär©\&ÓÈÅ*‹íÝ]Çó3“±Y4–hdy®½ÚRëdžäx%P°0‰<²ƒ‰÷¥©qÒßÝýÐ‰Öæ7+/în¬sy<ðåßÕP‹¿I ºÇK¦ÝU8E C ¶ÃŒ–¦ŽmjT:"@ˆÀø€UD**’+• …Rž.w‹=Î_oÜ ¡0¾³«£¤(¯Tà.L–Pïn?ÙXžùóUë0˜ÿ 3æ$ÊåìŠ5}=?8vpõ»oüøÄa¬%2Ù K„—©¢ðò¤Ôˆ cAFô)rµZ¥Q© ’Ef‡Õü?¯mÝscú®ÍŒ0¹Ûk t(#Jê²[óã jl* à:ÑÆ|ºt•úæ,ÜýÀç~´lõô¤~CÑa½âÂúM¿¸k+ç£Åhi‹Ôƒ%EÙ$D€ø€÷ <Š`"ÑhÔ‹Õj³9œÑÔ唾½ãØîãe·,ž>kJ®V£ô‰õ»„g»Ž^ìè5ª¶lP— ‰×PÈè9f'‰îž4ç;ÛÿVqq{} –>F”ƒÍøËÕÄcÝÙûŠŠ5Ò!–Ô œ2c¤ŠÆ8]ŽLð´€÷4+-ówqɱR¸î„×#N?J#ŠÅb™L¦V+íö§Ó‰9œP¡Ùb¶Û ½öŸ}ë“£ZBÇ™’ä#œ 2Р`&³µÇ`ê5pëÜËâéb›V%Ó&$$ê´Z-¬EJ qÏïÌäÔ™+ÖÀ)ûï—Ëß®*o·póH5ô?9yäWgOÀß^GEÚÄÑ˧Ãá4[­«Íb³:pyâ6LÂîõÛ“ “QÍu7’8¶Æÿ6kt‚h&ÐÝgúéë;ø`\FËcY4þˆïΑó5>CŽ5C°ï½òñòYEwÝÔ?¶oĉ¼»ü‹•J%Ö6¿/P`‰X"1›‹Ýi²ºÅv½¹¥¯Ç1zÑÛ‡ãtÂ8(έ¸U"7ÖúU*Õ ’()I—  /CÑà›0I¡xaæ<,!²»¡öo•e§Ú[Æât¾s© Ó21Z cÖBŸIꧺ¡±¡¹µ±¥µµ«»»OA48?~GJ¥T¤&ê2R’Ó’“ s²ò³2aió 61¿…‰YïTj"0F°"쌢¬æöÞceu/¿¿ÿ[O¬Ã\‹¸6šq›ÝÉÚ/¯mÑ›¬¼*ò;Ëç£÷}Ç©  Çú;Ã8â²%Gø¤bcn4r™ÔãábÉZK0$Í`’YÌVŒ0 q–'g-ˆæ ÃʰaŽF1Æ)›J)‡ý g°AÁ©ÆÞ£èºDÑŸwkþ$üUõta´Úæ+ÕV·~Èñ¶fü¥+UOÃ\G:¹ÿœ¥×Mu{äìùÃgÎ×6^åjX(4ËÄF‰À¦Ù5j‡Xè œB§†½zÂRèö=q"·GârãOêt×uµ«ÛÛ¤6‡¶$¡`JAÞ’Y3Ìœ&‰“Ûu Œ^RE£Ç–R&D ãä±öþ°4ìk[ž,¯ÇÂõ‡ÎUo;\æpº°:,¦|¬nì¨iê@ûý‡MŸ»w¹ßÙBïPó–®¾ýy[¯Ñ2%7õs·/–IŃƒaÒHLxÝ­7Á.õàÍsö*,úÑÜÙ‡Þ·5ó‹yÕ3­%¤Ca¬>g:‚ӱʨ4›aD°¡ít»ášÌµ›Ø¢´Ôœ ‚$róVžÙ‡. ÎÇr½AJ%$ LİX—ôý%+ÿqîâ÷«+7V•5¹”[ͦ_ž9þÛs'×aŽìižÚKz<~îâß6o·Z­½*iWŠªO)±HD#^¨Ëå(m΋£·µ¹ª¦î½{ž¾ïÎSŠX6z †–öΖŽN|öŒ³Ùd±XlvÐÆäPøýdm‚.!^¯ÉLKÍÍLÇ ‹ágIExQöˆ@ŒÈJÕ¢$,C»ŽWbºEtн³ëºü¶E—Û1ë#A|öË®BĺæÎUóŠ›Ú{°¾ Kˆî—Èî[±ùÀùâüôgïY¶íðÅãeuPE‡Î×̶gî^Š…Õ¶-ÏÏLb+F˜z‹ÁµOFPEèPãÆêÃÓH…&ÓjµÃ\äÂæ•Eˆi|r³Vz‡ÝqS4Éå?úÎ@IDATœwŒL66s…å†IÉ>?mÖSSgîojÀú!‡[š,œ²?¬¹„?8$}sî¢B¹ªGoèéÃêÆTªÿP|8“c”$Hyõ•Í{¶kdõ‰vˆ¡7,(h’Kð׬‹“:\¦_¿ñö}kWµwõœ¿\£×sÎ#ˆ³K%QœC(tŠ0DA \3NlÑ+:ÛNÈá†À:mBI~ÉÓŠ&E£©¢ï)J€ àPè>ƒÙæ«V]¬nÞwúRg¯/ú˜Œž"èKñ® ëw–¥=¯4F&ì÷åÊk›±Šˆ_0¯&Ÿ©lèì1–¤/™Qp±úª\&ÆÚ Pˆ{©¾-öTÊÅ„œEÄbLí(…ˈÍIät9!‰úmEÕª¶"n…n:o1'…¸ jpÜçÈ6(ŠU9yø«ëë}³ªlse¹ToÖX‚ºî——ñö.N_xû¿ØU^Wz¶U§¨KQìêcAfU¥k4 ½ïïØc“Š»⾌x³L”- 6'»Keuªm¶ÖŠÊ#g/à¶,.Ì_½hÞ¬’)‘ãò˜Î’*º." @ˆ@ › ©`­Ö^£ùo}Š5\M/€ÇOK—Þ7õáÎ2Q…¾ †Ý~p0<‚_|x5¼—*®´ì8ZŽÞ´¯?zsŸÉ’•¢KLàÜn°ÂÖ¦õ½VŒí£áaJšÖ”~+Ñ@ÿYT•Ëml˜Œ¼+À±cQZ.«ÍÞT]'>_3½® ÷´U"2(ÄmZ…U"´‹Ev1L2Ÿ™MªÎ@b·ŽAùh6ÜÁ¹¬t«Ý°!J €oþ:âäµ'èp%íÆæ¦Ê7ka¼\·tÑÍKF…C÷hÂÒ»•²Mˆ@ø4ÁÏúb-–e=ráJ‚J‘—‘ØØÖ³–'ƒBBŸ×€“¬ættõš†:{Ï…ê«mÝz«ÝYœ—68˜ÞhùÕ;{MË枥›\@`˜F ‰Ðg79'õjG/\šrÓFqDtø˜<%& D"t¼pÿ°±äø‘§>N1y3vømœò†Ëˆ·ïØ©÷D§^)iOSw©e~hðe1 Gà¬zª6‰¨E§ÀŸÊêÈèµ¼¿óÓGŽ?|ûÚ…3¯-úUF#RE£A•Ò$D Ÿ@Em þ0p,?#ñž•³±1#,w¯’Ã~SßÒ ¢’üôƶî¿n>ú•‡V >‹´RušwwŸ‚$‚oPi~ºáü‚Á~0¿$÷ðù+ð7R+dènõZ;wËÁ ¿Ü¸6¦ÙSrrÓc\Abã¬D.·Ñl½ÔÐÞÞc0`ú›ƒ3²Eíµ‡Q„•,=1¾8/“ALG­USÓÐøòÛô mycF"¤CÔÖI°‡ÇRuº¤)ÑYÐaúÃ;›ÎW^~ò¾;Ñlü1Gªh̑ӉÀÄ ¬U¹:=D¾öðtÅ«^¯MDzY“–Ì,„+ÌgY:è ‚~Ò¨úG¸ Nä¶¥Ó×-™Ö«7chšL$›ž”ðì=Ë¡`cð®{•k†âƒaS[÷žÓÕu­=h†áÈ…é­±Tï°‡¤3[lÍú#ê‚ “²’ÖÌ›’“žE~E¨=GO¼óÉ.£L\™§³J'Vã‹òVd%¤è­ž‹å˜Wé[Ï<Gùȼ'VÅDfP®ˆÀ$ Uû¯D…$“ö¿A> DL¼$bÄC"̋ȩZÝC…}Ë2ä>³ÁÁò·­¸XÛÓÚ#·.ŠÕ?v+{åÃó'gÞ¹l†BÑo7’L„„ínﱓooÝÙ¬•ÃQþ"$ccœŽx9ç¸ÝÔúÚ¦ÍÏ>t/ÞUÆ8Á\ŽTQ0”(  D B @ÁDïÝ®ž¾÷÷]ìÒ›^·è¦yšϛ†Ýkå¼’ås¦ì?Uõî®í=ÆGn™—˜ †ÑˆY#0ÏDe—kÞÞ¶«%A^›ª‰ÀŽe–Œ IuªZp¡|Íâù9Ù(Œ"Q©e ѵˆ ÑK€I"ÌØØÛ§oïy£ÕñÍ'n_½ 4&%_M(ÊøÒ“·ëM¶·vœ4š°–F„Îß Id³ÙÞܲÇ®¤ŽÊpzK´ì´ÇË\"áÉ å‘Yk¤Š¢åF¢|"@ü  Ñ…•Èh4í8VÙ¥·~õáµÞ©ÀýÃÅâw”ô+¯mï1m>x€"ÒJÉ4ëÉ eÝ=}W’UÜüC´€@à 0Å(¶IªˆnR"@¢†€ÍîÀHû᲋õe1s!f6›ßh¸±q-Š×—È|¥©ýÒÕž×.˜8’ˆÕ Ê‹Rêîj;PDZË EåÕµð5FÏQlÜu¡—SVŠ®•Òn·GZ•¡t¤ŠB¯bJ± pô•ï¾òñ›Ÿòb­]úï½òñÉŠúeuØÁ hC‹¥ƒè€@ïŒÑh>QÙ„iÁáKK¥ ²,(5ʾÿì  cA°~/«µ¥³Ë(¥¦¶¹Âæ,iÑë4ª¼ô²Á}H— D Ò ÀŠ‹Žo.Ñ~` ¾ßA3:²ÄÂʯS 2Ÿºs‰o\ì# ícº#ìç¤é0K¤B&Á¼|\¿ð±ñ/Ù°Œaخ>ËÚEÓbÛ—h¸ZC©QöÆŽ¾î>#€DŽíw5Z}LÕˆEI$®0O¥‰âlG&ÒŽczîŒËì†P8R.j (EZ>i Z¤Õå‡Ä,È”÷;SÕÏ‚¢œ”Ïßµã†`ÝÁD‹XúN·.ž¶z~±ÁdýÉkÛKóÓZ»õfë©ù˜ˆè¿_ÛŽ0—Ú°rAcŒ°v7Ö—ÅXG-39A,f$%@a–¿ßü}/ä¨áþšX™Þ4º‹õJK7Z Â×}ƒj‚BŘ˜*3\iŽj:(û[ŸÅ”•IZ V;‰aMhòYZ¼R®épа"Ýgñ…ɿ̙zAîÒMÛ ¯ ¾©=<)O#ÿ¡²Æ÷`(û`ú­Y¥ûZÚµrëöÜðæñdöX²{Ìb—G+—d¦jTJnÝL¶y¾VdÖ»áú¥D€ŒŒÀÁ³Õg/5Þ¼°tÁÔ¼ªú¶–Î>ü½»ëÔôI™Ïܽ,U¿ãX9„d Zå^£åŽ3t%Öî€ÜÁR¸èã·-œZÁ_}÷‰ÊšÆh¦Y“³«›:2“µp+jï6ô­†ÕE0³sc+gCн þSX;¦(GPÂõp•ñ•÷ö|óßz{û±p%8Úé ì ÐÑk`íË™>¬ PEèÔ+HKz<ÙÝæ #Ž,˜ÜGr­ÎL»=7Ó7„¡ K¾Q|÷YDØ¢„ÞáØ{´(º.Á7Íà÷åW^§ 3¤çLN· õÉåX¶/B$¬oYÈVäKƒö‰-°ôì;}yåÜ)7/(53Y'¨•o~r “.Þ·zÞ+ëZØ2g°ñàéùäúŘ³ñØ…ÚN—F £Å.—б0ÿr‰…,Ž^¨]9wòªySš;z±ÎZNºË®Á"ÿÀš9XßË¥ýêí=³m´Š4~é²þÅV«Ýlwj¶ê- uåWšQ§+ë¹mâåK Ww¬î‰SŽ fÑ)‚Õ9ø0ØÁŒä*…,ðß³#Ø×Æ«0ƒ9€@ˆ“Œ ©ðFANäRi~’6®³k¾¶èÂ&^Y>ÿyöÔµYég»znÉÎh2šÿåø™›2R¦&¡š^^±ð‹Žßšñóf@â´˜-/9}¾»÷à=·ìmnŸ‘¨ÝÑÔ¢–ˆý¢_ìîËR)~¾d^©.Áât½Rq6§×V/Ååž)™Ôgwl¬©¿QDp6?>)Imuj¬Ù¡ol­jëZ5Ïv[z:©¢…Iቈõ-]Xÿ«˜¡🟺•A"/}n]¯Á¬WòÁ¿>s;΢u4Ym19ôŒ•Ÿýí?ÔbølEÇ/^AÊ{OVà]èlUÃ’™EpÙáWØå† Å éê3 BŒÃù[—ÎÀ6È‚o?sç3—Î_n„Š:z¡æû/Ü7ø³ùñåñtAäH"”å‚­ª(!!-mÁ¬GR‰In²´šìNOœæ¢0ªÚv¸'°2zÁÊ{ú0WV&Ý[Նε\ò­êz„ pM,H¢l•ö¤Rmü½9ç»8_=lPK ±ÎFÞoaøèQIá€]]טŸ• ®;!ÄœQZˆ): Á@ƒ s/‰X4x梓Ë׫7Øä0 ­Pgm^ñq<¶%_Òðî@a0×s÷­Äß3÷ÜŒÇ/r£»gåpdg/z¢âû/oªoé|¤0+^=¨M8UÔ6C6A°þðÕqðÅGÖBZé–΃ßô©…·•dœŠÔjµN§KIIIMM•«Ô[“Ñ®—ˆ.¥kŽ&u$„d(²¼û[ÚUÉÿ-›ßh4ïÔ…BzËí«,Ìý¤¡óÅ÷࿟8WÖÓ÷÷[Vü×ÂÙmfëùî(!¤y_AÆüû† qCó8æÁÌ[4/9ÙŠB¬VŠNˆk0Šà’á5@ë « 3"Ÿš•çÈùjxX÷ÍËçL>UQ÷ú–C8}3uRÜáýŽ”dtô?øô:ÝÒ’6¬[„ó¼÷då–çÐév×Ê9)‰ñ~G`R ;ÎjÖ%li†–3a:’Á~e]ãþ²j«Hp%#¾Kó™Ay7zïž¼€?Äúá鋸cÑÝÍU6„6V×îƒýëšð‡aeð7b’hÑÛ½¡¸!£Wëpµ†¿6VÜÕÛ,ðWž€g7Ç7|›Îh‡ÏSr‚Ý‹‘à#Â…ïŽ )J‰"@Ƙ@^Fòïÿíiß‹þÃcëø¯?øòý°ô` ëJKÑÅûŠDß¾dÃ- ¡¢’Ô¬7äÞÕóî^5³N8Â_"&wÀ=º‡Î]ªÙ~ä$–‹¯IÕ`mÔÑ.,ä‹ÙÇwÕtÍ‘(ÈKwr¬†¿vqƒ &t{ :MIZ•\Æzƒ‰5aHdº D º  ›2õ³£ùA 1¨0Y«ñ-*‚]÷ˆoøØÛ‡«¥£sÓ§û[äWÒ>'ö d‰âÉ\ži¹™pá¯Õ3È< ŒüІ#Clj D€„DM>œfŽœ>ç jS"kbž Bd,ˆ6¥EŸhv”bµ ¶48`1ãb©†-*ي†’"D€"àKJq«²ôõYEBŒ›÷=51÷¥Wi³^msæëTI:ŒÑÔN¬9#óÉV41ïL*5 D€Œ.t Ácª(#Q§´96çè^/ÂS÷xRû¬sê{âíÎB­2+91Þ»AÁÕšTQ„×e"0A ôèM­]}ðõÕòci,9‹¹ŽFa™*²Ùliºx¹L:íj,%£q¡ÈOSmqÌlìÜfHÆéT©‰ZÌV€ ºH©TFÔ2±Ôƒù·åŒ'2ú3úC{·þ›öaÅ:cæZ»`ÅÜâào>¶ ÇÌÉÙÓ&e_7Ö•¦ŽŸ½¾í‹¬žS’wÝÀQ€9sÞÄn÷¼‚Ì£—êa)Á:h!NfM4<ÉžÝmŽ·:E‚¸4¥8Q­Äd߉‰‰IIIø„*ÂÌ2&Ÿ%UM7啌=¬’nö®0ö—Žœ+‚8DN~F#'0ýáƒ}WÛ{VÍ/™”ºçDÅß¶Á C%ùìr°!aN¿†`òÁD|ð—E~öªŒWÉ}UÖ°cQp¶ÏdÁ$Fp¬üG`šð Ɔ¾!øÍ¨äòÒTmmgß”VCF¯¥I§ìVK1‹Qf; Yòx4Vg²Á–b°J\‰0N'ê2˜…Øœ–DذJ¤ŠPvREa¸( "{ð4G¡ð©Š[õ7<ÙIŒÁš$éñržIŒ•ŽçR}kCkfY|äÖÅ8‚)ª?Ü{ZïçËÁB0a®jÜKgM~lýùõÆ]˜ïK‚`öD,{ë’ÿóÚVDÜ~ä"ÄSS[Ïå†Ö‚¬Xž¾ûÅ{·<·ùÀYM0ùõçîXÊO“$ùBá†N3L hãÍnW§Á$±95-z»XØ/ïVIrq¸ä&R[z…$\ òeñÝÖ*±‹„(Ž ŒCè/ÃtØ8XkL}|ýÒŸüeË¢“æ–äC a¦l»ÓµváT,‹uР„ ¹¶8ûÖ'Gc¸×Ì÷*‚•=Dè3²X,¢>5˜Žâ­V£ÕÞëpK»Í9Ýf‡HmdKŒr‰I& ‘9\"‰F[¢É9Ò¬•צŽÖÄHðŸÕÐ3ì,ׂ8™À£ b¡RÂM_ Q=]PBlÃ>s²Ž4I„ê#Uä{Ó> ×à™Ž-#Q AYmË’…×ÎM¤=”8Þ-V‹îñp3³¾0ß2BÓ@âÜóü™“s¦fî?]uª¢váôI³~ù¬y¥ùðF‚U –¤Œd-jÕ ^@ç¹»a7Ú~„[›â¾5ó0; :Ô^~oOem³T"ñ½Jˆû£ëBæxU„!úH"BÁd2I¥µÝdvºÍNÜ`sé¹ÕìÝ‚8«Dd‘ˆði“ˆ ˜0ÑûÃM« æ?„"‘:ÝR§ ŸJ» £ÜEnwÔÜÒbqq½V±ËS—¢v„µÛÚ+­ÏšÕc– âR$q(÷²œŠCÞÄBD(À‚†¬ÇGI¡¡~`BÇ\ˆ †°A!ÁDQÖ`Æo¤Šx´CˆÀ5L`r5x’èTâý§/aq{<ð®…˜{ð›AÙA¼SÍq`ƽè,¬ŽÐa)Öг„ÅËHyÍÕ…Ó8ù 1ôÚLJ–Ï‚•@ðUÄzTãð BG» HØöÕïgÙJÀ,à•Çí$©¢qCO&N€=ãðîW¤8Õ`8z¡vÙ,ÎB0¡6”º[ož—«<ÊÁ$ÒŠWÖƒ9ô\Á«Æž“åµCž›žˆž²®>ã´B¬+*ËNK¡b|·¡µ¥N”Ç8€k{"¡Ø¬A§B"hë3C‹`xW(C¿ðК?mÚ·÷dÒvÁj¯ÌØsÿšyð¶þÙëŸà8$4MucûàkÁ“zú¤,è§ÝgœZ°€Ú-‹§ï¼oY"pŸTQV e‰Œ?<éðò‰WB¼ãᯠQZÞfûóG‡>÷² "Œ ‰P^™À]ÈÀh€ ÈðÕÿjóGÆfg ]À> U}îs—VÎ+ ñêi‰ñß~æ.x—ÃÜ)ƒ¥^Y‚S ³~úG:z j…L­”ã K¿ÿ·§ÙYŒ>ÃÛÿê#·p?qÄ}ê®~gmœzàæùw,ŸÕc0%kÕ0àÈäÜ4>wÄŸ(;ª$]«ôÚJ86#Njô"B70á‚O&Œ „ØÃv hxmÄ„ÓFPEL±¢!)ì@aƒÔ`²ŸØG0¤+Û˜‹·z@fõ+-X¡¸?̨pm2îÖæ>9“»>Ù%ØUX¶Ñ† û( ÆÂûFx¦°ƒp Ûµ‹Eö©¢È®ÊMì!‹OlhðÎß“ßð8ƒ¥Æoø¨UÊ<­Noûý{ûîX>sñŒöôäÇÒÞÓÑq+‘4Ε/@ÙA@L|KŠÙ™1 Ž ïñ1ØÇë¹·OO„ÆP)²Ã³|Δ°T |¥ywi¾ H9ÝëxÄn‡‰žÁga|ÊqîØáÝPY({‚\¨IX›~SÃMÂ$ì(èW‚YJÈoÃA¶ù #dЇû¡z7¤ƒ šRcSL¡‡lQ\Liù¦Æ‰¬ÏÚŸþ@Úý†¤Æ§É„¾?æ7|EØý"²ãøÄñ(C|E“*âQИÐà‰iú|ๆ§!ä€F­2ªÕpÈqê[ÌÎöŸ;töòйS¦d n>}Sˆº}˜I0âìÀéK]z³ZhÏPÆÅ«ã5j5€h€‰o¡Ðw“¢Mò=26ûhlÐ"‰ÄÌCœ,sÕ÷÷ŸªZ½ tl29WA©;{³2PÂ5Ô,ŒÀidšÆlÐ+Cnì,S0LÄ .Rð‘ûÀÝÈoH…Æ‚-C½¼tœ*‚!ŠF¸’ei")ì°X,c~))îy|B1çë!„À¬,ˆË¢³¯|‚ìxÔ}’*Šº*£ pàd*¹¤©Eïëž‚§œ#ñZˆµpÀÓ.¡q&ƒÕÔkp~¸÷즽gTòµB©…3Oã‘–ÙbÃT}&+š™À™.¶iäb ›Á`â„ €h€ Ÿ;ÎEoJÖ%Œ}3€+¢9Dƒ%÷z¿"«*‹íÝ]Çó3“1q"ŸÃ˜ß©½ÚRëdžäxX/¸Xƾ:FÆ™U"2 ÙuÂÄ ¯„ø¯L±OV4|bc †ßáïLèˆÈh#¦‡˜$âíOÀ.ç›mo’\šÈ>¡±°ƒOäßÀbCDñM!föIÅLURAˆÀ ð}&iT‚–.?÷<þð„…¥Än×Â9Á;/»f¹Ýfw˜-‘Ãdí0õ¹¹ùJ¢{ÆyDqÀ¥¸Ð¶¢}Å0b.!)I—¨Ó‚8€†o!9w ('‹aô=5û~ˆÑxy½¾Ð¯9òäék“Õùë;áÙ3A„$Ê+¸ “1°‹åÄ¿üàûqó;!_q A…øfÂްOßSØg·>ýŽã+S622Ðǹoód’ Yâ|jØAt¶!?¾SB8‚³ƒ/KG>ó#¥‚QYˆ†žƒìI‡7m\âçž‚' §TJpŒí>:Î`%ÂT‚ ÂÎýW,¾…ÞìØ é[–à÷q£bƒŽü*™ý‰}r¿Õ b—¿{Ùñ‰¸C~Ÿ¨Iª(ª«2OB"ÀhHà4€h†FYÕÑççž‚³p*ÀÓWò†ç¾pä´`š¼Œx)øŽh )Wc™µh Ð@ýÀð_"tœÁJI¤Ñ`ÄÿjM ÔÑk¸÷¦ÅàÃ^£G#ÛÈ’eÙ“Šèã㯂ºàú7XM‚sù‚óˆÃéM]NéÛ;ŽaZ †Ÿ5%7Äáúüå"d½–0Ñí:z±£×¨ز5B·€Â¯‹yž بÜ$8Ë+E&‡p„ýœGdw޳op€‰p„TÑD¨e*#šž}LAaÓª• }F?÷„A«qŽè±QU0qžFN‡×ŒÄž·Q)ŒP.lD@!£ßA˺̸õ ¼óñâ ÑëÎr"/5)35ÜÑ÷,,¼;J‰ðjŸÕwA:ä WW«•Þé‘9yŠ+ ͳÝfèµoüä(VÓj: $¿–ûðfkLRCÁp»a`¯Á‚‚ÈâpüÒªdÚ„N¹ja-â‘ßìLì|‹xŸ»Ñ}nãÀ'ìÙÏÜC–œLXhÑ9¡£T¢ dkUWº~î)x’z…§`JG¿} ï";úÖ ŒØÛçpï ‹—5øDѼž:Œ!ÓrÎW‡}Ü?ß·-áÜYÞÞ©–KO/7ÐCÌÑ+ .ͶD%æÕ³ù.HK€×²…þMnäÚÂNb6+»Ódu‹ízsK_maTJV/W¯ ó:~¹µ·Jä–K1ìTDèå„UÏ[ŸéâDÌ ²€ÝèÝ{3eRE³Þ©ÔD ŸZtô±+Í‹q¼YvG“Áú?¯m{píÞ= 3„ëAÃK¹J¥Àh_®ÿ¬‚8ÎV„£W1[ʈévØ”¼èŽaEæïÞE)•,*”n 7zªˆé!–7ø5©%¿épiÌ.éñpËlÁà… cµÁ$³˜­¡Íõor5Ãù~ñ¥ˆÆ~ÜMÈ ‡w9ì_0æÁJI§"@ð«®²"o»h„?ÑòLªh¢Õ8•—|†€×ÞÀ­^‰­ù9ßrã<ÍF+ÜS>=^¾vñ4Þ=Ñ&¡éå,F 9³±ñ,QÝîˆ6Lÿ40ì ƒ5àÎR_¢d•|VanjrˆlEÀò¦aý‚ÄÑÞÃŽ…©Š¸½ÙwA:dÕ¡sŽ œ­‹ÍºiTÂë‹ó4r [ ò€SFØÂš¯±Kl vÐË ÏÉ>8~q>æCj‰”D€€`¾yŠüì|sKû‘C€TQäÔ儌ÎÀ ǤDñ°.°¾040ÙB¡Ábë1ÜS”:Ò×=…oh¯5·QÚî´¦¬Yem0« è¯; ·Êš\•TTš¦ËJKÁ–”Ä©"p½Ñ«6&‰p ¬Â†®1µL¤Ùü¤C†á0èÌ;VžF*Øü0§1«P&‹Ék55z9wÊ|¥ tÐ…åÜMÜ|<èå䦄ÄqŒ¿x$/`Çg’v"“À(þž#³À”+"@ü é…ÁfkÃï½6!‰Ä¤¶Ûap09\v‹¹Ýlö[;É/XúÚovÀÊäÐBA†R¢S)0Ë5”ôPrr2\|ÇÀP„ÆÞëíÄù€ã Mni´8ý¤cÂõˆ¹÷¸ªT*0™1 E.ÌS3`+BíDµ*⺡‹¸é¼1w×6¸‹“ÝÁ.`7 †cé¾¥²„N€TQè )"õð¶÷oÖjzWÎzd0Ч†‰q5Üðûk«D}iƒ+¤6FR=5ð»‚Kbb"$öÜ‚KìBÁ{ËwA:\¢¿_ ÓJ*„Ÿê0·Ûã/H‡¬2¥€ðÈ4.g%ЉŠcuÁu¡ùô'²ƒ¾dQÞ °ÃÌOÚ΋6"àG€T‘úJ&"40\/Œ‚›ÐÛ ÷JÃÊÛè‰a1J±:4 5 2`‚‚­ŸØÇC°°Ü.,vQŒ/÷]®¿:|V£ƒ(ÕmƼDƒ¤ã³í]=*—ÛXù°dx,á!³Ò±O¿ Œ`»ÜÌl>e¿ÔèëD&@ªh"×>•\#€‚ #Ö C!¡ù·pórkns†‡¨~­Áí±ÆŸ@Áh@çÙë¯ÓŽR›ª”Išz®-Hçµù¯F‰“l³[œvƒÁc ÒWE×Bl»n½)-y–õ½–oÚ‹T¤Š"µf(_D`Ì 0aÄúŒ¼ÃïUX]’ ð†$b¶"d*z­AåU³A,Â\äuìåÜYðǃL*ø`¸(ÛÕ  |¤Ã!Ë­F'Ú±èŠÙ厳xĘœ(6¤ ž 9²ì°¤EIaÞ^‹ÂO¤Š&B-S‰À ð'Øð{) $^#Q¿$Šy=䋉i&Œ¼6#îƒô –}ßkaíµLì» òW!LPÍ´)®È¯FÇùT;]RÎyÈÁjgâÔ î°;VV•…ÓX a©AJ$f*Š™ª¤‚°ðiiÄheÙÆRÇ~Ø.Á ñí%CÁ>G5¿¾ò+]-¿Üeô]‚ cÐ|„ÄX3|±ÕèF€wàFå:!ƒ]À®Çp×ÊåÀ8‚+R”˜'@ª(櫘 HFN` ÔÀÈ3C1™$ò6í\'Z.‹—˜}¤CEàl ¯F7‚Êä%$ã -`Wœ—••–ŠX$ŒF€=æ£*Šù*¦"éÐÀ£‘†7ü‡°a']%m0Ø|¤F,dL­F7‚ê ÄbܼS:¹€Ý®•bõüÙ£ê/?‚âP”È!@ª(rê‚rBˆÀÄ%ÀT¹0ì#Ý0ú/ÍáhµØ}¤cˆõ ÅÒjt#¨u^ÝÐv0­š3]í]§ÀGp]ŠóHÅ|S‰ˆLAaB£ÑU„ ÒÝ–N›ËoA:¨"6ŽÍî3«Ñ ’ °±þGXŒÀ;ÈRó]À.U£\XZ”’”ÈÌV4‚+R”˜'@ª(櫘 Hˆ@`Z 6&Šä—é…{uŠÀ¦v¸ôú¡¤ãá¯yÁO wx®F¦Ð„ ò~ãä«iÿì$¢’TmNFšN§^@†¬p˜>‰€/RE¾4hŸ"0núç%Òh`%rbÀ½Ë…¬ ¥ í2|u{,.Ãdj3™° m q˜z!D <ðCÃÏ ?:üô“"¥‘HEdµP¦ˆ@¹ãÖ¼¬Œß¼ñ6 £ hQ"0rø‰á‡†Ÿ~t#O…bFM ÕDy$C0˜Ì¿ýÛ;õÍ-Þ¶vå‚yXsèpt”€/:Î`%‚$úòã4*划¡HQC€TQÔTe” IÀétmܲ}ÿ‰Óiɉ7/Y8»´X¯2$$D x„g»oëìFǬDXZ-øè2J *ŠÒŠ£lÏhliÝ´soYuÛíIÒ&@)åò8Z–à3è ‚€'³W³y‰0ZѤ{oYE¾DA€‹‘ ¤Šb¤"©DLËŪê«íx¦cIZ+î "p£°È R!Ç{¦jļD4ÿFF{xREÑ^ƒ”"@ˆ D <È73<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú *Šö¤ü"@ˆ á!@ª(<)"@ˆ D Ú ˆ#³.ƒÁc2 dR¡V'"3“”«("àñ¸íæ·Û!‘iÅRu圲z]·«×fs{< R™\¡Ï´ë–‚" "è â¶Z;w˜>Ým=wÖÝ×Çèd2Yi©rùMš;理F2ÊC0õT7]ükgí'úöó·ƒå\¦J×e¯È(Ù6ù¡0‚~Q6²ZÙÝõÑ•K‡››®ôõ¸<–¥ •zazæíù“–gæÐ U$TåDgài2ŽùFôïþ½û÷¿s÷ö › ±8þ¿ô‘F3l:AX-•{ÿ©¹ü͸¸þörà̵ÿñy%«þ;£ä¡k‡h/Ôëû~|âðþ« 2[¤Õ}{ÁÒÅÙÂÐ)"@ˆ€ñWE.½¾í_þÉrì¨_Άü*ÎÈLÿŸÿ••” y–F £nçÙsXº‚’9õñ·ýA$–˜ÂŒ;ÍW.ïè~‹ÓLNžž:ó›ó ©>X†¸¸qVE®¾¾æçŸ±×Ô_¥2ó7¿“Ïœ| 9¡´^Útæ£ wP­&#“˜»jÁƒ[Ebù„…}«²ì?¼¡œ¯ÏŸô“7“0º!h˜LXã9Íãrµ~ó7$‰PO³¹åë_s´´LØ:£‚ Ð×zúìÇÞ$BjÝ {/l{6@²t*ìojø¯”DÈöÖºš_ž9 ù§<"ùÆSõ¾öWë™Ó#`_ìŽïÿÇ"R”Ø&àv9În~Üí² ˜Ío5WlADŠ26úl¶=´gX±€™øÃų§Úè=* #:Iˆ€—À¸©"WooÏ_q-XNœ0íß7dt—Ëm±ÚìŽþG|‡Ã‰ã8Ë¡Ñ `³Û.×h¤|Ý4νlꮺn°áÀ;ºj¸³üñö®î²Ë5=½ü‘w¸›58/™/4JÑǦÆ_½x¦Çfq~zòH€¸(ìûBÊét ™®‚çSðéDHHŒ‰AÎ#$3” "0zDßûÞ÷F/õ)÷m|ÓrèP€×=åêîÒÜy÷à`/Uÿàׯœ.¯\½xïÙÿ~õ/omÞž•ž’™šâ{Èãÿ÷ÚÆ®Þ¾âÂü Ç,\· Ÿ1¨q«ÓùÒ]÷È_iÚ-æ¥Ù·ÏgÛwçÇ¿ÿóÛ[w Nñ·mß!2d"'ΗýñÝMk/¸cÕ •Ra4›?Þ½ÿ7o¼ýÌ÷,š=cÈ(‘y0\·ÍX–î`s£iõ÷F3ðI]ÍœÔôáb-Ÿ7ûÎÕ+pÖæpT^©{gË©D‚ƒÃ…¿îñé“‹ÒCVÌ×½J€ÑXÑŠC§ˆÀØUä6™ì—.…ZBÇzöŒzÍ̓Óå¯b'/”ñªèäÅŠ)ùyõW¯ùL抚Z¼NMÎËÉLME¤Su¥.Q›€ œR+3K¦ÈeR–~s{ž•¥rr~®6¾Î$ô|\¼\m·;J‹ ÑLâ-*%QÇÂ#…«míx,ÂÀÞ®¸7¢ ¼æJNzÚ¤ÜlH7ôŨ”Ê9¥ÅI]´uváBx"£ îÕ]6½zCvzÚ…ªËf›mZQajR"¬ÙåÕœ8[^5³d²¯¥'@) ïÁ“©(/77“k!›Í^ßÜšßÒ¦7uññ™iœ-íRm=>§äá@‡{»­®o¼\× “J'Y§ÅÙÎîžÎÞ¾‚ì¬SËgOÆ~«»ÚŒœÏ,žÂðòÇGc§»qèÉ—n‰÷¶ï^0cÚ†õëØUüÑ»nëÑ>صwþŒi¨S­F““‘ÆÎ6µ¶á+T€¯ƒ+4±Á·ekGWkg×ìÒ),e@ÕÌ(.ÂÂŽà3@û]Ý÷¶AåâQZT ”ËQÀs—pK°¸ª¶>^­ÊHIF7ЙòJdÇçM/ewr€üRŸ ‡WÐÉök?¿#“JñÛg_QpXaAªw{¼Z- /Õ5,™3ü~f‹?öâÂ<þ7x¡ª¸ÄboVA')˜-–ÁÁÁŠÁWd¹ÂÓÖÊY%SªjëšÛ;sÒSK¼7;ë—ŽoEãq¾¼i“'!$ê?öY¥Sð*{ØùÊË8ާÜ훓)ù¹ìBøDoÚ¹ÊËx:dgòi‡ÄñQEކü°B'è¨çZîÁ›ÛíFöñ§ûï½e5;‹gÜM æÔ6]e_ñùÕ_ß‚.IÒi7z dRþóîéwïà +- ¢Üœ—ž{Çßüø“çϡe‚CÀŸÞýðù‡ïŸ;­ÏŸýéu£Ù’š¨{ã£mɺ„Ù¥Å÷¬]åv{^yû=ˆH„'Îà‰óâ“à‰‰Ä‘T[gwFJÒ[²hÖô‹—jò³3ëššO—U¼ðèƒ8‹«¼þá–I¹9xfmܲýK=TR˜þš}ÇNÁ†‡uuCã;>ýΗŸ‰DL¡Ñ*ÌÉæŸÈJKÿâ¯o‚|aNÖGŸî¿iÁÜû×­ÁÜj·!xÀAáÉûÂc\N^}çLøÓù:öw:†(TѦ]{‘X§Œ&Ó;÷|á‘fL)ÌOž@©¢(/áÙv¦¼êOïnznÃ}c ‰pEcEYŽsX»í–n©Âßy¥¡©»O¿nùb>$Û¹yÉ‚ŸýéÖÎÎS+ZÚ;¿ýÂçÙñ×7mœŸU4d…GlÈÛÂý·{ç‡_ÿrZ2—«C§Îî;q·ŸoN†»o_­5Û —¶Æ'ï½cÁÌi¸^Þø,aÜy+Ú¼—ßzï™ïÖÅkþ÷Oo8œ.T+n³­ûþóóOÃN6\þG¯ÆkûÂàÅ…¹}¡ØÇÝÞÒÞ{a6ï9€Ÿ0Ô$TÑàŸ@ia~³° Þ²l£§éÿ^ßøíž¹RÝtðä™ï¾øEÈ&`lî蘜—‹N|PÅk B÷ ð»"ŸO³ÕŠ:‚ 6™-¨£¿oÛ ]¾zÑü!Óã+zÚ䢷·nÿÕwþ ¯^»ß}äÄ?=ÿê´¦¡éµM›ÿ÷Ûß´ÚlCV´oNxU„˽¾i3âþ㳟ãóF;D fŒ*rôa!èÖ ~ðè5ãßÛ›Ö6gZ \Øua_A wߺ5ø ¯šWßyßã¹—ùôôé¡9rž)¿øË›h“29tÉ7>ÿ8T¿úöû1h–¶<‚§çþá¼µãùÿýîOPEpøÌ9`~ôÒ‹• ~ôû?ï>|j §Ú_zϦ×7m9pòÌ¿}ùYdïäÅr(-(9½Éùõä½w²WÒvï{ó£m?øú—/ëßzîI˜©ðüú_þòšæÁÛÖþð7¯~þÁ{ð®0¾ÛàR¨Š·6’–¥¿ر¾ÿ¯@™¡¼x¹fåÂ¹ËæÍ†"ܸ…CãÜÒñ~ KzÖÐ Þ·n5ìÛÐ.~áóUÑùú[~ü­¯aÿj[ÇC·­]³d!ßGyª¬â/ï}Ìd“oÆFo?"ß\!Áª·¤&ù»¥¥pGºzúPe?ûãëÀ®KˆGw-ô÷S÷ßÙk0 W¡ƒ‰!!oKÜu2à¹~å2„üB­ öw\ã¸÷†¼ºïmÝ{TìO‰ ñ0áèz¶;ìÅùÛöBKüݯ~w,l ?zùO¨tÜ038ÿ£Zãz{ü|1ñ#VL“‡înÆ/bÿ nH,4~ï(88ã+6üÜ r`Hî'°pÆ4<˜*:Á¹ú%ããJc‹¾óÐQü´üÒ×`~c¿»™qœ* ð à¯ÈRðýÄ;ØÃOp¼¯mÚr¦¬ªh¸tøŠ …ïlÝüÀRUUÛÀ*ª¨²¦¯:øÍnÙ{t¸Šæs‚»…ûëãq÷ÒsOÀ„æ›1Ú'±A`|T‘ \þzç#—Éðª';.”ãqàkMÁ;T,áø©£s BŠÍ;gj1$j]BøììîÍËÊøÑ7¿Šg%yTÛÔÌ:JÊ/_™7­”udÀ1fjvOœ«¨Â׿¶Ž¸8üÅÁìÄì#K¬§ }OIÚä ш&=#•5uÈ0Ýã8^ChY±v’;(¼ÅY#c:/…IDAT¯ÃmƒK!ICçݵæ&´‚,ºóðÈËJ»¶ºléÐm°yÔ44¢Gï¸È9 JS'¢ÍP+UL!XòÑFv÷rÏJ¥B¾zÉ^¡u/«®¹uÅRö¶=\&Ã{\(ºÖ£JÊC®ŒÃ&n¾€|úè<žL&Å›4ú_N•U®]ºí"nTÓѳ†«P?b,ÁánK4ϧ/rªVLýgº‡Ï¿3¸Æ[::‡»: †‡]‡á+º‡Ö¯Zþ·¶¡#w$nTØ;§Mbw,úƒp{TTײ¸~ùí‡6ž€/¯H0¬g!~kÇÏ—!$'~ËÞv3.‹8wZé@wó•!KæÎÜûû?÷èõè€ÆÓféœYü±s®òÒô)Eø]c?7<ØÙ þоé°ý¥sû/ÌÎÄë H‡EAç ž¨âìŒ4¼ðÜ¿n5ú¿îXµ¼¢¦nÕ¢y ¢ýr‚Wçß¿òI"Æ–>cÀø¨"qJxFNhðyàÖ›aŒñ†#ïlÝ ó R’ú=Xí*ì.l`'—ââÞøp+vChêxJxáðF“™šÌRèé3˜¬Ö­û±¯øDÏÛ畞¼ì)ɇÁ^÷ÑÙ´ãàQþ ^åÝÞ©2N¨± aðÆ6ðmèÿ—¢Ï`DPôÙA ²8È|§|ã£s¢çR]= ç]„PEèË›”—¥èõmº&¡Ð."®Å;Æ8s¼¸ö Žçï™ÓÑ׆¦1{zÊÔaqqHUC8ä¦%%y{g7sºâ¡áÕû©I:TèâÙ3`0𪢲óçàx€ õ#Æî¶\4kú2 …aׄ’æo'>Ø\ã®ÎGœ>eÌ–¸7Ð!òìC÷î?~¢&¨%„éÕ³Æ+à+~/[¿ÍÆ/ÿ£]ã©JNR„¸%Ëf¸ÆÎÃw¬òèóbLJû à ý›°Ü@ý4¶¶~õ‰‡}ÓFÞá ÇÓžü}Óaûx/b;x@±ç@€tøè¨P(!h»‚œ,ôìc€^Ã0Ž9¨h¿œ€@~VÆG»÷åsøÄi‡ÄqRE™Y•Êc2…ˆRV\ üøá6÷u˜mð&͇ģäýퟮ\0÷ï8Öì:Ľ.·Á8„Þ®ï¾ø6¤ÿÏï}„n,†$ª»ÚrÓ‚þxèS`v¼bÆkT_{òQv¦ëP3  6ÌÑû‹>ÀzÄ`ª:ÑÆ÷¿°—½ #·°v §Œ×Ä‹fº·È7<ÖÑÂáºð“€¿úþ`~g&”ëÈÙóð*`~BðDAD½ê¼µøtî»eÕÊ…óÿó·@ó—âOÞN|êìÐW'• ¹ ì…ÐpŸúÜ=ë}¯òéÑ“P®°àà’Ù3Ñà # D!ŽÜP…¸-ÑâÂø„¾3¨¢Ås‚ï6ÜÕM+_ä7-ÌE¨GJ ÏkXƒp(¼‘}“~›vî)¿œ\R‡JDçþŽƒG ÕØ»Yðýâ“ë&ü´ñ–8ÆÔ@Ù¢sD : kOÕâD"å‡ÄH¯$ÔhdS§ˆí'5yãæíxÖã5—érÃcÆ¡VqÓó _lûÃØq{Ü|¿›Í†®¦TðJ¡:ì ^Dç+/¡»ʘã„ïÕ‚—FÕ•zf܆¢úåko!–_šC~E¿\*Û~à²CÔm…cGWe6ÂÅh2™šßAF†!ï0Žœ8irØœ–8GiÆ =/L¹ðMAKÌ,pÕÂh5¢£aЫÍÖë, ´ l˜œ™!í†*4ðm¹xöt8Y7¶´.œèž÷ÍüpW÷»mP¹{,)ÌC\Ÿ9V£ðƒÎ```w2z^°³l ûÆ÷BØ—Œr/ϼæÅïwéà¿.ËäüCÙü0;úá^½l®¿:‡zÀ«|•ð³ÂïŽYm‘?(üŠ0\:¾ ïÀHl¤ … úö;>ƒ“‡LjÁŒ©x§AWšïYôõÜ}óMhÞ0c›Ýá|xý-í]=ð§ö Æïcì Ãçç¿Q*h0î½eâî9zbí²E˜|ïW¯½%§àqƒ¶±Ðn¡õúùŸÿ†—oØ{nZ8oÍâù|jvþù‡ïûãß?D+‡غŸyp>¯ !ã?~ù»o=÷óçO ¹óØ]·c Ë¿ýü·2‘=&lh=žÚðì†GÜ3s3Òá…PìA†ÁÛ -­ÌHg‹'î½®'FPEèKzú»†¼;ˆ,¡# ãH©„C?%S¥¦®o¯ÙJRÙÓŸ.:iC£/²·„!¬,ÿúÂ3p­å£Àç|˜¡o¨Bß–üÿζ˜ãoùüåï wu¿ÛVüXc‰ù\nÁHü‹q'ÃìÔ§7à†GóࢣWã¥IÉźĪžîW|J"®/èC8à'óÏäü¼¦–¶™Ÿœ©aæCxƒá ƒû'I—ÀëÚ?(ür8\:~ „ŒôŠèx¦á­†ŸGãF+úî›WÂ7äç¸Û/3ô•D;ë{¨ŒR anizìû¥RiÎû›$#w%Á* ð]MONFÛ‹䋟ÝÛ¯àðÒ˜“ÇÙÔDx¹‡›‚D,AYà‹Š1hw¬^da0²#p²~WÁWd]~xÏósa’ÁäCpW<(iÈðÈ*lZ@hÑ™Ÿ8 "ßéU†ŒËbn‚–Žx ±^€!ÇþToó±Ão,ñuS‹îžÿ¦ÀÑV"LS¥7¬ÀÙÙªÐánKˆ°—~üsÌÁA‚¹nà«ÛÀ•³ÀvÈÌ¥Á_:¼!·×_ùÇ};Gœæã%Óÿuá²G÷8²Ÿl´°±£ßOuÄ ß,a¸t¢®¢ýÊE_‰À7U„rZ/\¸úìÓq#Z3+ñ+/êžyvŒaù]îÈ™óèáúÊãCú`X2¼Vø/1Ùä’¾Ž Û¿ØxîÕ\N$U¯xúœR[0‚¸£Šã>ûý¯½àצŽö¥#*ý/îÚŠI®G¥T…rÓÝü”GE!D`‚OUĽoþ­ëg?½QÖÊ+Òÿ÷—‚0 ֽѫóá¹ÙÌ>Ü‚éÝà|œ¨}ôÎÛà–ÁŸ¥q!àr˜¾µ«ÜàÕsïý{ú”ûo0ÖG'i¼Juÿ­k0]ç]2"/Ómµ<ºõƒWEð›T(úÓº;¬õ|R’˜'0Ϊ|{þøj÷oüÃκ¡]t¥ª´ü¹_ÍŸ÷u±4ØQ]D5rìkªù™smCf)K­yjêÌ SJ‡[ßcÈXt")ªˆÕ„Çn7Ÿ8n;wÎÑØà6›1ÐLœž.+ª\²T¤zú8ªB"˜€ÅÐÔY»]ßvÖj¼êvÙ%r*qJbö ü †Y+p‚t6r`Á×ÃÍM—zº:¬fLC¯•É ´ Ò2g&§Nd·ôÈ© Ê ˆ:‘¥Š¢e˜"@ˆˆã3·uÌࣂ"@ˆ 1C€TQÌT%„"@ˆ‰©¢ðQd"@ˆ D f*Š™ª¤‚"@ˆ ! U>ŠLˆ D€Ä RE1S•T"@ˆ D $¤ŠBÂG‘‰ D€˜!@ª(fª’ Bˆ D€„D€TQHø(2 D€"3HÅLURAˆ D€* E&D€"@b†©¢˜©J* D€"RE!á£ÈD€"@ˆ@Ì U3UI!D€"@B"@ª($|™"@ˆˆ¤Šb¦*© D€"@ˆ@HH…„""@ˆ 1C€TQÌT%„"@ˆ‰©¢ðQd"@ˆ D f*Š™ª¤‚"@ˆ ! U>ŠLˆ D€Ä RE1S•T"@ˆ D $¤ŠBÂG‘‰ D€˜!@ª(fª’ Bˆ D€„D€TQHø(2 D€"3HÅLURAˆ D€* E&D€"@b†©¢˜©J* D€"RE!á£ÈD€"@ˆ@Ì U3UI!D€"@B"@ª($|™"@ˆˆ¤Šb¦*© D€"@ˆ@HH…„""@ˆ 1C€TQÌT%„"@ˆ‰©¢ðQd"@ˆ D f*Š™ª¤‚"@ˆ ! U>ŠLˆ D€Ä RE1S•T"@ˆ D $¤ŠBÂG‘‰ D€˜!@ª(fª’ Bˆ D€„D€TQHø(2 D€"3þ?ê‘ ÂpùIEND®B`‚networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-compute1.svg0000666000175100017510000006163213245511164026033 0ustar zuulzuul00000000000000 Produced by OmniGraffle 7.4.1 2017-08-04 08:50:13 +0000 Canvas 1 Layer 1 DHCP Namespace qdhcp Metadata Agent DHCP Namespace qdhcp Metadata Agent Compute node Network components Overlay network Provider network Open vSwitch Provider Bridge br-provider Interface 3 Internet Geneve T unnels Interface 2 Integration Bridge br-int networking-ovn- metadata-agent MET ADA T A Namespace ovn-meta haproxy networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-services.graffle0000666000175100017510000001241713245511164026725 0ustar zuulzuul00000000000000‹í]mw¢È¶þ|ò+<ýñNG)Þ™éé³P£Q£1Ñ•µîB¬ cœYýßï.ÁtÒsÓÓ]ôꈰwÕ®ª½w=øé?O «ôˆ=ßtì?? 2ó¡„mÝ™š¶ñç‡ëAãXþðŸÏGŸþ]¿¨ F—'%×2ý ty]=kÕJŽ+Õu-\©ÔõÒåY«?(A•ÊI÷CéÃ,Üß+•õz]ÖˆTYwDЯ\zŽ‹½`s…ƒByL?@5ÛÒ3æÀÑ©©ŸþõiŽ7ŸIu¦®p~¸ûT!Çá´æyÙù×'?ðÀþÏP]ÙYئá9+·|{MO»¿·°ô©‰¤¤‘¤”¶° Çò\JäS%.:4¡æá°þºà¤òH˜ex̰ÇH)±èw–ùQJ¿1°¥‹K q¼}ýs-fx]êh¾¯­5OÛ×k˜lÜ\ÅÚ*pöe¡µî¬îè«¶ƒýÞ2íØûŒøO•x?Ö[™SìŸ9úO÷ëé^äj ¥‡¦oN¬œY£“þ¾|k¡¸æ¬ N/oMΘ3Óž*=o̹fØ8x¼fôg”²ŽTID î#—;0äÒ1#3|‰Q~GÒïÂ!ß„ócn+l•TÛ œÒ%¶´“WÄd O5{já·°ëøåýqᙆi¿Xü†² ž<÷_ _‚–}ï$¢Q\Gvö«N8‹sÍ b™TT'Eß[ޤ#89ãL`Wva[éã™9¶fݦŽèsA$PPQU þšž¶­Ñ bðmÕ·N•ïU{­þÉL]\3šÜ´-ݨÁ÷éÅ5Ï&§CK…óµa0`ÛËñM—Q[7'j 2'k]k>oþ¾þI÷ ùùò/5HÊ}ó/ü\ñ~x¾ ô¿E„ÊJ¸1¼È±‚(,I ûå+•·¸Â$çcð>pðÿºq¹2Ù›ÌÀqߢøO•ôÌz…µé…mm¾ÉEé2,9èhªÖóŸiìµì)~JŒß¥{&îc5˜ÉÔéÃÊ Ä à•`à^'íÔX¹@¯ªés ìi8QšzJ1iI$J¤üÝùÓÿÍ|,1_>–þ8±Ìˆ¢ÂŠð'qÂÇ+seɈ‘%^ádE¾d¢!*½fÁl_Px„Vú3ˆK¿\ÓìGÍ/0:W^«ž.,éƒlDClvy>Ûð­€?Ó¦Î:%‘ÙÊÔ=m¶ÿ_žž¨ä*;Îâ…>¹íûº ó/QþvšL)ÇîzC6wúߦ•IÈ;úÛ3¥’€LZШ¨ kµ°UË4ìWiô]M‡šR:€¬ÏDr¸“ˆ×MßÜÓ×5« ½@`+é‹ÒŸ¥d?߸((|B,R…ì²eÞÃs± n–åÊ '1Œ³™ÂÈâÇ’"–9IR)<‹8YIôËB™áNT™C¬"ËÜÇd 0H†ÿ"Ç¡ýè/ÿ¤þ0ð‹ã<"NQGŒÃŽhäe¶B“Œ@bSæ9Y`ĽÚ#%ã #!I*Vò)Ö%æjÊdŒ]³ ³Á…‹í¾fûÇ}¼0'Ž5mš•ëº(óeÂ*ñK9å–Eé*“[SÉUÚË®:’QIùâ³) åë‚á¾7-ëk£w‰=©B§ˆºˆ)C( EPv[¦ÓxG¢­ˆ¼Ä¤´…Bmï¶Â"AÜi‹…Ú>d¼_BŒ`=c’wͬ›å.?ulÛÀëgÅ.ž%Ã+ÊMˆo0‚â$Vd’}Õò2Ë ìn äW §ˆ dЯxÏ?Á¸—<{WÚÔ\ù{Uí{áž‹ý~ ž½«A£¾7rZ Å_žc'‘ȬÚ’Ò`@ÌÙCf|s5 û“…®Æ"­^k`u/G ×q=u6îœDÇ‘H¤QµZ½jg3¼Îy£Ç*+8þ‰LšOÀ‚ÜÁø¶Íj7]«¶P؉‰&¬ÀD"úBñ'ÍáfæôjÚõ¼ÚŠΡ¢Ø\Ðd´e5ºm j»¢ÞÌës§cÓ‹Häšé>NšÊfæ†Ú§ ]Ÿ°O@Á®ã©uÔ;/àŸsa¸ËÑoûÐcêb=¶euj\ÍfXø¬ Ä­:Ó þ¯c[]ß”ÁPžSX¢zçjÞô.xDþBqá‡(“–åÙ!Š€ùàùPÍlµÉ‡ˆBQR®éÙZ°ò4ën©“¹Zƒí{M9:"Ý2ŽAbéN¿gKÃn©=æ9–…½»£>öMßý`+Í­uΣ}¬'Ö}yûý‡¤GùXB!Ûb“=âxøGYe=¬HYe=”õPÖóîXÏ“r9´ÎASyݬ;‘ÈU³ÁŒnÝ³× 7þkÜl:;ÜLÃM\QÄvNRl'¦9;Žc;1ÍÉÂå Û‰iN$RÄvbš£Ø¶ÓœlE¶ÓœÄÜ<Û‰iNL¯ ØNLs"‘"¶Óœx ØNLsbs ØNLsöùJŠíÄ4'ÍWöØÎ>1*`;1ÍIF:Ïvbš³ktŽíÄ4')b;1͉ @Û‰iN$’e;›íÄ4')b;1ÍIXOžíÄ4')b;1ÍIÌͳ˜æÄW ØNLsb LÙÎÛé—Îäê00Í?LÇŸNŽ}`_ß‹çÍ‘„²à^B2ƒ$žÒJsXÒJs(Í¡4çýÑœ ¤9!¯éW{ÉÍÆjÜs§öy0µ»œv{õ 5ûMæn$Òï\?™©O`Ó•;â®\@º3ݞǶ ´Þ`Qí¦Êƒ«ø®C°{Ò"uÇ|¶Œè¼ ü0›E ÄÅÚÂ}ëZ·Þ»tº«¿©#-,599ù[37™Šª¤"„¦Í†9i{éÛ4I¿„üådVͦH„ð&bfõŒiœ(½]È46ÉM¯ávãz;ìø6tüõøvÆ ¡â“†Î» µº‰xŒÊï±a|cç´‰Ôªêv?æ0ph©îN?¥ö#>u¨’Úoªi†êªÍäô5ìÏŒZ†Dchäøqb‡Ô ½ýû‹ÝÃV{î„Ä ^‰NÑœC·A1ˆû%uDUkà £Öb]›¨ãÍ©5g®^#½»h{vý…ÛÑÁ&ÓœuKušdÌù²ßwÇÅ|‰ûËvÇMhβ¿:þ|Éö½-‰{„žƒÄ_ªC/ºÓ*agµ5ü©W*sJ5¹¹s:"_µ9WGkpÛ£öŒê#á31EW«—j³Eíª§'ê´Ü®s¶*.eLh´ÊýI)λ¥8„Û”.-Í~GìæøÆ.ÐgÓ·a7H.³¢$!v"Šd5¶ –E…UÇl±„Þì­hSx ˆˆÊaB,ËH /Êì¡7/g߃ B@aûw퇖^ üíHÿ×Baþ‚ùñƒIA€={OþÀj埕¬eãŠm0ãÛé ÷#¾¦½PæãÛ‹ ¨¦Y@|Ç E¶, âuÔ.¥ÚâuÔ“•_Åxõ`À멇ð:ê;‘B¼NTc‘^7ìu„×Û²:Lßš¨ÝGxD¼îG"ËÔ¡§Ô>ê;´ª¤N7·àÜHn‚×ÃÓ1^'@=¦^ð:ê±È¼þmò#¼~Þ£êœWuía·Nªi­[`Çh±GãR"V¸G_{· ÈÀ¬½ÒÉ'ÛŸÏÝßúΘ°€‹xŒÜvÇš/µ¾+tœùrÑ_º/ìºå&ñFô^Ã;â,O0/SÚ"ߦ൶¹;*u)ö¿|õ !2fÝõðé×B`Æì?Úô­ )e™gR¢SœH˜e”²$@†“ÁÌW–èja^PžÞx·¾ à~©{"ò,/Š'ÈÜxT0ŠÌ@Bž—8 £¯#ÙxAÈŠ‚,Ñ{ßJAš‚±D÷z’À±0ñ@ñdIx=IYYñ+þ<÷(ŽøôÂGʹdivéÒZǦý&×Ñ ’˜2tÄÁ,/²‚"IüÊ€§€B ($ àG>´uŽ¡ƒlÓ_”êžùV‹à|I<Ìç2"ÏFIrY!—‰ˆå‰•¿¶fP’9Ž ‘§hàg]3øë-„h ·“xò>¼×ß{„4c-(8¤o¸÷ B$7ª‘bŠ (& ˜`‹ úÎ*˜MÈ|þë=ßPU8’µ$.‘I’eå¥X…‘)VùY±ŠH± Å*«P¬B±ÊÄ*]°þWÅ*<`•Aƒ)\ƒƒAäx„xž>ŒùKßf‘(L¡0… S(Lù0åîèYX[RÁúà»ÁE)³ˆ¬¶yQ‘yY¤¯ £×.>+PP@AÐæþ ær¬\–‘,+‚Ä"žD>¿(„¾IŠ¢ï3b(\¡p… W(\¡o¼|+Bö®ª(,…)¦ Da …)¦P˜BaÊ{Yòþ®ªØÄºé® 3µÌŠ1ä!Cž¾¼îŸÎéì{yÝÏ<Ó—×Ñ)›NÙ¿Äû­â‹ áë­Þ×Û­ø2Ë´ “+¯Èk,¥Cü&â2¹t #™Q$ ÿôŸp%â(ó§ÌŸ2 #(ŒøQÌ¿‹ƒµãÍ¡»ïŽÎÏØ—¼Ýê»C‰¢%t&Å,ÁS,A±ÅKP,ñ£°DÍY¸«¿eô™MŠÒ€¾‹bŠ( à‡a€Îð¼tºqÉúßùޝo O@Ð¥…y@@_6EP@@÷»þ .ÇreÄCHñ<dAòk 9‘`€!åðýÜÏ1"Oί)”è’ÂÏH¢K é’Bº¤NÕtª~“%…l)¾€ÿ+so5!'ï/F œÿ—¾ü/S¶OÙ>eûBPñ.–j¶f„Øà~+“/óäDzE$@*â¬dH"D¡%VùŸòÍ çŒ“ÌVÃÊ&¯Xü ûáDøf<ý«HîÐŒª¼%çþ™Øä·×BSÜ RÜßÚ’R”íÈŸøÐ3™éÞçØm^м×Ò6Î*ø:µ¹¸/ ÕýÁüæì%—…M‘y”¿@(‹ÂÁ‡Ž¹œù`™^!üÌ2ô !½BH¯RxOáý›>t¼{ÿê÷xì8}&,­é™ÓìÔ”ò’D¢Î 6¥jÛú#[–9^$?ëÍJ£(™Ðˆ.=8Þ"¾[s;ÕÌœµO4Ó*‡8ÁVÃÖ\à¼P'Ý+DøôV*êvæe­ …;»§¯kéÙðÓ½fù¸²“:Ó6ØK™ºü•8*ø1ÄÖÆIêèó¯ˆtµ.*¡=%TQ.á@Ö›o•-uhâõ!‰ç\ì,„‰Ï;™j› -ÀÖ‰DB7=Ý97íºé9§DrÎ Cù>† ÔÓ±s*L™ÉélÑí‰m˜v‘]S'(0Œ\,sÎ@ã ÛFæ²Y\Ël7V‘–ƒ¤“«8,ã•ÆkÇ5_¬³ïúž ¹dOs ü?t— “˜y&®œµj™†ý¨ñ\šØÚÏ¥3K”0f ÀhN@÷áq©óЕžé:AqЉ“×§ŠkÁìüùèÿ7wü&¹Ónetworking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-compute1.png0000666000175100017510000020435613245511164026022 0ustar zuulzuul00000000000000‰PNG  IHDRŒrC‹"sRGB®Îé pHYsˆˆÈ¥†ÕiTXtXML:com.adobe.xmp 5 2 1 °ã2Ý@IDATxì]|Uö>3óÞKï”лôª("EĆ]±­»êJ±ïº6HpßJ‚ºì_wmwW]wWe-k]E齄!½¼÷fþß™dÂKòRÉ{á\~ÃÌÜ~¿;“ùÞ¹çžC$AA@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@€€Ò€²~_ôÞgž‰Ñs]WêªÒmGÅ-zÌþ=)ŠA P%]Õi“®}>wúô,ÿî³ôNA@Z"yR¦Í|zNj*âà 5*"تÛ5™òæE ¨ÈåÉÊ+гs‹lŠB:Ž/5MIzÕ™´®y{&­ ‚€ ‚@u´(Âøàƒ/Ef¿D¤Üê¹hHO[Ÿ.í(2,¸ºñK|3!“_D[ö¤Ówkwºghx_K°÷|ÐéœTÒL]’fA@A Z aTî|ì±¶ÁA‘“ª¹êÂþÚyýºâ²¥ ¯šÙkѺnЪM{è“e=X°þÉ¢\õÒŒ™-`h2A@A Å Е2áŽ;B»tì¹48Ø1讫FÚ:ÆÇ´˜ :[’v$‹^ÿd¹»¸Øõs¼­ç(‘46þÌ?<«K±ª Q<Ôà :¤¨êް˜à /<òHaã·&5úSœÏvt¸]ÚË)3÷úC¤‚€ ¸¨Ûu³çLxmíº½¤hê`!‹;›LòyþHU†rm#i ?fübB¦<•zå”äYÅí!ñžAÆ3ØüõC÷¬Ì;‘¿cÚÌÔI~ÑQéD£#`¸]ËyÞ C6û5:¸R¡ p–!è„Q»å¾‡†k6Û¼ -’ÅÀ~zyþxYõž'g ìÑ4ï.\¨MMž5ÛðŸ`óWPð5¤¨/a£ÑP×xço±1¬½G×ßœ<ëÙæïñ™íÁ”ä”w&'¥èS“gßxf[–ÖA@<l×åò3Ùu„GÆÎâ .ÐY ä±”êl¿`ÝÓï×íòœÈ6f‹Ëqèg;&õÿ¢ ÛètRŒ˜2zlÁ¬ä+×5efê=†®¿kSCùý‚§“>«œ§¥ÞCÒÊRlÅ0t‘f·ÔI–q ‚@£!ÈFÛ¹&´Um¶Ñ¼Z6¸4Ú3Ѭñ<ò|’¢\|Ý´i­šµ3ÜøãÏ=¡ÊÌÒ!¨û"‹œ¶`VÒ_MI#“'ñÛš†ìt.‘e5ÔiSæÏ·7AµR¥ uF P?Ltí]ºõ™ÈvÙtŽ„–ƒÏç‡KÖ©‘᭮¨^Ç!RÆ:NoV®ûè)¶†dñ@P›ˆy5×lêCn·ñäñ0)t:Ǻ­üX®½¸û ÅœîZ]Èm†¢|”¨%¦:wYù¦8燮ÌÅ8>~@¯_.Ú¸c²®Óµ¸kí‹4UykÞÓI_šoˆœ )#QÖ%ñÕŠ]}j¾sÆ–òº’gu…ž%ýj›Wà.xÐ ýRˆûc³ÎF¼ü›Ÿ2“Ÿ‹ò0efÊC†NWhŠúò¼”Ÿ–'àâ>çã]®’7PþÀ‚ÔäÉSf>ÛßÐÝsâ¾!(F–¦­*Žç§<¾“£XçoêS©¢Wân¢2ˆÔïPÿësgM_ÃyN¦ÎL+×#ßÓAŠq¸ØPžBF Íœ24å•מNú_åzØá€'_e¤eöæÇѵؤôû³f¬ªœŸïÑÖ/1O×·AP=Ø~/!{ë— ÷Q_Ùe|>+–HA@h±ª„‘ p;l{ߨð ØYlYÏ'Ï'Ï«Rê¡GŒ­×gz c¸YL¡·_zè¡âšª˜ëLÚðZJòØ)É—V$‹³þ¬ž÷Qö"ÅPö@¹×ÝH7’¹­Ÿ’šZþK-:ä$æÉ¸éã¾Ù°ý JsAŠz€èh`'·{<Æg“g¦Ü#›+@f@n(G–…o0ÜžE÷%¥t°úh(¶XÄß"wm;o1ˆÐÓh?d(eGb™ýïS’R^³ò›gúrûEïX!7ª»8¤¬orš¦¹ÃPw_Ôeæ5”ö|¯Ú\ÈGôç Ñ `_:“hƒËìÇ8Ú ÷» ÷²©I)7›åNóŸNF/³]•nÀ¸W›A¼=(Šñ]Iºþ_Á ÞÕÜëLàÉw¯C[SѧXH¿£Í5ë1<+¦$§>æß$¶É˜']x29 AÝýp<²øÚ¬b„¶±Æçݹ–@@FEQ¢áÁ¥åOÓÙ7BžW|ì0r!Œõ˜~&kf1ÃØWâ4-yöD³‡! <¤)öþÌ gRÔ&*qŠ=B½ŠN$HK<$hcU2ݤ$µéù'òk š¯€x¥©š:ñí{p<êÚÄe<*Kò*ÄŸ‡˜v6ÅÑuƒ£=¬!ŒClü5î™öT*Ô:‡¹Î䕯¥ÌìòöIYá{ùž‰3ßC¢9Dlúýq¨-×P«²ó © ¨¤rt‹¼Ç'SÚë@|N›w^#›;`¨Fa];2Í9§ Àǯ’yÉ€îUvM³¡ošõ¥õzÊ ÕŽ®}n]ó„qoé½’ùªsú.ï4H wó=HZgïxóZ¡’u¤™ƒ]±—öÉìcÅ´†Þ¹×%è „uêg¯:ïÏ«\ŸfS¾à8·‰•Óª»ÇsüÞ‚©S!<ÚQâ&óÎ0•&(ÊX¾À£ÿziDÅÿUMù¡4F11pfJgôdÏS‚=éýй‰âm½ÞÄi’㊹êw‡Öz bhÆDèIþX¹Kë&TN«î^5”*;OxÃÐä¤Y(‚…q<ç,qE0qÃÞæ«.[°òsI>w:; ‡ÜÛ»•åÛë 'ö–ûšéÈÓÞª¯)ÆgÕ-gA@hÙ"aä N$X²g·UZBªÌêF¥!ãx.­X¿‹ÎïߥN„‘¥†yÅÂã*Ó}7K³N?”Bƒƒª”óÕo|T¨Äí¦ ûé§øtýªÒ`YûæšÚèSa¶€º­¹öVu½’øRðFc4®Kuk€…7Aä»ó6dÙìÃÉ­cƒ8žAˆŸw 7Ø\ SɸO%šÞõz_ã]8à}o]óÆìj.Dz%µ-¶â«;cý7– wuÉâuÅ`é*?yå¾ë67ÿÀÁb-ƒ®RùnòÓik¦«†OÜñžš¿N1OzŸ>ä9¼^i ÂÎKª_v0´mΔYuSŒï4c’dA@h!œžMøç@K‰þV6´{·ž¶ì9LÚÅц])62Œn™0Œ:´¥ ;ÑKÖRaq EG†Òí—k’£O¿/]›ûÞ·tÁ€®´ôçôÀMc!Iñм¾£‘ºÑ#ûÓ+ —L–дë/¢Å?m§oVo%G§‡®;˜ölO;Ò2èÍÏ~  v§•›öÒ½7ð7¾4»Üô×—QÚŸrÝ(úpé:êœÐŠ:Æ£šú½eïaz÷럨¨ØE ÞÕ›÷Ðåô3Û°ê¶ÎÕõëß_®¦íûŽÐŒ»¯0I矬 Œ¹tãÅCéOW˜}ß´+Ÿ"ƒÆíEc†ö´ªlìsƒç¸±;õ©´•#Aà~ùà‹/þ¡¦Ò؉|;¦;…)móÉ´)ÎÔV& ¬YŠ6‚qÀróž¦ÃÃèâ«n–*¹®¼þ¬2íã+/Ç麧Ö'¶½L¯p^ŠM'wWWgSÄ£Íýh{€îV}J/=%dbÎóÄ’ß©3gï#Ý$öÃÎ׃}a:OÑEs\Í6¾¦ÀLê3‡@Óˆ†Î\ÿÜÌ}PVnøè&¡è þ°q¯YïgË7R§„Xš ²d§·>]I­¢Ãh$H‡ëÇ ¦>]ÈíÖiÿ‘ãt0#‹—‹hÿáÄR¿ƒ™YÔ½}kÊÊ) /ØLýº%ÐÝW_@mb#è¿ß®c¥}lÅ’Ê/ƒÄr@÷öX–Ja&ƒþýÅ*:š•¢zÌÌ„˜uê œjì÷² ¶ ];fH^ŽY?ç¯jêWÿn‰TTâ¦Ý3©Äuç£Ôý'»ãûGNÐõ ½qQa´tÍå*+‡•›“û3ˆ€ª½ ÝaúÄâÌœ©Õ5 2Éœ™Ž…Ñ÷8_tˆm'¨ xû<œšZ*õòªàAç‹‘HîÃQ/—é2zeh¼ËnΙV¹:£À=žã@†xÙ½4”élBòÙÆŠ²ÎЄíg]Ÿö¬¨;Jó—óRqåü ÓC` ç}˜·IªœÖà{Å0ÛV }¬¯ºÝ8¿4Þ01·…ÀÞ"&*(]O/K;U„ò|¤™¦‚Êc›s|å A@D‘0ZÄ­sƒq‡þÝ8n éÝ„0œŽ˦´#YtD’æÝé0úh£œ‚":~²€b …äÐ66’ÛDc‰9ØÌ03›öhO‡2Oâþ„I{uЇäò™ÿŠóû߆4Ž7¯ìšý%~äôǺæþ˜p»“óüñ‰'rÁÆÞa"RP`˜qo…bWÎ}(£âùûjÁ¬'7Zñ}æ6rN?ï]/÷Äé÷ÃÛ ­4˜N%“'Ï>´­øiÎÔ~øó°u_ñÌfjPFU"¬øP-¤YÉDÛñÓ~?Û$¦V“kíÿi°w¨³âë¬Ø´¿£mÈøÝ÷;gWŠ2q‡DõFnKQ´?ñù•3ŽØŒüµ7ÁeG¸|œÅù¼CsŽÏ»r-‡€-ðºÜø=¶cÙXÓJ¹³ª¨Bê˜ír„Hlj‰År4,a =¥Ëhõ¤W§¶0ž ÄóªQ°ÄN«·ì7É_‡¶1´ Ò9 ö^rÀ—Ö<³„Æ©ÍëhH½ë2ÆÇEÑ*,SÜÃ$¥Þé|í«ß:¾ŽÞýòÛÈÐ`êÖ¾Ô,[]^çà«_6`Ò¯{mÇ’9&ÇL\w—%ØQúè0iµBB«(²—1 $ZBó"0ÿéä&'§À®ŸñkœÙŒMC~†$qä‡ý(×}žŽ(Ìá<ˆ7›D±¬Ëø váqFÙi(—€YþyŠuÃSZŸ’›Ü¦y›¦%­<¼Š“±q£=úø%–؃)Ïu7âzáýød~JòëVÛ`ˆ+àºûȯ7ìøƵa;É9·~žp&µ¥Ë¹VœQÿjÔu;èÌ©3SÎqhö¿ÀŒÎAlvy ¸¼ax à7ëEŒ{Èi|QFÎ](6}ùnü€ž]àUWc\²·´=}zÀåÒW@OóEü=‚J½gañ+PÛNLçÏšñƒÕ~^>YLÊ•ø£u;¤ž­0WŸ žt÷ö_"o7Ì[ê+ÿ¥Çf‚šk|VŸå,‰@ JÏÒíÛF›ËÔùò²svn!í¤-ÌkÓÉ Hó˜\öìOG¡ß— É^‡øXSÿq#ô!YšÈ¡?–š9|½j+ñëï×ï6¥w=;VYí3óñ,í¼ù’aÄzŒŸc‰¹¶¡MLµŽ §ÏWl6—¹ÿ·¼Ôz—ïŠåqÖAäã"ÐÓõk0$мl½vûH;œ¶ £‡ô(¯¿Kb)1=m!ÉФÀØö=ª¢ÝÀD&8®…Ä#xn'@’'ô ·õ·ì&Zy%5y¿FŽa Uð:BWã·Í_¡&ñxOe‡Cµ÷Ú¬éUv[å第Óc*–ÆG±×üøy}é…>ªÚƒîñnãÕÔä W"í$ú8›Af!ïmȳ"Èn‡ÝȪÁ®ÓGÈŸŽz;ãÇÛ£%ä2_X¿~&l®>lkò ,¼ö_@ Ýðk/%ÈyUu»Ç«¶R·˜ù)I Î@ÛXV0þÀžvпg@`{à¦&Ú’oõ®ñå”™{y.5ãRŒùeŒe.òw ›6çÞùùº9ÇW¹/r/ƒ€H«™«¸¨ps™šõóæ¼õ%9ì]7f0þ«Øtݽpz›?žüÕeÔ£ck|µä³0Ƙ:½Ë#ë=2™úöçôãæ}f׎T.ÕôÕ®%zCa^gÍÖýt^¿.¾²ùŒ›8j 6ȬÅ&›m¦ÞäžCÇLò[9óéúÕ&|Xw2;¯Õ‚0V®_îýù)3>p:ÿfг 7t =ÊPm›Úi]·²é•êzɳQnh:=ÓÒ¶A oÅðà²þÕß?¹Ï'¸È©P&„@«j([¶ö™Æ}C Ÿi\ŒQ¿ÿÖ¯gÐòaØ ÒްѦÎÛª¶bæ]ŒþÆe¸ÿØCWÝá{yç<çô}ey«´Á¤xÊüù•ÙmFIÉË¿OJ›ï,š²tI¾ï6ø¾1ÆÇõH³*D`èvô‘uŽÚÜþðã¯éßcÄ]/hÒngeçSˆ“EïÀ›B¬¥Yïøê®YZÈ’JÞ‰ÍK¾MØDÎFì^Ž ¥ŽØé½ú“ÿüß*úõ5#! õ-Ñ<ýªëX™ŒoؾoÉ›sRoDÙ\¦¾Y]ë‘ü‡Àä™Ï ‡'•Õ OËàoTà@z,‚@ËC@$Œµ˜ÓìöêB¹<ÛDäÒMØ&â–½éÄ&oxiúÈñjaš ª®Ý3ѯêÚ–xA@A@ð„0úÿÕ¹‡·NN郳)»µ#éKBksI½ÎIA@A@€€Æø°þ#›ûáC‚ h(†ûÔ߇Ä­Öwé¯ -!Œ-ufe\‚@€"+»ÑuÖ]• ‚€ à'4ÍÎ ?\ tƒm!òæ˜Ã ‚€ ‚€_" „±‘¦…=¡|ôÝÚ¾ÿHj\µy/ý~þǰáÈ}‡úÖí»¶ÆeÛ’<ö¼Â¢Æ¯\jA@fG@£)`ßÎV`Ãܾü$³;ý~½ÙìÜ÷¾¥‡oGÑ¡ÞÝkA@A@pDÂX6nÇ$wí±³ø¼¾]`’&›~ظ›®3ˆÂ@øØk ‡å÷Pn~ÝuõHÓ Ë?l&&S×d¦³ ½þ=黵»àýÅ {o¸nþ:É“¹”q"õî!öÃuï;Œ ¡e¡º2µ­ÛªÇ:WWßácÙ´rÓÓ°÷e#ûÒö}GÈ’ªV7>¤¹†ÇÀgöõcÃÓM˜9¦øØ9°»ÙäõãSüUKA@–…€H½æÓnÓhÜðÞt4+×$TÃût1}:oÚN{Ò™97ÁsJp°¶d—”.ÁîHË0¥Œœ!aÂC‚M2¸å~Þv€8—pÙ/sZF–YE®ö>޶öšqL }•I(3sººÿ÷ÃJC}.=¿oµ}Ø[–箫. `HCO@jÊn 9T7¾ðCÍaì°Þ4„˜%§L– ák;žk8´¬ÑÝ¡™Iþ«¸„‹á m‹Gä:c}ê3<:gNX¾Ûíh[üdv™û·ëô÷D¸µ³e­Œh[Ü-¿&W…þ>éŸ -‘0zÍ® û…T¥­ÌqbY4ñŽæ“ð«ÌžQb!Yk×*Š®¼°?õèÐÆ«–ÒË~¾Š>ÆFXHâØ´ ŠŠÍK<²p`h…êÊXéÖ¹º| èO·ö­Ì#*,˜ªËgé^ò²2K•Òã1N;>Ë» Ûz”и`^wžûé®å¯TW{vvñ|Îs˜žé]]žšâ§¥¤$>à|.¡¦h`ȧŸÕ_ç'ó+lò•Gâj‡ž•ÉÓfÎY»ÜuËå)2V–¸K–Ö­”äJçGžAàìDàÓ9;Ç_ëQ[µ›Æ¥ömbèÏo/¢w¿þ ÂPêkJ{wnK?nÙG+7îQìFÛ`bç¯}J6[)ÌÇOæQÇøÆ“¨½ñÉææ«Õ•a‰fmê¶ê±ÎÕÕÇ{F îAGOäÒrÆŽmcÍ",5¬n|V¾Î<¦¸¨pzýÓÐÓ,ô•Eâê„€’ŸŠ®ëó¦ÌŸo¯SQÉ,‚€ 4¸®ÈÐmnøñׇôï1⮉445W™WPLªªPh°£BFsóPµAêÈú},}äÝÔ•Cv~!…8ìUÒj*SÛº½ÛòUë€.%K!#±Iå¯~¹Í ?L¹ª¼huã+Ïàã‚Í÷XKÖ>’ëÅäsÃö}KÞœ“Ê?ØHe©Âhj ¬Ì¼$=%9Eo_˵ w*¤L_šü¬÷H¦$Ïú,:ݦڵ¾ó3¶x§ÝëL »õ¡(¯;ã»—SfîµÒë|>6_/{N ÁG‹ISîPUu¯â¡8¢GRb«e ¦N-ÇyÊS©c¹ì‚§“–XuðyjRÊE†]É[àLúÙŠ¿ß9»§Ûí‰^v¨vÇ¢WW0J:eæ³ýÝ3?5ù»)Îù¡Šç؆¡/GÝ0ž/ñBýúµYI·ê|pöìÖÅEF?ÅPŠæÏšñƒ_Ýyš3µŸá1.Dz;C§=AöÈ_r>”ãßé|=ø°ûàÔÁªa몺æµY3¾õÎ3åÙg£¨À3„Tm÷ç“iÜo2\*Š}ÅüYOš&t¾YâÎC[žnª¢­k«ÿƒ·Î)σêÑ<¯Ìzróé©m‹ŠèR´¬¶ï_My§ËÃÓahõˡڶ·¢Ç¡ÔaºÛ®–ýë/;ŸH÷‡u]Ó3Áyj[ouÏÏ<çô}\c›î:ÄDzb×lÊú¹Î䕜&AªL&°ÆÄ«¼ÍÂCƒ|¶kI91¤™ô.vJwÑ;¾¦2µ­ûtõ1Éýø»õà ø‚ÙÌÍ8¼œíªŸwžÊ×EËëõm"²<¹%_„ÛC—ç*¸c|ê^ç3 ç:§ï©i¼L`Š]9u»ô›JóTŒ·cJRÊûaq¡w¼ðÈ#……zápãa¥“ÇXŒgà)èt„>ë=jZæh¤}Çé÷&?×Ëí)YÌ×÷%?×Ç"9SœÏvÔ]®oU½Š¤ŸNC=ìJQâÒ¸‡Rï§'UwçLMžõ›ù)3_G|i0\OÃ?ôø©O¥^«»¾’­jÊÕ0åTªØkåÙõäŠ ?D³¦:g÷Iwú¸ü1Hs» šœœò¯D[â=Nç]bÕß[þ@\’¶H¢áq»ó Kðw_B]`»Oüê2ºí²á4»Â˜4†®Ù¿.U4y^žWÝг˲æ¼ÉÛõ—^p>rB#ú->À!·§Ú 0V‹\9 ÒõŠ¢&Ù‰v»­‡¢*³Qþ†üÿå|ºÚê{‡]ë…z$ûù:Ìò ÊüÓ EÍgºâ.¿vSÉ3ÿ©Ï8¾64ås>ö¤NÑɘ–ö$£Ýl½BTUc‘†n(a;ŸóY}t€R¾ÃùY¢èPí¤V ÷%¥tЋˆ¥~‰ªbL¹úªbŽŠw ‹÷‚tÎ@û‹4Õ6„ǯ’ò;ä Ç3ô6Kn|ñÅ Ã ²l(Ý5U½92*8R U0v'Fs)¯X+b åQHx R´k°Rpp[¨©Ê-ÁaZUSÇ#z3ëï'?•rƒÕ^]1D—RQ¶-ú3ÒNJG+ê GÜ V|®Í3áÿtõV÷üpºK˜´!….I´÷ µ…Ûb1ÿn?â:T>Ö íÉ -a,))>ž•W€¿ÉêŠëEöìˆoŽŸ†“y:~õÓî‘nÍKMþ¤Fw€ð\†eà›±”û®¯†!É»L7ŒKñþxAJ2“ +$ABÖå'±Äp®sêv$ì@œ „­ä猜ññçžûúdŽŽ¥hã"« –ŠGƒm3£ ^°1ˆŸËi `ƒ…E‡,þíóχä/p‚¸äµ¼á¥‡*æ<?@šw5¹<[!}{÷åõ‚T8 üüµ”™¿2sâ?HÜJ-Úãúþ¤”N% -¤²5–¢/¦ÄÓÊ[ùÌKÛ†+ó)ŒýX˜-ôf&ÚeyžÇ8‚°ürêSÏõÃRø%_'¡ž7+iaYèŠB#9å`tó´äYãæ¥Ì\lµ~¦C:v©%ÃÄêDÿéÜ1~`Ïk,iâÔ™©a} ‘±–—G{àŠêƒ^í-²ó.—¾ @þy߯†À%RãQ,5.ëëóÀð—h¯K+ÙÔR­Ÿ‰”'ø™0C-ê-@Æ*Ï“ñâ£ÙÃðœlO´%/.3õTât.œ‘îÚÑ:mÊš“ 0(ad¸ñ·‰Œ¼¬{sòŠÕÒ–ÐràùÌÎ+ÖŠòr™Ðð\Ÿµ!H1î*qûóoœ/DûD…—®Á…”¿VI7”O9ÎCnsù²J:"þøÄ¬#º RªóË7Ù6*Ê"ˆ% H£­r˜ŒqÇd%/«¸¦§-ú·Ü‹,šY8glCüQ”eU!(ªúF…ˆò£ Èâ· y]°º}ë:–'UsaxNôEZ£U^dÑÌm ³ýÆnWS¥éŠq1Gbc—¡³*5XšI¬×hÅð²ÅEY4ãUuŸy&Za‘E¾W5cŸM'>Ÿ ŠéøðÔ=“täÛŠ9ëÃX×Cý¼È¢Y=Ä [0¡Çé QŸg¢6õzźæ¹GûÀ9éîÔ¿Þ;ó™¡¥ÄuR ôoo\’TAòi•“³ (aÄß[“DèëW/_ѹw®ü”ýð‘Ð"ز'vÃØðÓŠ%5ß-bluoZ”ñ÷€aN¾+ÿ”¿·J õd” åú7ò2^å8j HÖk'# –˜±ÚáÌ¡÷9Ÿ9ær¹¡á·DÑ6(|ôÏáJ]FIâ¹PTôt‡Ä+7 _V Ê$ŽŸæœÓfžó±ri1¤“¾7Bè”–V¶éÆsê2—Ò+ÖYùNïÆ1'ÚªæNŸž…>XO±;NùFû¸ŸøÞ;`ÓÊô̺¬4˜c=h]ó•:¼ã«»KÿÓc±³B@_™Ü PŸH4ꀡî*àªTêEÀ»V¨¬ò­‹ôÒ¿éõy&jS¯w›^×6E»þ’ڻ܆û®t=5 Þ¯!b}gî’þ ¢^á¹ô**—‚€  "adhù·'#--;?'g÷?ïènŸÎPMŸh éÛµ;=Å…?îÚ°?öæG: ÕÀÎ_2°ç ßlØ~HÆÔ{)ozÜ•¾½Ù¹ H•æ€äXK²e­ba¯…JêÚšº¡ÚÕÏu—gŽîfi¢„D1ÁêR*¡¸ä“¥zŒæŠ„¦i¦þ"$šAXZe¶f-EWhý툶õxêt²BBõ7G5Åv¹NîPözÖó{íéä÷«ÏŽqz? З¬y™A!kÉ=>&F_P©BèTvä(Hq+Aàé®”µÆ[è€VþÄ’[_¦läŽ=ªªÇja:™<ϧïW=ž‰ZÕëk4ˆ›;kúœÎ™2söy˜kñÖŽÞg“¦&§¾ˆ´‡«)*Ñ‚€  "a4É"ðå?š%ëøîŸ¡®š³ ¾‘χíC Ïã‰ìuÛúy£Ï1‹~ÎjÒÈËŸXæ›ì6<«Ü.šÂmfYQv€EŒÕˆvÏ›•ü/ï4ÓЧ¨‡=„öxÇW¾fó<ØÑº_'ý"°ž,±{eÆ &PÇ¡ãw›:Æ€QÙ‘¶íÔŽme§Ya.÷þŸwÜnž+¿'âÒjëîOSŒ™ófM_ ýÇiäö¬‡tóe¸J\\&)ô®¾ü$}À‚¨·ždYêÔ§R®ÂU›ö6¼ìÄÆ/Þ²c(’W—W€ èØü@>1ò.S«kÅèÎú}•—ëQ¶ð=°À9µÆÙk!vhתY3S#<µmìþÙ³ã\…Ô=L Þ µ€U(ÇÇt6är{~ÄôLÅòû£Þ&›j[·äÿA uY¼Â$¢GѦWm;vøÐwŸ,Û §1WŸü]éIàùãyGä…›Éá+?¸Ô"ÄáÃPyéº4D†kPèò_Ä›O¬x>¸ .-»×;¾¦k¢ò\“©ÿH4íÆ{ò=ˆh•òöÖ?CZ—ø îs¾n¥³©XÌyú–·F…h¬_gŽÇã"ìv>Ju6õq¨#ߪ™úž§Rv…þÛ\97zׂ5c»Ž “*þo\ ­¶êóLXeksö~~ô½?cîüG½Ë–mªÚŠ85¨¸8¿5ÞÑkAà¬G %ŒvY¿€zNª¶à?á\ç`kŸ#з²Ntáþ]Õßøª„¥tØ!ü{Œ}žÛ}â+HŸƒ:¢ã› ;¦ ¿ÝA‹ÿÂý¡ý³âʼu$CRW¤¨¶/¡.çIË|q}±”?û¥32}µQÿ8Ń?Tó§$§ÆcV@ÂÙ8þÄ;xÇõ6†u&ê2ÊŠÏOkêúsº²\ºÊÌÔ=0A´š‘nqôj±DMú²Ö¥AÉ+~€@ þê³c0Ì-ÊÏÏiüsÖñãç}°TgÿȬ 'ÁÿàyâùâyË>‘µÿ‹…ÿþ}~~6‹Šyn…0zM¡¹BU˜ÐU Z˜v-Ôâþƒô“¤»Wƒœ|ÂtÈÞÓa±¡Ê@Ÿq.òÆ.™?» ÏõVeº-z¥˜õaŸñ;+Þ¦E¥×Jn;µê=Ìšñ¢ih¨FÁKt×&è¾ Ú>•‚ÎõÞìrªÔé¯xw2Œz3ÁCPT–`–Æ—þ;ó1Î{ñÆÖ=Æ1vx²!ìòV_2:´yŒs1±ÔìÚE7ÛR|N×]ë@Žy\WcÌIóŸNJ.­­ñþ‡Dxª(³°á(E×=+L\øª·½šš|Àj©©0¬Ë3aõ¥6çÊÏ©r`SoÁœ§óÝ.÷n˜Z‹g¶1•/‚ÃÔ©µ©Wò‚€#€» 8n«Ã9ž  Ð6—Ýò‹ÛÛ´ï8"6*T=¤§Ö§K;Š ØA¶Ô޳éÞ Í\Xg13ýà²/ßùç«……y‡0æ#8X.“F µD€^{4„] .ÃòÓç£Ç|5½£lØÞ†¶ÔVÇÐW=V»—SrÝuE"ÏÆ—f%ïY8£¿Ú`÷Ï‘áÙÝŸ4‚´í¾tÙÜK&=ÓÝí¦~ªMO Úèk'³5®úž¡º¶$Ý ³=ÙeÞ÷Á0îÒµ¸µL^}ÕÛTÖö™ðÕ§êâ|=?¼¼¯Î¨x”®Ø@T¬éö%ÛÕÕ&ñ‚€ 2adé(3ÁHLÛâh#¦{¿݇Ž71*.®ëÚD†é1á¡JHˆ#P%ªVËìÁ…­³ýLð #ûÄñíë–-ý`û†u¬³x´ì`²˜ƒƒw¾žÕ^0~ ˆ€7a ÀîK—A@¨‚@ ê0ò@˜H°ô)6ˆ’5}צ %8Ò"ãâ"û Þ7ºuëøàÐH›Ý ½.ÿ …@/¬w KeXÆI«ÐAE‰F—ͱAŸ‹‰”)µÁ2Z ÆÂã†JÛ©ø eýì —«¸°¨° 'çØ±C›×®Þ˜}ü8ëåâà±±4ŒM°ðœòÜ YA@æFÀ"YÍÝú¶ÏºŒ,…bÈß³)Ž+Ê9~<å¢/˜ˆð8Mr…³_QƾÃGÜmÆŒ>Ù»}³¹{ý4C×~ýGiš=¬Ð³sÃÚrÅÿÝz hÇ™íÞµ¢ /‡I—?kyÒš#&„¼±…ûÍ>£Oà`ÝE¾çùã|A@AÀtÂÈ$„ "L2˜ˆ˜›apÇ„ƒ 3aä%i‹\â²yˆ ×&~ \¥±t±$ýÀþ%è“¥òîR­>yyŒLªÊ‚’Í7á11£Wš•ÇoÎóLŒ‘ç¹BW©W•A#Q™©¿Æïq}ºÊá×°”ö½¿úüJ¼¾„s:¿"Ì .«/׸)€Þ@Nà8¬ê´I ×>Ÿ;}zVã¶"µ€z/N7˜’.ïF ™H† p#àŸäê4rŸ3uÇc¤ÂXõ¥8Ô˜ÈPwlT˜ìÐNST’›‚¢ωì|=+§À†¥wÇ—š¦$½êLZ×ÄMŸõÕË{á߀¼þ=?Ò;A@¨€"ŒNçBGº{ç‹XvÒ:6Â}Éy}í{t ¨ˆÐšG)©gìÜZ·#¾Y½Å•y"äQY`ëñÓ9©äŒw¦…7(ïE`M°¼5_Ò[A@(E `ンg·vŸ`ÍsØMã‡i éMª0Ý?kŸ7]7軟·Ñ¾ùÉo4?Ù‚•«^š1#󬤑.ïE#z«“wã ‚-M ‚@ƒÆÅ¯ÝÇr¿ v؇Þó%¶.‰â¯Á3†+Ø{è½òî×î¢ך¶j‹DÒØð ÷¢áúC ònøÃ,HAàtÄ.é⣹aÉ¢ÅÓM§ÿ¦3ÉçùãydµÿíiàôLÞ‹À™«šz*ïFMèHš ø ~¿I¤L‘Á͆«ƒ{wòܤõ@›“(,Ø¡nÚuh舋/þèÇ¥‹M?Øõ¨ê¬/"ïEËzäÝhYó)£Z"þ.aT˜iÆ#dz F§MÉ¢÷¤„†ÑÍÎ󎢨ðºvìP‚ïXÚ±ÿ-_·ƒÆŸ×‡:Ä·¢h¤qX³uŸùÑcE÷Õ›ö”F«¢¡çt¦!89N߯ÝAÿ÷ÿÑÓ÷]OqQáf–¼‚bÚ¼ç)aùyë~ºõÒøv¤¸èH7Ýô¯ÏWP·ömè¢2²Ë’J_e4íŒóñÒ!â×€A7΄ÑU)ÿ×­ ·Zd­_–#Þ½ùÉr¨ „Ñã‡A"Lÿ[¾>Á‘ömbÌçÌ»fþÑãë9*À+~?|$㵕òYåWlØI[÷¦›„¯]«(úhéZóGRÏÎí@ü­lÕžYJ:õ†qÔ%1Î|÷–ü¸Å”6zÆ—Ç”êû’&²„nIëÜïj;T]‚¼Õ!#ñ‚€ ÐŒ4+!«fܼðcJ Co&6SÚ³. ­iw™q÷Á£Ô1>Ka'+t;ËÁçõëZ!Žo,i™0öìÔŽxmÞa’Ø:&$2†ÖnßO·_~>Ù@꬀3“F>:£óÞ[L?¬ßE/dfùiËb×b—žß¾üamÚ}Ðü'¢K…˜0¶Ž‰¨Ð·êÊXmžésÙüò yαø'á4˜ïFS¿ü¼29ºqüpSÇ}JhM3_ý€¾]³Ý|Ξó/Hæ(ýèIÊ‚ßð‘»Óu㆚dŠ ýëóðƒ©˜bñgòu£AÒZÓ¯VѺíiÔ­C[ü`ÚK­ðãæîkF™Ï·5n–"Né?Ô»sF½@@aŠÈÌÃzÃüÃÔÉ?ÊVâýÙ¶/®9€Fa™{éšmÔM2OæÑ4H';´¥ÿ­Ø`¾³Þýnìky7Q©OŠ€?JyLeFƒ E³þÅbYn><»fÐ5£‡Ò²u;+ô‡7ÅŒqj³oTXp…t_7«7ï5£û`ÐÉxè@IDAT­ml„¹¬Ì:cüáòx£ –Èp8v2ÆL;¼îH/ãMÂÉÄ38Ènæ©ü_}ÊT®£Ñï s~…,Ö Xl¨Iß ÖWdI6×Tè™RGiV)Ü{˜:½ëv¤Az¸Ï$püƒ¨}ÛSšèÀ†­ì¼:˜QªÏ«`­÷Ž+Gš„tÑêÍ>õ|Ïí×ÍTÙà÷ntt™¨²„’Ãìn¢ÕxW¶ìI§•ú-_¿“ùÅeæ;À;Œ÷â½à6¸ok ª±>Ìy,ý §ÈĬÀS\&",„ÖCÊß5±)9äeg&§XŠçM?FíEý{´§Ç) j",ůî]3 4ô?y7Š ”FFÀ # ù8õejäA×¥º®Ð\e´Œã9>%ŒüÑà%ªº„Õ›v›Ù½¥‘¼y…—ð|…m{›ÑÝ;¶5ϫʤ‰¬ƒÅ‡xi›7äø õ)㫞ƌƒ.ž5×MJ€³ÏÍ\—‰WnMÖ^î]¿ãm¾ õ#†õY:h©Zpã%è•/¬¨A"ŸkFE……Bnƒ I+Õeä%eKßQU˜ûVÕDè×=’É Ú©ûfÃ1Ãz›u²./ç¿ôüþæqKÔI¯¾GËÏ?šúuM¤Ý02átɹÄ$v9~àEàG\ç„V¦T¿¬ÃUN¹Ð ŽðÚÎ}câiÖãä`檽¶r6ÎYÞÆÁQjÆCÀ #®”H”.Y6ÞhëQÆ÷¾YmJ}Ù½ã0_¯Ú\¡fþ¨ÆÇEUˆ³nxç4ü!¶|cóÚ7¨ƒ7¯°®#‡4,£ñò–¡—bÙ.:"Ì”€pÎhøe¾ñâá|k.µ½ùñ2sYº:ÂXŸ2fåMÿß©¯rÓ·ÕZhr¼x£Ô’Ÿ¶Ñ;_®¤£'rLIÛW+7šØ]‚%h+°Dqû¾îæÆ.–¸õïÞ’ÅhSbžÝà =:Bçq«YÇØ2Òg•­éÌ„o$„ßáùgÓ>–Ž0ë¯Â³Ÿ“WýÉD,‰27¯ðæ,±g‰#«h°D’uYW’õ«†RYÝ„õ&@z¸hõV³|a‘Ë|ç¸å²7צ°Ò7ù\WÅEbA@ð€¿Fß½m†XÖcä%-Þqì+°yœ÷¡³åx—tu„ÑÒ5^?|ÔZ™ÅúÀ¤+ísšEYÂÃGÃn¶} vA³~ëc±´ná̪ÕîÊ »M…}þ˜Úa#Ò;Ô¦LdÙîmïrr}v"ÀjSoCÿøtv"ÿl‚ÀÏámÐûëÙ1¾”ömbiþKÍ)ݱ‘…7Á°}O^ræÍ$lŠí•Þvù&‰+/X‹‹s±‘ì[è2ñã%i7AjÈR¿oÞNŸ~¿ÖB±¦—©ÇËé¬ûÈéñ­"Í~tT‘wU÷óa‡ß%–d²²6ÍŒ„Þ"/a¿õér®Ê”Ô÷íš`’a3BþAà,GÀÁ²+ëµ¹ýáÇ_?oð9#îŸ4þ,Ÿ¦–7üW~Ck6ïÚ’—•ý—ð˜˜"‡#ÈZß,¬®*†ª…¤¹†M=²ó…G),Ïpö]˜ïƯKzohßîc›ú½àÕb^bæÍ*¼ãž%V¸ï™7i,v!óNjþ‘âKú~º¶Ï¿Æ¬“kéô6¤nÞ(ÃëÜGﱓ9Ć÷›ÓHyÙ»±äÍ9©7bl¼Æ/f§2ÉRV‘06 ŒRI}À2{âèA=&‡……ê6»Ý§Z˜Ëå1òŠŠ\Ù¹…ù9…é÷ÿá¹¥®b÷7 f'•*vÖ§a)S+X…ït¤Œ%z¾È"7À»ø›*œ®_µm×Û”×’fÖ¶¼äAàlA@ãÙ2Ó~8·­d@·øŒèèHwÝQ…0¨¹Rb`¸–~"·ã®´Ì©{?9yöû‰¶’ÏNç©­¯~8Æ–Ú¥»¯¹;ü}ëé¶Ô1˸A@8›Âx6Ï~3íláÐ E½“D¾å³âÜz§6Ñy]ÛµÊÞ•ž™¸vû`Ë2ñÑ9sþñ§ÇË÷**—gÞ”"AAàìAà”RÒÙ3f©Ÿ À„ÑårÛ‹‹K•2"Éd2ˆÓŠŠ‹‘GI±£KܱۘC{º°+÷–‚üâ_AÊ(?|üdN¥‚€ -ùÐVšWV|/,.†¼­Üo4Û~c³!Av{¹ ¹JÅä¶ž¸ÝnK%Š•«1%ŒH‘ŽMºëÑºÍæŽ r]пKÈäuw>„rU.+÷‚€ ‚€ Ð8ˆ„±އeÑ#ÿ÷69ç}`ºãd¶›ÈqlNBã!®À›sÀò÷ª÷Áie¤Ò7o¶’—)‘,))±G…8܃zuPZEÞp÷ã΄Æë™Ô$‚€ ÞaôFÃëš½Z|µ²¢An¯d˜ñ˜~i½ã¼¯MseìÏÖW(€qc_]üUØ¿®¯P]–Ž–¸ªX¬1ûÎ>zý- ¿Ju‡E,™8ò2µËå²un“Û¥]«nšM½ cÁ>W ‚€ ‚€ ÐØÈ’t5ˆ¶‹4__¹Þ‰àk.¥ p[ÆîÃØ°ð÷Á#|æ>úÂÛÄŒ=iÚ¯c;u¹ùEôã–=A¿½}‚i¦dÍÖ}ô¯Ï0]ÆF…ÓäëF›õ=‘Kÿü|9íLË `Cù‰ ¢7?YfúÈeÿ·lÌ› _7n%¶‰Þ4ÿý¥” 0¡ÁA°×›®=Øì27þ”2Ããw]3ŠØ+Æ??[az´ 2¨/Ü©ýí‡9¼‡éw×L"¹S,}´®UG^ªîÚ>¶xwzÄEw<úè»oýéO²ÆïfO:$‚€ 舄±š÷h‘a!ôá’5rlÞsÈôoû‹+. i7£]pWöýÚfž"¸Û—~œ.ÙŸZǘÞ[ØoîÕc†1^WgÞƒg˜nZÓon¿>s4ï½%¦Ü/ØHGŽgSÊ}×Ó%#úÁóÄ>ÎBÓp2Ç3Ù»nÜPÚyà(}±|ƒY»dõ¿Gy9èßÕ$ˆ,e—gŸ~·î[Ó¯®IŽž ¯Wn2‰ç²u;Ìzî¹nŒé/ø£¥kͺá?KúÈÒFl˜1—©[G†E†…&ª†­7Æ RÆ@˜Hé£ ‚@@! Æj¦‹%†7Žf’9–Za`ÏŽôàÍãi ˆãpÇÇýíZ¡wçxºRI&m,ý»fôb·{xæÃàÞCÇ( iíÛÆÐºíiä@;ÙyÈ›E¡AvÓkÆ ÿúÊ”"ÞÉ_H+–²k4¬Sùó¶ý¦Ý[/;ŸÖnßO«6î.õ­«ë&9MÏ®§™"ÞX2jpÏò\#úw'‡Ã3;ÿ¦5[öA2\aIº<£ lÐ0}ÕÞqåH:rì$=5÷}H·ÓPxͰA8V²róiÆKÿ¡,¥=:/s-[:f}G¦„ãÎícÆÁƘ»˜flЗä‰%òñ±4ôœÎX6ßEsÞüú˜Áæš‹‡÷¡‹†ô2u3Sÿö1ËÊ£aÈç/A÷”nn©M,}FÝ£«¼­è€/™nmj’<‚€ ‚€ P¹YÓˆ˜Æ›Uæ%ÝY¡–Û.?Ÿø°ÂœßÜLùŦn¢Çç—Ÿ¸£üöò‘°ùe@ù½wçèN|ðFÖ/d²È¡¤†Î©×AêX@a¡sçµUKÑOÜy¥©ïbJ9mâ¨Aænj^ªv`iy²UçÉס[°i¦¨ÄeU+‰Ç2i¹¨«â°K»¹B v9»<&Éód;ŽÏ ‡´PÁXôöq¹!ÁöÓÚý±H#D½¦9ŒEÈbsM¨´+‚€ Ðb c=¦–õy#KCC\tD9Yô®+:2´YôN‹ -'‹V›´æ¨ÜaSÝÑaÁE…….;Œ‹k ½Hnƒx}¹ØíV ‹\¶ÚH›£ïÒ¦ ‚€ p6! „1f›m)¶ÌP* t³"‚MStH£ÍNžð° ƒ—¦ë9n®¸ŽÛ‘êÙ’A@ÎdIºLò»_®¢­{Ón$Ða´ŸÈ) w8ì[ ‚Ù {X°ÝÃçüÂâ`ì‡WÒª;tݰjºaذ­²#@%é€{¤Ã‚€ þŽ€H0CllûÈñ“Ô®U´é¦}Cg`‡rb›XèjØ= ãÚsW5û€æ+™ØèÂF´-;VóyØDs"'®ÃMÓ=Ûµ"ì.²Ùœ®Èo¹îc¿ÔÛ÷gÀ“L4ÅÂó oY¹i7cçvW˜Ì ™ >–MlÂÇ*gµãOg![Ç„è£#¡U´çDnApÚÑœ0UUø˜°èY†°ë¿šú û’:êñ Œ½¦|’&‚€ õG@c=±c—|ìâI{uaÛ‰º·§çÞøŒî‚w•óúu¥—ßýŠú#®Øå¡M»Âþ¡NœQxŽyäŽË*´¼~g½ýÅJŠÆ¦6®Í»µ a{ñ$Ìì0!9ùÓõ/ƒÏhâ hÚãLÃáÅØ½rÓZú3–ƒ[Q|\uDÁ³`k˜Ð  2%\¨ÈUbÏæëžã©O·D¾¤èˆ0b)6‡síp€}BÚ¶ÿÅ ýï}oƱ=ÆÊ!I6î}Nçv¦KB^÷×Àn Ãá"1ãµÛëOZ˜[²µS|Œw¡Hvª#ŒÈêÀѺ¨¸¤õ‘ã9ýv¦e\¼eOú÷““gÿ5ÑVò¹Óé<­íGųº~±:Ä;_®4]MBþÍQŸ.íÊŸÅêÊI|ýÈÁ;òN´k&Îï~¹Ú”, YlÞÙc¢þ«‰Ð|ücÕ€Ûñ÷€]yV^uhÞ^6¬õûœ©ƒò±{=_n÷|dmÔ(o²hUÌ +œ•dƒ¯«tÑÂË:3i<¯×GŽeßs÷ãÎÿþGgà·,LÚác´lÝsZ$‹Ö 7ß™ç`âÈþôßo×CêØ•ºuˆi ü?¿NçBGºkÇK.—19LÍòô^®Å۶ዟ[¥ä曦†´Ì€µ"#‚»zk»JF^’¯Ç]69iÖ_ƒs¢|饇ŠRy —e‚¸hýŽ‘†bLTH÷Íúíý@ƒL VèP ÜÁZaWŠTM)Ðl†»t½ˆ-ªA8íVl†‡‚t·¤—áF‘ šæÖ¡ŸcjÒÓy •+Á1—!ëgóÉ?2^þÜ÷Àÿ‹åÏèJßNñ­¨OׄQ»ÒŽ\Îÿ ‡ùw-¤>Zº?fÂýv‡| áÙX}e•evÓgßo€I¬¸FQÅh¬¾Õ§žgÏn}¸`Û'°l5l@ÐçJgûO6E©¯s¦úôÀÿÊ€(SÇÔپƶÏ5Œ6_qwqDÖ ;{ìê7æÌÉ@îïI}QþíoŸÉ É`ц­ÀXZ¼ƒ Ým컵(å¥eP¸šIÁj®¢’~º¬Ò`r«/.#ˆòXÊ÷ÄRŽ~Ò“8Ó·Ëî„tw?lóÎjgK~Ýéôý@Þ?{vªñ.Ý£h£x(ã†pÜ…ÆJ CÏÁ=”`kÈ鼫Èj÷l? a<ÛŸXï¬g§¶Q];´žxÓ}÷½óŸW_-ݦ^!—ßä`£Ë6˜wáÝÐ<žÆl’§Øå"$dÞ¯»†ÔÇö<ó`‡”¥ñl0ßÏŨA=`ÚhTYr©5ðs8|›ò‡þÖÔ‡_|1¨$#ëS›RYÀv_[:iTùÈôîù!9iŠÒ³ƒ}­Š£j4¾aWŠ)Z9LÑêaJ¤Íü|Á+˜ª÷t î!” ZpØõôíw:W8ìö=ßs ‡TO ß§$ßETjAâËSÁû³tÈu¦$= ù¤j«uRÖ(š¶bóÉR›x§JžW6gZ2H¿G ¡ËÒ<@6‡Þ+8(¼7n×à˜?ð,]ܰ#Í´³È¦sš"ì:x”þöÑrºaÜ`s'pum¤eœ µÛÒÅÃ{Rø0b½rãÓØýØa½LoIó?øŽn¿ì<УÔdUuý<“ñ<'.Y‹9:H 7uk0u&»V—¶”¢Œì—À‡ Y¬6&ÒÀȶ¬àž!­û$¼LŸÐT”`Ó]ñ7ešsNÛ5B!ÏèvÀ¹Ö;Á“k  a3¦†Uc¸e%xÜÒ³q>}ÄL(èö UsF„þCPÕ R#¥2IoeÛχÒűJù.oêh‡K9`¸ôV8aj¦;J;b€d“5‡ìJaÙçð.‹‚J.=„ŠŒH*ÄQ GÇœÔãGe{:Œ(ÔÃþÆNKþÃV]Ñ>„gÚÿ¾6kú4¿¯F£ßOÑÙÓAï¿ •åbÕ¥yÇ7Rlr§utdHÒâQ'‹€¼ƒ6V3MRÛ\d.10Ïæ]Îd`É#ÿ³Ü^fÏ¥°Ex~ÿ.caq \YVÝ)\Tâ6]\V×gv•iI Y‚ˆ¦@¸NIè6ï9L,]eÂØ66’î¸buðÚîÁz6h c啬\™MVîuýhH<ÏItD\wfŒÈCÂè0—¦Rç™.{Ï“3‡`Â=ý± -’ÅÓ£Ï+m½qÕ·Ü÷Ð_ßyõÅÕ(å—ö^ñƒ[Nà¡ÛTÅs™ÇUÔžGhSŠ\aê 5DÍÖ˜h9°KÉxð‚!b)—Jôˆ#´=üÁ ÒûvÆÈbåYˆVÓéœàEJ®ת¶ Òü=J’_óÒ7>:! Öò(¼×+ð'ÉÁcËtw¥ O¯Þ‡Kz?梠S“œ›À›ÿöï–î9ÌÂÇ ¹Î,äC¯´Ã™‰ õQ¯”TÚ9~}ùoUYWC²XV•yÂÆ¡ŒhÜaô`¹³€xGý™{£×?YDíiÓ®ṫAc‡ö¢ÛЧ߯7»0÷½oéá[Æá»bÐ[ÿ[—„'aÓFã@ì˜Üeƒä½:Ò3³©#LC™Ëè˜ß{oM,^K{ÓøÅÒÁ£'è¡IÓ¿¾XE[÷&,1™„ðžk.¤¥kvÐîC™æ³ÄÒÏ‹‡÷¦·a‚êÖ çRT·ZüÓvúfõVøv×)dõº±ƒÍ>W×ÿ1C{6|l˜ÿ$Œÿ—À¥§'$8° zcz´Ù¡XücÅ&©…UÌK²»J.øö¾0ŽêZûlÕJ«Þ­bK–eÜ;wc:¡×JÀ”„—¼4òC(O¡%y¼” ’GM(†Á0½¸Û¸[¶eËEÅê½oý¿ï®F^É’,Ù*»bŽ=šÙ™;÷Þ9wfî7§º½QàÒ.ÁBcÏ€1ø¼ñ¿0š?½õžû¿ï5“ t¥˜w™cM…P%J˜±®w°uüxû’;þØìɶ~ÎYb@ :’jÙÅÅàµ,e®l9ä¨ÃpÍz“ÂA ,0RÄ jUþ&ÀPK7Ç( êt+@ ±¸ÝÞpT4ÏyÉÌ>ÍÈ :Déþ¨žq8]RXZ-W„ÅEÙx£Çù‚éãÔˆ\ ÕuD˜MÖ@eÌ—ß»t pÿÞºÝBÀ¶~ÇAHÜê`ß7Þà r¸¤ ×áOê/¯i@Îô 9}R¦ä!õ%—«Îš%ßùÚ™røh•lÜ}ªñ IKˆV™.Yè]¥µ':o ŽŸ5g‚²œŠ—T9ÅDÚUÕTÓ9f×b•·zïáRiƒt”WPia©ŒKKDŽôirÁ¼É °Ù•~|ý2(ix#bV"e&¥Š¤ŠÚF•N“é!-PQ'ÆDt:}Ú%]4oŠœ6&Y–@zØŒþ1ïºF]ûϬIƒE c­îg¨Øµ{x°Úàz‘áñ—0Î"CçèÔ?gä]fÖ¤‹q&%vÃ9ÿ®ùÁÉÑãr?D–¬ÿcý2úÜðß§ÛVÂÌ °¦—„”’Y¶–ØŸ0@u¾6Þt×—4Ö ç ;Òx©_O9ŒGpC¬Ç Ô·Ý®¢n’FHœêÀ7Ѳlûq®ñVu”W7(i»qjÌ-{ŽÈÖ½rÀ€ ²¿„>i€±¿§[yx4+›Õ'0é)5Õ°L @8*>J¾¶pªd§'ß_Œ§?ñœPHåH› M|êÍÕ¢:dñ¬ì³ÿò]·yo‘ 0Ñ'1%%IÝ;j ‘‚OÐÿöb¶Ò¤Á¯ "“Áh˜j¨s3|ŒNýãyFÞ™­JŸxC—û¾éºÛš¾5ÄìX8?ìYÃtÛ[Ú&êtj€'¸,µÿÅœbÙŠ ã¯-¿3g)j C_®\Œ}á’^fP8 p#'O #hO÷p.%ˆôÍ©íÀ "¸lïçFa*<‚Æ&86TBêÄ|ßc!¹ªol•£åýÏÔ…~±;Z—…Y©â#øu̲s k?ùºªka J…ʸª®IÆN‚Lˆì9T¢$“Ǧ@ÚW.o~ºMþ¼Ëµ½¤³¤ý£Òʯ-š*P½«ËmïÁj+Ôãõ§ãOSÇ)»}ù`Ãd¾©–/¶ç+;Föc¸ˆcE {l̆«'ýj—;%ÔT¯ÏýbÛ±Âä2¬]èhœÙMâ¢ÂåäQ®mh‘kΙ-i‰1òÇ—>’W>Ø Çœ0g–3‘ùdlZ‚l†D¸¤²^Ùò ;š5aŒrÉyj¥ M"Uø8 MÈH’¶6—<·r]§Sã£í²ÒHJ'_ñ©’>Sº9Ü1$ƒ ,rD 3ÄX¤IŸ7:Ýa}ÿa•fF:Ói€±‡;½ïuö£$%š!iã3þlOƼ°çÌC))¶GgÊ‚ëWɒﮑ¥ßÛ ó¯{GbRæö£ûÁS”6«3lo‘‘&66.ä*ô|¸¤ÉÊ´€0¼Ð+Ò+ .h¯K…ú ô¸K©6Û ècôþQ»ðÇWÊw©œx5QNlÃHôÄ¥Î2 êÏžˆ¯†þe³ª‡',ù(@ÉÜoxeÇ•úoÓ»ØßÃøß9OüÃåÜtÙÚ†Ç0›OÍ\ ©ðtЙcã(=ë±ÐÜþöXD?p"`ÜùNÑÀ≊Äq¶Ç¹>4.!ñ{VC‹{´yËÎýs‘‰S$oí¯¥ªh­º¦Ío}GBìI †@E}Æ•¯`.0«}7?.û×ÿ̼è)ì3‰Ù![´Øø9°á2îÌŸI挛PäÛnÃkWʘé7 ççÏ/’ä±ÈÔóþ ÿß,9ýò—'Á†Ù¬ç¦7®Çm<øÝ –·yÂŽ¶–Zt‘NNôô ¬—3:ÔÒ›¦?ÓË~8€×˜ ŽàPÅÞ£$’'nsŸFÄ“J}‡µÚ‹?dôøÍ_£`' {F•Õ(/T[ˆªÐX­:}``¸›»n¸@…ÓalEJ£x\§€â=Èøý¸€ê^Ðu†¯5ÅÓ!è9Û!` ³˜ÍÓcLfå‚f5á˶TSÂÄY"ÿJ¢“gªíÝ!‰c–!2†C6¿ñ Ip•dþrhË“êxcõ~ÙòÎr™¼ì7’œu‘äozL2¦ß({W? U…kdÞ5ÿ’ô©ß–’}oHÚ¤ë$,*Câ3Î’ŠÃŸJXä‰M;SÖ¿z™XBbdÎe/HÜèERyäSU÷`þÙÝr¢|]{·ùÚ!Öâ˜ûD©ƒÙð Ö­ÆAd®^uïÐ$…=‚Dõö$Xäs…ÿüMâ'ÕÒþDûˆ)c8C5Þx5«Òcg~迆˜J¢ˆ6µtr Dâ±òÁG_gÔSÖÍ£æÛï;À󺥔:XìÊý·Î¡åíÉ F«¸õƒÚ°€ÃãAöœAngP/bp*×chKSKq-e"šá¾!£†Š=ÒXµOÒ§|v…yÒÚT&Q‰Ó;Úo®/„ â$ÙüÖ·%vÔé•‚F7öѾ±'mßùÔPcq(³º4Õ5ðˆ¶…'‹žËnGS§K¡ [œ­ôñÑç/,Ö6á0óµpGÁŽç±¼ ÔÖþãÕP¹WÞ{ttÇ9Äh»?¹Kö|ñ+µ r0(ÄØ,È–c¬rñÔJŠmêóož<{Ö%ÕUµ/ìÙ±éo{7nä—rPÛ/’oºJz0î½Î~qÀ÷6ã_F*¦e§ÊÔ¬4¤vtËZäŽþpãä†Óɹ…ÙxøÉpÆ” ‡p7þÔ†€ÜôrîJ”`jatºão¦( mßÓÝñáÚWW±K6¾þ0®ßˆ,6FÄ =Ö“ $OsÌ¿ö·†Ýx*B*D=”.ÆÍ΢èc=íÿVdòHçþp÷®Ä!è7HñοJmÉ:súªÂ)þ]v¬¼ŠqrÚ’?Š=n"@Y Ê=¥ÊŽ÷_™4G+¶‹=~*p¤S…Q‰I[")“¾#E;žT’À˜ô¥8æF¹’ûáÍ*äJÚôïKêÔ›•gnCù69°úNiƒ}]\Æy’5/RÐHq@•÷ÙÏ ñꬺ´†%Iyþ¿¤`ËŸ$2ùt™|þ3‹¥òPç¢q%ÖR:ëÏ?5|.žðPñÀüzЈ7„ú„¦5V7Šù L5eO=¡7xÖxo0œò™a\Ȉ ‡Î ¦v†=‘?Xì©Ì±ýÞÁý±2¾­ÁŠZ; Ö=9ä}„h£‘§ª<£¥Ð1#Ù’4õ§‰ç^ô½9‹ÎùÆß÷ð‡Zù`]ÿÖ+Ñû­s@ç@Àr AÕ IÌÂSˆŒ+{Ì%ýÜ;ëdáôq²~×!ùþUKdsîÙ°û°<øýˤ®©Ež{½a¦c:A‚J̓>™bœjîù8ãîƒráü)ª®û‹åõO¶*58sSó‚¹2:)6`øãu;ÅÑR-m F„…sûFn"¥ -ó“-¢#ÌÀI3aÃ) JM–PHè¦Iþº¨—À-—òý¯Iéž$mÚmrû-U2zöÅ– {?¼M¢GÏúOi¨Ø 9‹„Fe$†JIîóRwtL¿ì ©:² ÒÉW$*eZò×þê©”‰gÿY³¯–ê#ïKúŒÛ¥¾t“”íÿ§Œ=ãnI™ü=9´ñ×2fÎ/¤¾l+¤‡Ïcû¿ìOòå«Ë:I5÷}òŸ÷ObÖej»`´¿™§ 1ü$_/ª®ÁâÁînJÄŽÐ¥||5ä?#Žø%’;ê,Éo;SÒ­Û%Õ²K¢L%èOÐ Âú;$ZžA»ãM‡%>ô°L´}hÚÞzq\©aÒ{È-}ÕÿýúÞ¡±@y¬û}Ý:`ì7Ëôtè N»¿:ˆ€*«ªUPëñFàÐç[òd!%ŒMÈ=qcäÊjàÉî‘ÕÛÈ¬ÓÆ¨”nÌ¢ÎöàëÌÎR‚ØšçŸ9Idÿdó>IŽóyO¾³z'œ¥ rùÒ²3ÿ¨ªÇœã¤wbpLJ¬,š‘-ï¬Þ!/tþòÆ z0{Ñ(ßjEÌ9£„t‘06CÂèv;Ú/¯½ ˆÉ… ßè$ŒîS¾¹Šw> )ãûb‹£€ É×b52͵”Ô0nÌyÊ…`Ñd±«cÑ) ;Foû[Wú\Ú¹:ÀÍåjÙûÑ÷%jÔ|Iw¹*™!1造ý_üe*|àÓ`–ð„©‡di®Þ'±£Ï »¢ãÅ3^šª÷ªs´?»Yó„Dò|)Üögô¹Ý1C+ЇµÇkt:=Ò©-Ó™{ÛS÷áÌ“+ˆfâfhâ-&,f`m£Ûà,ùØÐZ±ÝèLœk>?ÃrÀ¼O‡Ça,“q!k £-ÛO®ÁnÎ2[ÏŽçAíÁ”ü±MJ¦‡"æb7—¬vÙ rFèˆM-×ÊQï¤g®ÿÙϲ_úÝïhoÏuOýîi¿{⌾_çÀp 'g…µÂz Âã0D¹Åû(ñ¢Œo4°OÞvHS#QøhÆv4rhÇâ­>+ïH©ÜñÇ—UÖ-´ê7'€#c[ÆD÷î`0—Ö©‰ÑÈÜ2~t¢Š‹¹ Ò¿Še¤‚]ºxºœ19SûÙ±Þ{¸TÒ—qÙépù^Ù”{X«D~jJ*¯Z6Sæâ<¦| ù¨I¥5Êi&9>RvHZÍØÛ\§Ò¦$D©2Ãý'*aŠÌ=û;’˜/áá*ݡ֧ÇW|(_î>°ö¹Gºû°œØSH;yðÖ4Œøîi¯¥[œuªÍ¸œíök=P6@ŠpcUn<²ùàa›+‰c/·«¹GoèÄì+êîWRÄ£»ž‘HÜ#$‹Í'a¦'5‰H̓x„öpŸäÛÑZ©Ú"p­>ò!léªU9íí'û7 ‰-û?¿ªèwµCýZ—5Eïì ï#8© " cTÜ÷ÅaÄš/>\€â*CEþmMw iÉÅW, µ‡G6{b=À—YŒù¡ÿÒ9Ðg¬X±ÂôáÁƒáÒŠ—¯Ú>`€†ÍãSJ e ‰–£…Kò„ë•ä³tï?:5•>óGª¡ ”µtSÕ.iFÀþ>|¨…æÍ±º‡âAÖ4ßDËôüà€)2&ƾèkW,N3vB¨¡Æ{ší-I3o7d¬FgK¬}ùB4'Ê;ºhÏkR¸óyuáQÉ3¤®tžm“D%M“Æšƒê9L_aŽ#&#ÓûÑ!¦»ýªbüéšVPÛÏ5C;Å! Ã÷4Õôo¼üë9ÙíPc=Uý¤,ÔAÜE½ÐIuÀx²w€~^Ðs€!ZZZÛ Îu*•nsǶSö)‘ˆè˜‹o¼3g²Ùl ëy†v0øÁö}ÄUÞúêU€?Êh ß /UL‘áap˜€=æe¶Òs›ýí’¶@ fÝÙ°û’0nÞsDu-5)6‰½ófÊØYémyEÈIí”’ªz¥’NŒ‰x ü{ín¥ªÞ I¢FiIÑ*|Àæ’ÌY·#_ªê…¹¦u \Ð9¥µ¡N1Ë–ž'ùkïVö„Ó/ý'0ŸQ*¾ÔnÛa‡˜^¨"ÿMÅ dî7ÖÁ™f½PUMJõ2=© ô¸´Ô’Âí>µ2íS§Ü$3¯ð…cÉ_ÿ+:ý›‰I] ~¦ø…î¡Ô³¿€>|+Ðu›ÀK§W~5·ù/SNŸ—>wÙÙ7„˜½¡“mo2,_¶wm`›§JXËM-Íø›1'ŸõåÕl‰Pao˜ÑÅlSiýoÑb‹€4ª ÆÍt]÷³L×´‚G¶?Ýq™ ¿ñžQ‰Óäà—–üÍvŠzO"¦£±¶ªŠh•`À½÷—ßPt¬Ÿm耱Ÿ Ó‹Ü0j€hiuH8¤¬º^êÂe#T–LØÆcmm°çãšå4`èÄ>îw(goWc±XÎåq¼ãü²,õ»ÓN¿ã'ÞÖkCêš:¤”­ø¬ÃlX‡Zk¹†0¤Ymjyo˜:|x×<Ø6˜ëZëªä溳fÎøÛìÉãκýÚs:5æ¤1{c£üõÏ:í„tPáBEWdX¨\0o²dÃz^»7oj&ì‹å¥U!)´K:@¦fßxñ¢éòƧ[áq½W˜^ð`q¥ŠqQárõ²Yòé—yòÈ «¼Ý$W, OdMÈÒk“úÁæ@þÚûþîSµÖ•luÏ‹X¼ëiá¢ÑÖ×/Bæ;âõù¤Ä{>¸U,P/ÒÛY Ÿr`Í="\4‚´Ù¿ÎFdÙ¼b B® D œ^ü‰ÞÏ– «Œ!­ …‡*¼)\BÂÎ ;HJ(»Ò–^Ðu×Iý6éæ$´c£Î›ÊßCAœóm /¼dñÄ™§ÿG¼ùˆÌ [a¤­] %ŽÛWý1Keé÷ÖK˜%ª{Ýí‹ÌÀxuN+X´gEÇå$f,S óèÞÊ¡­OâÛcèaÏ~Ç"¯xœµ?~o7:Æðeć ¢¡ç\P±Gïì`r€Î¥•µRVÛ¢$M~­°ÁóIüô¸tCgû1€=ã|tC›÷t³wàvì9½/ìÊ uˆ W‹,_uøN¬ªåš@¯ð‰©Î#n€?sÕä­C&€ZϨȚ§n½õøÙ¨o]ä—icßJ@)¦üë.ž¢Öµ c’Ž;N'.$‚¿;¿s¾ÔÖ7K4£fYEoiÞš÷3m"é “ë³Ýœ=qŒp©©k‚`¨5†ÁZ‹ZW)!ì/1¶cW°¨ÕÁþqµý\·5ûÿ”m£ÉÄ矀‘h–Hídß8µ_Dbž8ëŒô‰3gß“d9`8#ôEde~s:J»Rm)<âU,MèÎ!uäG©ëþnÓ ú‰?ºÿ-¤œ&“–>¨âoÞö´”Dœ!¢×„Ø™n(Èßû×Ò‚^Äñ;D}9ÕftÀxªÔÏ?i)©’Ç^ýÞ¢^e‚:u‰`#A£CC¬j¡×§Òµ*•.·mð.æúhE­üûóÍO¸Ž×"b£KM!ÆÚа°ê?üô§4Rh¢ëåH"Ú.ÆDùìµëb*ÉÜCGe×£J5] UuR\„ŒAqêzžÿ1}[çÀpp ]ˆN©ÇÂõPFMº6}þ¢«¶)tvèkà Û š&1¿t4ŸõcÔÓ{¬óþîÒ 2§´FTe;[ë䣿ͬÓÿSÆL»Aö­yHÙ­jek]éÎM-×yêª+6ôú+ï¡J::_À`5>õê€q˜ªW98 jÑfõ<€Ÿ `]c¾@‘d>s‚!‡Kœ#|¶}¾µ‰ýà̘~­MM{þùÔ£q5Tu/ÒœKíS­N—Kê›Z¡Šo‘†æ6©®«—²Ê©ih–Ð0ÚÓlºþ¼ÓåèÌ:î#Ãm’™’ ÔÏ#ûª‡ùêî)f~œTóʆ‘ ‘vŒ\†0ò‹œó½Ùl1OŒ2!z€ax¿}kŽn’&8¶œqõëRU¸];9bZÁ0Ø)2­ Óþåoú*:†ÉêÊwÊØ9·Ãfò›P]·É!Ø0E¨"çTÙÚz…§¡¦nÿÊgŸþƒËå¢ )Ç›&Ç:ˆÁB:` –‘ý´Ôe§%HTdÔ†>ÁPDšµj 0 Ûøî‘ ›rISS³,˜6Nìö0” Z©ÿq×x ¼±£¶íw´¶¦´4×eæ$†”yoí®NeÝžTõ ÈÆ_$™ ®¯Äsšaº @LiDjb´Zúz¾^î¤9 &8x§×9ÅÎëû t“#ïDòÏ)ÕÐ`ÐÖƒ}±/zfÛ›jk‹êÂF\^«˜ Ä/CGïÿy\GcÌêòÅ KŽKÈGóÞTåh›èŸê¯§ýÝ¥ô?oõ?Ά÷¡Çö`ƒE×,¹mçÈÇ|©)/Ùúï—ÿñDKKc .ˆã®99å§—Õm©ÿ$#sÇgÍX´#ÍótúaNN¤ÃeJ‚CL’Ç`d~¼$Ãö¼¯Ñ‹Èfc¨1Òj0B{²á´ZÍbñÏ3À,}Î9i‰±rÙ’é}î%|¾5Oî[~qç¼·n·‘S³R{,£89ì”·¸#uÀxrì“fW„×Á46CO”0ò%ºcÝëξ꺛v´}Í8ËöÆÐ÷¤K‹ýKØåd¿Ÿ'ªÇÑÜÙÊïÔÛ,s—m-»[€àí’¡Äÿª †n†  ç—!mY9Ò–y*âËŸzª# ¿xžín?”y¬ÍÁ0jiÆ™ d*,­â"^{îõ|þñÅTPn ìNŸ*›ÛSÆÆCêHMc12wtBöÜ|å"ÉCúÀW?Ú‚X›™=aŒJ øÃëÎ ØËg?­°ÛÔ%7ìV•Ü>ºÌÚPˆ× ¤†Ærd¡)⨂ªzP& J¯ ö‚”­E…Ü Té:Sòþ¨E fd‰=æ´qiØÞ/Y©qŠy|Žï½ék팡3 yS2Y @|öÕ±¢²jIk‰”\NÏ\gJæk!%0*R]=#üòQ cÃGì=é½ñžð,ÆXSQÇéÇs Ú&;Û.ôT–±kÓ†½(Ap¡I›Žÿ¢<¾ŠØÃ÷©*%øaÛ|EÞ·Û¥8:>ñí‰3çLN›5¹*îœq» ç™ÌâðĘ‹%ÖT`Œ0VH˜±Z±X ÄA'&$7ªoÝ RãN‘:O $tcÜmÞH“×íhƒ©MëÏõóíÏ«úO\ãÀ•pxC16_óBµl¨()ÞjKŒ°†¤Gà}¼ ·ÛÄ?M-µM%ÕMu{ªJ íÍ-öx\”Ö’Ÿ\Ÿˆ@œàPŒ\sÇšeùì=5`$Xd¶W †.¢tK‹ÃvCrrÿîÅ󅞯/¯Z/ß¼h¾²K – ÑÁ´zÍŒiˆáyö.{Tô¹YÑ1É‹|)@Z¨aÁòŒ€…š”ªåß/=û‡«n¼-{÷[™óì7é ±ûŒ`q]Ó·ÜÍM…ï¾øüßPŠ ‚ƒï”¡8dÛüÎÕ~óýÄ}áXÂj+Ë[Ö}ðïêu"ëÃÂÃmc'MÍHJ=:&!1=*f^ºÑdíx¨Œâò –£Ç,-‹± Ž3L· œ,êÓcñ¶y"Äa°+ÉêG«nWscCimeeaAþš={¾Ü|(>9)ê¼k¾þO¼·ÇgX63¬›$ÒÔ{ÊPU×Iþq¯*e)qOô;¦xq±eõ‡+¶®þ,—=ô«–Û\øÂçÂñ"ÀæB¾q!jæ˜j€QÛ&Päqžsü„ÁHA  ÿ•Êz¨D©†Ö%‹Ã{ ’ÿ/˜*o~¶RDZ’•ž ÐX·˜ ¤A®ë›(ÃҾݵ¨?5ÑЋ·_­Èè† ¹ ¯€2Rêd¯ÓRþÔoï¤bȈù”÷Ô– ¯‡ÄkCȨvÍf1Êç[òà…œp¦Œµ©¥ ¤·³ 6—ýúö+´ÍãÖ7_±¨cŸv>wø‡Øùk—JÀˆåX' óð>ß¶_âìŒ/j…¤Ô$ÆnR¢ s7{kž“N3lñª?|ýåÿ:ûŠëXm¸)cjÈ»FLö¿#fnì'spø¢#ÈѶ5é£XÓ^"¤¹±Ñ²kãºz,»ñ›ÖÆðèhœîâc’bñAkµÙB-V« aÑBLæp+¿ä<‹¦ç6W[KM=b}SC}}myEeáÁ¼2°æ}ÄE³Ò¢ÂæO>þ¿óÏ¿d‰küŒ…sÃB õîdóS4ŒGË$ÜT »ÊF¼Žýñjè<­NH)ÙlòÄBº' FQåLu7y€mk®¯(8¸{æß_ÓÔ€¢ÇÀÖˆ?XÔ€"Ç !yG^j kÒc–'µº°üxoÕ>ò”“£’Šw¾Ø!ñ1áÊf±§êÅ‘´]½#_ËmWÇ))ÊpÑnn%0lí$9l¾Aá ùàiÅ}–/SÌ}øÉ[V7#»òÕk¯åËnH /`Ãõ?ùIªË!³L&KZÞoû :…SRÅ|Ë£bìa“U[ íKi2¨äªym‹ªÚ&™—‹t˜%§z}¸ž“à·28AÖ=r¤ðµ'ÿ|÷ù_ÿÖ<)—,Üß6Ï3Þ¶Ö”lÞû• ¹ÃÐ9ô†¦ƒ mË‹ 6¼÷òßÿÞÖÖLC@~TÒÞü#p#?‡š´1äZ“”±?”ÚÛ—P¬¹ÐÎÑŠ…Îv\LµµXªóe'A—&9ägu×Ok%ÀÅö4 Èk×ϳ´67Û>~ã•w@?8cöøôì “â³3­a3aŸÒñUåµH£ÇbhEgb1:|>rh…jo§7Äë€9€"²cã‹ÙŸ\¼µ¥©ª®º¶°ªlcÁáÜûŠ sLð´EñÈãZë3û«EMºÈó´m®ý¯K«»G-`äGJM}ƒ(®@àß'E9·Hÿ®„6Tþë³mj| í²Z;?»ý«±o¥[ !dJ<ævâÕÄTy¾ÙŸÏ.a!XBU<ÃH„`ÚÏÛ¿Øô)R®@ üúä oH)!y f¨ “n¹ï¡‰aa1Ñ.³ÓŠwc§ç6Þ£¢U,BrºÏEi˜E2 +dåêʾT—¾éÐ×XAi Æb‡$Ú͈1iW9Ë­HsI)c©¤y]œ89QÒFËŠ æ7Ÿyâ±Ó¦ÏülæüÅWµÄ^|Ú6¹Ô`“ZO˜¹Á`‘¦ÁøÑ‘á&fpaPnÆYdèœ:xÎ~¹zÅ;víØ¾iáUè1K¾‘äãpßeÚBÃþ4²o”.ú/þÀ‘ïJ•ÄkŽ/—®€‘õk€‹Û¼^.lO^ÜÏóØk8²Yµn_¿f–]øm²Ù¬©c³£ââb£¢£BCíf‹5Äd¥D†òA€„¬0—³‹£ Õ47C¬Ù\W×PUVR]~´¨ÿþýФ„š ™km\XŽÄþsÛ4²ÿþ‹v],ò#(âºušx´¾æ¸;NÙ‘W¤â,2tŽNÃŽÇŸlUã³xv¸’¤ ¦¥6mŠÊ•Ä9ÄV‹|Â=8ÜàÕÄt„ m­F®ý£6Î|aqÑÀ–ª¸f;\ºxœ÷#¥›íkMºiî3!c y©Rm­µ…C¤µ«µ­­50çX5u2ǃÑ)¸¦”exI«ýÔ®Eã™V'÷kåyΈ¦ ŒÌá`,©¬A¶ LþzœÅ€ºI9Žlj’”ÁŒ-È-Ü5zMHˆ €}áš}âö`öãd’Dsq›u¬×â™hðìŸàtyøÂìDBÁ.HŽÔ×TDÅÄÏ‹´gŽEHš®D's®9**BÎ/ëòÊå‰×?•KNÓÃMueØ þ¦Í"ÕД,ÚŒ^Ÿ€û&ááj|8N¯ $N–”Æh“6'MN´­uUU-k?|—‰`C,§•Åæˆ#`¡"MbG°CÉ¢f¿H¾‘D ÒúN§-Cmá>nsÍW[k㫱Æ®5`ź¹Íµ¶ð8ëá½ÂwÁ5#¥‘ÜOªµÙS{(ÒîX§v=\û·©I79\8F\5•2˳¸í¿°NÿãZ¹¯ÄštDgÚ/2<¥E:8.Ž“Û‹’AtBÈ/yq¥H‹×&s¸9)>¾k—ËÑSX>—IÅ.Ãx¯ÉÂ×SW#8Øx½‘>0×cjÛ÷Ì9|á1p7_jÝ1ór0F˜ÄÇÆÈÜL—l;R¥œ‘¾Øv@#Šо[ö ÈN-¨=SÒÖ4Úâ‘ÌX«Ä!ðxdd¤ŽÇ)?`úÀÞ¤œô9Ñr›÷#¥Fœ|)­á¤Ï Ÿ“}×I»Fñú¹hà„Ï(Á%VTñqÍßÜO¾±| ’6–O Ðõ¶hå4°¨]—?_Èÿ…õó7‰ç‡ð^"xßø«Áµž@#Š+êÚÛÐŽ ù®FMÊI¨mó8ËêØ kÃOANÃ*³ƒ=œ#:ÂàÚˆð4Hÿ8^·Á²Õ¢”&!&BššLˆÓgf: DbþhguC6ø1 ±³ÑG ÔuX{GÁ ^>bÆæºÄ™÷lN'8Ÿh“i M&#%¼n—K¦ÃÓç(<ª‹RïM¤Í¤É@òOGãù lp} --|$5#‰@+îuˆIŒnɰ{%H1ÑQð­Æ…ãÃq bâM«FN´œx9éñ+^“ñ"¹h€B[cWГöàr­òÏ,y¡I°ø›ûƒ ðš¸hÀ› ’c¨£ÿ6ËhäÏ­.m­•ÑÖ˜#€#Ïøžã¹?Pìúñ¡õÅT_µú5pJ~s›k r­-Ú˜Ó¸ ûÃG9³öÂÚ/R%M‡Ÿ k/…õCÃÆZ-q|8N/n¡½Ö)óï'¿ÿ}hceËi^³wbKyÃ8p/=¾çüÞÃ^03ó מ“sÿ«99œ\Nš(µ¢}œR,‚uí9áþH[£Š3YÛìÆWƒT57ˆÛÏĬ³„ó¤»ð•:QKgˆTŽbÆÐ&B¢bP5a*†+°+ÑQ‘j\8>A*]ô[NМ|y3sÍ ŸR"Jˆ´ ß’Çî Áí`'^?‰k`BI–&½Ò¤[~8\È©•&¹Ò¬¾¼?ù>Ôî‚D’ÿ}¤óñýÕøì¿ÖêÔöù—×·ûÉ ŒÚÄF©‰>Áõs´‡¸8ÇG§‘>V?ÌɉlóXNª­sŒÁÜÑo¨± !7öyÌæÜúIY‡3ž#ÕÿTy††Ú”z”E1áöFijnV6ŸôªÖ¤Ú³5Ä·IÐ7G^sQ |¦½®Í¢¼¡éàB[RšPâKÏq,óŒad¦614i“½ÿÏ®u7Éc—¤i ¢ñ@[H_áJ4Þxë:ÀHžé[Ü9}ìBocÕ€ Ú¹ùGa߉%ñé"ÄDv]ß¿ó×1.òK nvI:³¯Ò5¦˰‚‡=ifçH•¬±ì;¥bþ QF¨A #ÂíPG·*ÀH§$dLí$±×}g;mIä5yl$0Ÿ :‡Î-\h³¨…Ñ`ÑÇ„cuÀtŒú–ÎàŒ#†û_ñ !˜¬ih”Z€ÆQD€jIŽ‹’ñH)…`ÔH·ääÄ{ÜÖ‰È|5 ÝQÝöÑ`¨(Øcq{sð—EØÖ¾”»->˜; N¨"¥ô‹ë„8²Ûä ù¦ËÔgcê³säx´õ`ök¤Ô­?®5 #%ˆô€ã¥ºÜæ>×Iç€ÎÁÊ ŒšôCŸÔ‚ã–ÓÆI7ÿ^Ûá;=;UÂ`›º²®Y ˪åÓ­û$#9A¦d G™îÈI4‡˜&Lò¸¼‰4Sêª[\@ì9Ï·ÙšûtÎÿ;ª]矺GÛÖ5ÁŠÅâSS3.&¥_TE+‡$ؘrœ´±ÖŽi㌠4ÂŽ‘^û”6ú£ÚÏc:éÐ9 s ˜9t€1˜™­÷½g„@—š%£â"å(’$ì/,—úæ&ÄU̸9>KÏ5 Ì‘æV‡ØÂÂÆ÷Ž{oX g­]@@9öåŠÇ¸ç¯ß]60-^- FIM Ñÿººöƒ×£‘Q³?Ôx¬­GÆêW¡s@ç€Î_Œ#'Á'ÂÅ1Si¦†“¸â¡9…¹¨Ó™)ûv(–¹‡dþÔ,¥Öšˆ*®”½‡KàUl ñïv„¹ ´Ä`0冸$÷ñ_ÿ’-‚Žšà|×þ")®¨AÌÀijië°½Ôcÿ‡“€‘¡u˜b’ëSã£eJvÒP2¡N:tèÐ%Œ'9Ž=ó®Ê–qí9³O²ý´Þ8…4~ÓÆ¥Ê–}ŽE2cüh¥âëíœ:V »JB.ç"£Á˜k·Øsÿ˜ó¦ø J*,­’}¶UrÃ{Ý+1‘a…|Ú6[Ÿâå5e§Ë*[$ïH‰¼_ß {EƒLÌL‘˖Δô¤8¥–ʾèméÐ9 s` 9 Æàhk›^ÇOº­—ØzÈ:BÛ±ž¤“”^jÞ¬ƒeiï÷U#‚ÆìôDÙs¸TÒcZ-bH@ã”±©røhœBÚr r·>¶æƒJÁû!ónèqæýóòªõòÅÖ_O ¤jotrŒì`lBvS¸I…( ľZŸœÈâB°X^S/·^±Dñ1Ðú8’ûÃ{wÞ´,IMŒ•çÞ^+O¼ú‰üøç#ÜŽeÄ‚Æïÿú×1ÞF÷E£L3U Œccá.6¸_xÃ~!l–AªÑ£Ç»Ëayç/wÝU3ìÝ ²ü çñp¹&ÊÜêµÂhOOˆÑ+m«ÙÚâñ´$µe5åä\ÛaOd—7"º«ÆSƱ©q°­K‡woŒŒ­ˆmg6åök–Ê®üù|ë~©¨m”P?uõtÃÏ™”!³'Ž-ÙQ”+V€‘Ý úuùå UÞY½SÒ † !i/Ô²»H¯X:C–Ì/»pîö¼"P 8I3NKWêÛÃ%U²À4Á°S£Õ±`þeWsN ¤4ap$ *°¥ŒÁÌ/öžÏ¯¬âE•‡y0ùÁóÝ‹çË“o|¦L¾yÑ|õÑ3’îáä<4Ãáò<äjt £Ãdt9,&£~ÃÌþ!iÞê·ÕéöX³§Ñå¹éîûß·åî'¸oÇ!éD€7²bÅ ÓG;N1ˆ{¦×ë™3ê,A2‘+ yØ#œÎj#Lji]Õµ9±¿XöÉÍ÷<ÐìC’$” ªéàÊ<|Žìܰþ‰œ»8 ‚¾{:`<…!Ô¼ µx¼Œ%H¯Óÿ}écIŒ„ú/R±6)­¦£­°'óý*}Ù!Úwt VíFêC«_ÖJ+¬^¬ˆš'®özZ‘©C£I™£(Ø,{ edXš3¡~ Dql8©úwAÍoAjƸөg”TÊêmyJ MÀ¢Óðr€cpñ‚©Ðl‡Ôq¬d!8=©;å䬰uæ=êtzonµÝGcì¦j»UfãñFÝÁ~±½÷_åζBSÛØfJ©m>×èð^póÝüÍVõÃGý‘ýô^Lj;ŠWÃ-÷>x æ®ëWíØwD†‘¼H|L8›-&S›Åht`žs˜#ÃÔä ‘˜á|BifÏÂ9É-Ö^±º¼af·;,ÄåIszfÚ8Ëã1s¾\~÷ƒ‘ÿø|é/Üó >Èt >ÀwT𿱘!§Z]%$Šm5 ¦gIæ¨8yç‹Ïr,Ã=Tó G+•t ÀÞÄŒäŽ&ùphDIäûreËÞ CÆJ'Ž%fH×V­Ï•’Ê:HGË6xS•M‰%Á"AãÆÝ‡„Î2”~Ž µX¥©µŽ(.¨ô¼Œ#åÊþ:˜¿û_ŸnU.´YÔ)08À±X Ó¾n»:NÙãsö—>üpBqËþ•˜àçN°J£BÍ#]ù|¢; @YJ£C¥ ¼H®k‘ŒŠ¦[¢êfÜpÇ—>ûÈ#4H?ö’?QeÁ}ܰü—¿:gù½> À—Ùd³¸ªÃ­æ:|ø7ÚÌâ6âƒBÕÓ+;滨fgBl“㻑Ëo¾÷¡­·ä<´ü©œ»·hl¼åÉ'-æÂÚqHW0¢—‰¾$ã~Å×´7ƒ K iÌlÃþzÌÇEPe¼Æ"D ÜùÄwøªƒP0jwӭǹ…ªä—ßߤB–¤À9åðÑJ8¡øL/FÁ‰ã…÷6"—¯C2Râdüh$é†N‡Ú:÷`‰¼òÁfu4.*\–B]T^#Ÿ~¹O¦O“ëàLCcú·0C= ‘BÊmy…P“G+ÀÐMÕA¹Ë ýNmÒÙ¹û™Itê™õptÙ‹ð.ô†V’éž‹êG†‹E3²Úh›ÔÔ7H$ŒVë±É!ìÊ)7õÃ?ý)¤¥¼þm̰³v¥E›t:ÆçÇ›Å4¥¸v¦9$ò­K.¹eéÊ•Oµ TP‚F.cIuªÇéNóŒ±&£Çäq dƒXŒ7®¹ ઺ÕÙVƒïù‹ð1ôh-â¡»4… ž8½Íb.Õö´%–ˆV§dT6Ohq|±üÎ_-7˜ ¼Ã…RP>ú8u£zŒFÎñ:MƒËl4¹€)Á¤L’Nˆ,Ýbz¬.·Ùàuã!uCþ`Tâ[Pô döz{”é® 99†¯Ôd¤?åÇžñ~måÜ|qGy‚¹ßþðÊŽß?¼î,¥šŽ€ëêЛž«` ª¦7¥FÜv©¶©Öáa!rûµK¥“?âÐÛZ£_ß~…¶)?ûæ¹ÛÜHˆUII”Ä±Ó ÿAõ½o'æ>¦6zxpƒüª¾ûäÍŽ¼åøDi³NÅŽÉŸlÅÉâÙáÊ–1¥Œ†Ö²ºG1 ÏÞ•ªƒÅÞî0iðÈ<µ°vV¤”Çd¥ÜŠò., þÈ#öººÖy€Có<&ïø-ÍöTĹ5&/ÓŠú®\]í±yšfµU*ùI¾ù¨ýà¬ÒÁó(óœCUÛÒ!Ít×Ú­ÆúP‹¡ aîZ¬T{Ã>œX:Ž[1ÿÚÛ\boqÙ#Ú\ ¢šó€”Yäy¨ òu1˜žüëwm’‹æF:˜2ÌýqÍ3ãCOD ã{*Çý”ö•è!M‰$cBΚ0rÔÑÚõ{ðà*°ØñrÒŽèkü¸ Ô9&"¬Ï÷˜v®¾|𹎅II ¢8!a´*Õôà·VÍn¯X𢴮¿á"öiwZ”Ù„¹¢R^ ΞLàÀ%Ž0ØÐcÑ›ÀÑ┘¦¶˜Äú¶ïZ]®›–ßóÀV$wøŸ'ïÿåK˜ßO¥¹“éâ£Æ!cµÈ×Ï=jâˆAk11&RyQg§'Á3{äúæSHÉ"ºç€Ÿý5È4ÙËK÷gê{‡Šü˜¬…sœ6Ìn‚ôW@o£×`y¸ÕjpÓfq¨xìí€W2ª¶Ùó®å,Te„:“Ž)pÎùf‘kõCeŒ†3Š«"Òf®‘f«©Ïcì¢dq ,h°›{HšqªÕsö´R-ñáæ˜F‡¤ÔµLjrücù½ýâ–»ü§ºgõ©¶ˆç§M r²}¢ÝaJBTJž\‘øh»rt±Yat°ØëA MÀØŒ@î¡zº¿^y5œéÄFf‡Ã¡Æ+˜>€®¸í¶xØbž o诼ƒKî!J¼È3Ð’¹ç—„sû ÄúÓN?Ënø÷N]~ÏC; {jÛ´mcbdkF¬¹(6Œ`±ŸÕ}õŠ<™‡*pãÎôhi 1OÁ¾Ïo¹ûþ‡rr¼#_¸ úêݲ_Í+¦Iv¨Fˆêzª¤=ºv¨X~RíêØÐvœpM‰\RYNBúÉM¹‡„1= ïÿÛÛ2!c”•ÕÈ”¬Y4sœ¼øÞ&É+`–3‘QñÑòí‹æJqy¼òá&¹æìÙÊSSîäMÞ*7_¾Hvî/’ »˃߿L¢þíõ*cѨø(ü¡£Su1Wø ïnâŠZ•ærÙœÓä,,lë¹wÖÉÂéãüþUK$9N¥¤UçñÝ´ìTõ›Ç5~t½†6„³ÑúÃÞüt~R‡™©I#ÍvåZĵ¬“Ѹ¯%ºÊö{ê«vn_Ö¼§IZ?ûrÎ0—1¢³'þ æNzówòP‹CÑ]¶Ç¹>4$$ìBx?»á ÁöuD„#Ø7ÒšjŠÐ œhfH>íª‚ZµŠI¢®™–t 4p\8>½§KÀEõÉå[÷ö—k»t’-J·üÚƒétò8RR-ÌWþï5»€ÑÆ;!yc®r;2QJ¸q÷Ù_X&—-ž&_?ït©ih’O¾Ì¨LBD[£ì9ì’{8#Bm „j@Ÿ½ÛàÈ<èçC¢9yÔË«::½j놦VùÞ¥ T¦¢÷Ö햃ŕB—ƒ~¬Þ~@¦K“p¨Í»REM£<üÌ»òðÓïÊ_úP¼Ÿƒü꤮×àߟ²êz¥.Ÿ0&Y._:Cƒ­ßqPõuÑŒq’•ž€cU¸ç| ¯§¾jçŽÀ5A¼?uÀ8@c˹Wñt€êë­¶E¸Ý`1OniWôv†~ì”8ÀüÖãKêÝmNÇá÷^|f*ãdÈñjê}FÐK£ÔŠK4"¯ï-®‘zL2}Íœ —4¢ºÅñ``â £"Õ8iRÆn/Ò‡û,¨×Þ°ÚZ«“ÒæìŽ/”¨V£#Ã%)6*Ô ¼ÅµË²õ¿×ìTm…Âæoâ….3¾£ílÅå—/T¿ßY½SåJŸi‰ sWþQ0~2¤“”Ò‘æ@a¹ÌŸž¥ÊøÿaùôäYvúS/¤™‡;ï:P,6ÄdJI¬/#Å'õ»v•gLÎì(ï¿A[E;ÞNÅnÛW k0©j'ù_ÃH<5ÚÛp¯\6S"Âlr ÒIÒôc\Z¢\¼hšú½9Ú5ꩯþJ­ìZŸÔ„gC@g òc4œ¢sÓ‰ø‹¬9N|Tv;'êÇ #æ ÊhË ‹@J Ã<.w+í °}RcÙß¾DX,ÊŒ¨¡¯ Å7´IVYƒÇÝæ¨ÙüÁû644×ÚBP³!èfS ,2ÿjj|„”)õÙ™S2ƒz FRçsP5>'m̺½F>JÚÒmc;©º„¡˜á«ÉÅßæŒm¸Ü.©®­“Ú¦fq)œ9“㢅ңh¿\ÜÇjÔ·4|÷ây’–©`ˆ/m?×Q°Ôˆª«Ÿ€BD€T«R…½u_¡¬Þv@h£8ó´ãÓS6ÂT!¾},8fl³¥ÅüÈHKŒFÞt_{_[8UR¢Å‰c¤è^²×ЖrQ»½"û¸÷ §ý¯AUÖþ§¹µMm™qŸ’ü3äh*cu€ØQPo}õ•ÐÿúsàDzäâ´$¹à£uÒèêÙîrT„œ;*Až?X$ÕjÞºoÚirÞ¨Dyá`¡<¹ÿpNísÙ8Ò»¦fˤÈHÙ]× +ŽɦªÚ>Ÿ?Œ ©µ57ÔFÚÃg"uŸ‰6vƒM|m™ì¬®•›?ßÐkS×e‘HØ4ÿuo~¯åâ ÏúϦMÕ¥•XÊ¢JUGb}«¤U7yBcMeåîÿùò£Õ•e‡qh™/±ö7¶‚”|oÉ ëœHbÆýC¼à@»[è›–£T™ §eÁÞm¬Œƒ=Zu]½|¼ylÙsýWç+·ûÑéy/%t‘°‘Ç] Ùb;vMÏN“ÂòÙ²·@ö*U l"$‹f€sJñ¡†þxÓ^•Ñ()¶³!+™26E©v·å)I^IU½ª›àžà° vŒãG')µ3ÕÚÜßj¥"½¾_ÿd›¬©‰1§ú_CÇNlLë³{\µa’f~¹§ ãðdôu?$¥´q¤V tª}íh ¸6Ž¿1N±ÿp¾x wÔ’“«Ç¤JDg(þÖì\; ·o„àþ`Ç'ÆÉÇ/ÄiD©¦µýC@ÛÇ5ë¢Ô³;Š„D¬'º}B¦Œ‡æâ©‡% æ6wN/¼†“$íDm}’Õœð4ÖÏ…Â!˶/>}ßâuR«‡×”+¤ ÿÏJI’‹FûžEÿ+ŠHÖÑõ·v^$èüïÆofgÊ”ØËºǸìÒ!XÄ<Óêr9jÇM™JO¿&,­Xø¥tìEŠÁHA'a$Xà Û )‡7BvR¸¬Ë¯– ˜(æ(è4¼à8TÕ6ɼ¬X5>'ŽÇí8Ò^eÚú¸~;PF+æ¿ö+Ñi“^¶é˜„UÔÀû·Rê›åŒ©YH›w¼ \§“õ=ràôI’{°D^ù`³*õÿÒÙ>õ5ÁæŒñ©°5̇´qL·uÌ›š);ó‹å¥U!I´«1¢#éšsf UÞ|é#¥®žqÚh“+û N, ÄëoUõ„bìé sá|ÆGîFC=N`¸nG¾l„*zbf2@k£:éÌicåTÔô¶Î„Z<&À Šzêkï­;š[S'ßýt½X£"Åmð›@› i›˜6ÒsO삈IÆ‹-ws‹±¡¬)ºÇ.¦[30I?2s²|p·$)—'J*HIÝNó½¿ÿ2wº|oÝ¥AxhúDÒZp<¨P•½cò8™%»kd"¤’.|¤ÎÇãuÍ©ò\~¡Ü?c‚,HˆSêÐÜÚFùÉ—;¥ ’êïe–oMW㸫¦AعOÊ e^–/wLÊV`µ ©5ïÛ¾GIý/­NPìç»Åeá(ÌþJC›ÃBfe=|ß禨(),Ôã?öþí È6Dÿ^‡Óàjh4ºjkŒŽÊÓþºJG¶Áâ¢.‹´ H3}©äΓeYj’l­ª‘óÓàH‡÷ñ]·ÉBH•çðÔ=±h®ÜöÅFuü¾ÙS…`¯¤¹E~¾~‹ì￸ô\ù´¤\¦ÆFËE°“ÿ»«“RÍT{˜üaÞ,™ˆ{…÷Ï_÷ìWÌçΚ§º{#î·:˜¾¼„€S%æ­þ2#V"ZX\¶h›uþôä”E“ç/¼µ¥¾áá—ûÝsh#è íƒ0rR¢ÔÊŠ/&þäON´×ËÊÕ;$51V8è4<`,<ŽC¢Ý¬Æ…ãÃqâxu'¹:©^r¶ò_´J¸¯¢Ê”®‘öPÙ¾¿Àà€,œ‘­úÕMñ¯Ü.zs鉸íÒN‡ÂáürûµK•·3U³}þÄø\ü‰Î$\H˜w~ç|©­oVf Ñ»ù¦ËHcs› Z Å c’ºá¨wû5gi›Ý®»^ƒxÂw¾v¦ºM«Ÿtë B Í›2Vn½b‘P‚ùÈ ïKFrœj£§¾vÛnväÔ´µ‰¹µÕgÜäUq“_5ÉJ×ßÍùC¾ ÀƒcE—¿!ëw7(‘ ÅGä$½Gr÷Ë73Óå[XÞ**•WË aÿýÕ˜ÈoÍ#qf?ÿrÀ`,~gÈÎÚz±@*9`€ qÅábâž_0K>}³ DæÂsn\ŒüvwžTµ9å‘Y“å’´dùÇo7F¶V×ÉJ´÷ÓIYr}FšüjÐÛO+;kêåôá?°ýðÌIrÅg;¥Ù|6ß'}¾gêi²`—À±¿`‘ À7x‚º÷iËÁe°¨½n%±f„ªÂ=¾¯®Zñpšjv Cì8q|°‰ñÞG……ÊæŠj¹oÓv¹€ªèGwï“I  -Ço·åªnü|Æ$ÙZY#/ì?$?Ÿ6QþwÞl9û…RÄË1f›Pdžò*¹ptJ·u0^±NÀô}ÐÅ0WøÑÔ «µòÀ–òe d%¤Ñïtµ"÷6— ŸbÅDð8ª¶5-Á`úËw?°ÌlqßðTNN÷ ªƒ\OÐFòÃ-Ùà½çŠpÙRP'Ϭ\-ß»d¡ù¦é®z‚Eò?šž‰£"Ô¸p|(æx 7Ñþm:®l3Ä6ØÙÍ‚­Ywˆ9éaѼ¨O¦~<ÄDušþõ”5uw=Íÿxoƒ¬Zo‡÷v&z¯Ìèb—y²}5ãC&&TxFŽ“0BÂi˜ÃmȆ„‘ Æëñ ^/õx=ë û8pÏ*P*äTÁH€™€gÒ!Hžœ°C]š¯XÃÚŸÕ35ºaíu\S7ÖdV †gEyµÜ±e·œûå ¡ê$F; !Å"ýjÇ^©D¹0W1ã^œ!IùõMJêÙêq¨Ú% *òýØ×•Ø8¡>‚_J¯úCÜHGän?™Ç³s@ÙþTÔ§²0Æ ƒ°4 °DÓ¸£²ÐXÕPk™êÉ ™'ªp«i¥NgnMŒÞ§úûWˆqRÿkó5¾ßÎ+ãñáPÖÜ*°y¶cŒ54ÊTŒ[2¤îûàφD’ãßã1N¤õe•¢ÙD0vW'Ë)f=ê%X´·›,HN€¤ñK$ËUí6ÌjÇÿi/’-†ŠÈ™PÜp¥Ûá­C·`Ä/„¾ˆ.Õ)`4B`‘0¼Â#Â%ª©IN‹o•½­òÄëŸÊ% §!8ïX%¥èr½úÏæm©†¦dц¸ˆãB%R'Ž Ç‡ãD•t Aã8ÄÜw¤L©:ãac`6x£÷áxP­}× (›KÚáR{ÁáA“ &ûÕÒY’˜Ïþp±øI6_ñ¡¬ß–»íÅ?=ò´E‰DÏž"Ñ™¾ÕÁ¹Â>çGwü¾-Ù6·o§ô\ª©dq²ïŽhg–·¯¾QJ EÚäñ}‡Ôï R¥*òž¼¡é`sçäñ²²¸T^:\$ÓÀkR >^IÎö6ñê‚ÊÚ+£FHUN‡j‹í}^V%µø`ð§«Nö£?ï;({ëäWpœX0ûaI…±n[ëöæÿú÷ `5"ÒÁ_~±ó")û"jæXáy‘Q‘Ó,ž’1qÊô„†02ÊÛl1z‹âìÆŠAPW;€‘ù1@b ì«íR!"U¶µÊQ˜pùájHùI%-í/»«“÷OÆ•¦<_¤E~é45Õ šËÁ$»i\iÃòzïcOÿþ¦ÖêþÆêÎõ³½ Œ´‡³´ÆH“fØ8´âKa<¼dU;0x»|OÍųÆË$¤9ÓCîôó®èCqÚŒÑúó­û•£B´Å#™±pBŠ`ŒŽ #Ç©[ûÅ>´1EèSˆ Ô»ò‹daäiªo¦.ŒAœð~µBB ÔlÃØç&Lö´›íšE†v’g:çô‡g HìO»n·‹R¼,Œýaõ§¡¾—å\ÚÞ¯¾Ÿu%Sí6)Ä;=à,54T6@59¡§&ÃnÑç$Õ[µ§EF”xå±½‡d¤”š }uE•RIÿÇøL¥’þ-TÕ_@­ù<<« iøEy¡\ `˜†w×kÅšù¤]–ôQò›]y2#ÚBé-Ý_‚`‘¼$hãÍHQû`F²@ÖDÍK õu®5ï®Ì-9r¨nÞ¹- ˆ²"P©Ve‡„(½·ã£‰’D‚:ŽG4Ì™>>š/_ÏÊ1vù‡fkبEPºõ¤Ûí²ɉN^Ó‡í°ŸÔ°ZÚ Ã»b(BþT„Û$K½ƒg:°Ë`ù Œ[PFr‚“‚ *‡Ó)nLX”ð uRÝÐ* ðnÜ*o|²U¢0XÑáaŠÐ:ZZÒ,u­ „Ý’a÷Jl„Mbð‹‘X$s\8>§@"ŸMcŒŠ±×ˆ/Ï(]5}j#”WP*O¿µVî[~qŸRžZk=ŸMàúÛçV)g—®p¿ÿLJ*¾äùó&÷\A ñª|ÔÍS(€Ñjÿw?xš '–"Åÿ†SÌÕŸo”‡aƒxûø±òìü™Jõ>œv ] {ÄÞèÝ£er6œXÞ;{žl†ÍZ $…T}S½üIi…\àÇåÂo=DJñ‘ñÛÝ´§L“—.­^>’{@NÿvÏ;$͘(Ož9CÊ! xb?‚ÐCÚ_B¸&ò“¥~„?C´qc[ì4Û7'¥ŽÁÅçÅ'É¢gœ]*ÃmFV…†‚¾€”væŽÇæÏ–¯´Fr ¶¾á˜Þ¹ð,5÷ÃiI“Löµ?÷ Ú?¾vîBuÿ¼€í°m¤T›í]‰ñ¦ãÍ3ûòûZåI—£ï¯ÃÑFÌÅ…Ït ïI79('-`¤ÔŠöqv¼.EËô¡¤æF on–ÄòjhóJ[‹S*›p£ø¾›ø5£Sÿ8à ~Ã^Øýˆ$B¢v5a*†+°+Ñðüüÿí€]UÿÏ}mÚ›šB:IÅ„ÐA@QŲЫèºÊê*‰‚ø×ÝU‘$:kÄî‚”€WÁ ½—4 $$„ôL’™ÉôþÊý¿÷Í™¼LÞL¦¼™y3ó=É™{ß-çžû9·|ïïœó;,–O&Yí+Î÷F$ÙS^‰|¢ ÚXÊÊhéô~J‹-;¿DðѶk£×ó™©Ð ý1ÒGb †É ë³ ìôÂ!Ù±…£ÅLEÇ$†Tûp4ŽS‚vìùαËѾi<Ê‘½á9L_ >ùQøÑwŸŒžÎ½´è@|œ¹3oU SÑãšuÞÆxÓc;áPhîB»·qÈß~‹C$yp EÖÅòåž)‚/_½Âs=„#øóy®cÿ?lÛmmøôs«Ñ–Íol•õ¡ÃKq?»¦åÎp,|Œ6P$§IáùÏO­„Å0`tòç¸h훦dãV´µö›=IU›ËÑ…q"ž•ƒ´PvÌû%ð%É*ì½Í-}~‹àYòeý*Ë–q0#ã G˜¨ßÇñϾèƒ'Î9㟾Mãæ2?+í2ñìûáq½°¢Ñ Cî†õ½•sÚÛªÞ¦Œìé\ÖV,žõר]¼iwinÁÇ{\³3 ¯ŸÚ¤&W>¿.—ü(ÃÃËù¤éGQ#ŠÛ5¾½»¶ÓºH+o1îë^á°Œª X MË"Å"Ûd±\2µ} EF.$WÁ}J×Ûej^X,°Ç™f•4­xžÃî㦚O¿ÿLóÐs¯›½xÉÒý Œ¤’,ïv­'9æxwû¼†ÑU_ù¦×¬¤޽?uÑÞH37Ýû4z7Ÿã IxëýÏ™ÓpL ÃûŸyÍ|óëÑ\â/p³…(µÕÓ›æA¸î¡å»ùšwéyž?W¦7aL¡7Vù·.¿xÈ©úý 6ŽË¨6ŒÌ×@«+m!ÐBØÛEû¸ÎbѦÑ]Ïæ#Y ùª)ƒ€éOÀ{Š VF›]ZLøÎÏúÀ§>ûÉ©³Nø<;d¼=>ì dG{ð#Mɶ Ï‘ä°VàþÛö±s:mèL3†E3½²1ÖÜÜôÊS÷ÿßN“Õnƒ]럖SÖ‚Ñk“„¾øØvŠ/}+bÂy ¦VF¶mŒÀÊa­}»nb1á8›œ)²(ÌÙK\Øf‘Õд,R,ò…Ì}25°½]ª¿Øœ!;Nq›©9Íü|í„`œ1iŒùäûN7{nÙÒî7‘¾é ü3ï§g}\½a»w2'b<è]û«²&c<ð¼êf FºÎaçF¶Y´ntz$ùIic÷YðÖrÓtÔ d›gâu‘ÜK5]Ç é°:˜Mþ ¾-³QuÈ*æ`Ö;´íbõò¥ï>þQ‹Ì]¯0ÛP=Ì^‘š¾ÏÇP~܇ì飭ó>ƒr«S,2 ->þ&:®p¨ÇãeŒîò´K¤¤1yœ7¶u5Ž}þiÇzÂ’Ç u“ãV·âÃ2²J›þ·í9àåñ])ƾö<ÃìU‘)‚1î÷I0ö÷r@{–'ËÕ–ï`FŠŠþܳOüxµq[Çæ[á‚óÈø@¸3+Ýqu-îúÕ+ox}Õ‹[±ˆ÷u:_eƒÊaØ FÒJˆF …›€U¥Yè]•‡¶­¬rdµT²…‘ûXáÈy…î XñÇ©WíÁH "{@gÁ±.­ºœç2®ïMèhCbo¡.am°û1~tºÏ%Ò\ÁžgºŽ1Ò¡u‘aßZO´­Þ°Ã|ø¼½žè\~ô¤D»ÁÏ^òNþôÂ:8PgØvˆwîö‡l É`÷¡ølAud-:'mÞ¾ßT¢mÚç1æ5Ã8X_ÆÈ,ùðÙx:|j¶EbÈC¹"‘‚•¡²ºà!+Íß5ש\þp¾#¸ìØ[eæÎšlž^ó–9fr´[dæÚ®E¶s£°Ñ®ª)ïж¸¯ áæ{w£U®3ì¸d‡w…"Ë•VdÆÁŒ¼)(áqÆ?¦Ï@…!@Üw”ÕÇ šÚÌæ×ÖÜñâ#=‡#Ñ¿Ûòƒ÷а #B0Zê+Á ,.a„Ö/ E6rç3NÕÑ–TߦTŒ‰±¼ãyÓâÈê>»®7){hí¥p´kR¥Ñ á@+•ŽÀŽRŒúxè;MV!³J÷Ѽûì¬93¼aù{,FaÊÁEç@K Û>öò›^U5¨ŸrÜ8È>tŸã¦M0Ͼò–¹î7{bôc°V$|´Ñb¸yç~ó¯h+ÉP†áYŽS0G0ˆqÅ1Žô ÷<éùÝäúih¿˜qùªË¿·ìAtž)B5ö Þôû1òǧËͺ&àÝ®ðFQŒà*ÁØ5©nÖÛbnC[+«£=žIÓnöJË* FÞŒy•{ËÖOÉÍûP>²ZÛ?®Òr%â(A—cöÕÇ|m‘¶•O?~ûÚ—Ÿ+¬5™ `ù`ËßÛg¸üQ‚‘ЭpIXÑ /m¡¨:Ú’èù4Y£YÆvÚóTRoéÝ9üÃA-RŒ=ÖŠ&õ°2$=ÜÒ!õ‘º¾îAt´m;¥t—nªutç“*$‹ÅTëgYz¬äƒ“׎£¸kW½ðôû>z™ +гCÉ)ôœ@Ik#ãîÚϯÀLâÁ×óÝÓ±%/:_UùÞ¶gÿúçŸ^pé§œ¼³:´u|Ø_—ú^IÇAGz3zrU³;å‹&pëV¯¸Å“X«¢íÜDÁÈNl‚0,«£YŽÝ?­¹…‚ !=ÈöfmM¥£¨Ã‚œî,QȤÃj™î|õ%½¬PÀ³¢õeßÞ‡Œ~ñC&ÑÎp —‰éÓ•P¨½yE&毋B‘¸au3c¸%j Úb¡xÜcF¤jÙ†76m|uÝK/¼GÙ>‘l)k)9µb‘ü³Ìq¸ô†¡-¥ôž‹R¦<ù‡?ec-Æ«^¿e·i¬9`&Mk‚öÄÆxÅèÜd_¶Ãô”4ÛcÑ1d՛͆ã}kõEÝëÄY&5p@~ü„ü^ï;Ä;xbyˆ®zæÉ G?ç±éÎø 볃¾†l½Fº+ŠŒéhÛVQ¶ç…õkVnÁ¶¬–d;¶Á<ëÄ)h(bhJd»F÷åÇÿñâš¹ï<÷„ãO9ãÜé­ãfͨ0þ¦ ?^ùêÑ´ƒeÜš¦6«~³Œ¾VÒÐfÆÔ·Äƒq×·slžÙU’èІ< j Xì%ìøÏ·Q'û¹ëÆöUTì{ïî[7®Y³¹¥ þ»ŽÈÄ*g~©Ó‚l…"—ÙŽ.ÃZ,âû…™î´³'Ö4ÅËŠóüUxÙV—;tÃÞÐìàÂ6‹ûvíXýè=ø¿ÖÖfv| `d$¿ÁŒ8¤gͤˆ±"ˆó=9´"2R4²aj"a ¢¬¬ q?æ_EôŒ‡˜:ᢢìpAQNNnnvNn^vÎãhß¶1øëŒE1îjmUe]uÅþzXæ(º’…¯%þf<ýà_}õ‹¯œqþûþé¨é3O™X,€‘ÏmùâY Ú8òšbô¬š°lÆ¡½À׫ºƒð«€å0W¡Œ´"fGânNK$žó䧃<Æê««¶oÚ±íõ×W¾ôJM%œÉ&Ê£³À³œ˜OkUä6ÙÖ²H~6r™Š<¿#q.^`´$4t Rní[»a•ðcÜïS[[g”ï7!7b ³ƒ›:Ï曂‚„`„UÆïGÆAª²t ý< …4}ÒqýQ%ùfjIŽyðùuö®ÄЧ¡ÂÐع¯Ú+‹±9>SÎö†Ðd9±¼†Ñ_œ|aRð°!.¬fÙüþ¶ßÏš3÷¥SÏ{χi;ë4iõ;ñFi%íG8‚ rÓÏ"t[UYñöKÏ=ùèÛÖo'ZŸÈ‹mÛ8%?+N0;¨"†âÇŠ k)£µbѳ4â7ý&Q8²½##õ«³} 55õˆÖØQQ„u <Ž<–=^²ðâ2îGaš}`ïÞÖGî¹ëÌ/ŸqÂìI“g;£dÜøIyE‹rs‹Ð†B¶S»BlÜUˆ¹1¸ÎŽ4´44TUÕÕUÔÕ(ß»kçîí›6쎶µQØÙsgyPôq™zÉyç2æ›Û³Üì¾vÊ化+ÏkD†LŒ,4…J #l¼ðüN 刡ñåB=NÚŰê™Åâ¢B/ä{ÕÒ!O0ÚçÓ…Ò“ÓêâqD!O=kl–9yúS»a¯ùíƒ/˜/|ä]=a;ÛP,þöÁçM®ïÅl›ˆŠå•ò¨‹2€ìõ6I¾ia¡Àð,P˜ú¶¬}3Ⲃ’’ÂÙ§œùŽÂq㎂٩ ÊÊ9Øæ[ŽÄ€·TS[+ª\›êk*Ê÷o|uÕ¦ºjŒK™´ÞÑ‚E #{Í’ù‘ãP¾W­…ù ð¡`bž)SE–µŽ¢Ëø@n¯'ÂÜÁÀcð*¶‘Ça´âŠSæûS˜æ!Z gö¶774#îh_Ïm|¡Pv xìØpV^^VVÞ°Q;Gì‚52i¶5µà¦¡­¶¦ª¡¥©‰â.9Vøq9O¡Èò±Õɶ\¸#óÏýlä¾ö\ì”ë¸Ý³#3d´`„Y»¡±¹•!•0®¿Æ¦Vpᦠ€*<Šèô™ns²!98-‹Œ%ÅE¦  Œa³à‡±‹—ëcs¤Ó1Ã+§¾/‚mäP}ŸŸ—gæN ›µ»ëÌ­÷=m>rÞIæ'ÎTÐ#NÓz¶Yd54­¼he¦8¸¶s0Da®W>!|(±¼:–-˸óò ùÍç1_¶|Ñz–¦ö|qy¤®ª*òò“¬À<ß-ÅDg+Tû®ÃzbE‚ä`…Ñ¢H±X‰Èž³\F~Ün¨ó̼RðPQ0±Ü¬5Ñ Ää)׳ÜíÔ–qò;:™…åaÁãXÑÅuÜÕV‰szH•8~{µ­ íËv“a*ŠÅÍÎÇO{<62Ï•‚‘b‘‘‚ÙŠX»?"8™_+9ÏÈmGM`¡gl€Õø@Umã¨*Œ-Œ4g¬º®VE<%²rMnN¬Š!oüïÜܯÍ"«¡iY¤Xä 6LýrMs¶†ErÏ·º¶Ñ>lÉ3«8C`EŽáü°))Ê7³[ÛÌæò&sÿ3kÍs¯m1çŸvœ™=c¢\îB.}?è:‡½¡ÙÁ…mó}Q3•|yù^3 – ˇåÄòêX¶,ãÎË3è7_š|áZÈk‘ƒËlç‰ëcûvv[üqÁ ËÁZ¯hM¤ð·þø¬u‘ü2)0ÿɢЂÊ~ Xa艶¤åv½ŒË—i’‡6}N­Hå6ÜŸ‚”¢Uá6Z+§½Žìñy{Ó$“i1¤:.G¡Èra´‚Ѷ=äuËåÜŽùµia¶cžËläòQ2Q0ÚÂr0æfMýD_-\æó:R XžÕp^<½àÆe·whI¸ÎaohVG3ÒÒHË¢ÄbG©{÷Çó­®/ô§º/Ø޼(H LšššMKK«™‹˜uͦ²¾ÎÜÿô«æ¯O½j Ñ–Žc4çt1„^ÇQ5Ó#ÍèuYƒq¯kZPÍŒz6'j&ZMíqÃàÌŸ oÉraù°œ:·_l¿7ü,ãöƒÚçaò0H1O|¹ò…ËyŠ+’øâgõ"«)¬°àË=ùŸ#"ðü)4È„„–D DZ9åo.'7nŸ©Á–'Ï…Á Âî¦Ü®sÙZ.–Zi×s?^#dC‹_²P´–Mê+\™›—äcÚôìq˜Éœ‘eÃ)¯U+ 9eä¶Ì›B72Q0vd÷µϯœ~Âlwí[»œóO;¾c¹f†7×6ïô^¨SǘqcнivÝæ½ö‹ì M‹#"«¡SUÛ oýËýÚ•Ï?sᥟîò¾`g VíØAGEkšù$ž…Ù~H´÷nˆ8¦­±ÕT4Ôá)™üÜåÛŒÏ\…žHr …·˜küh§Xâ`T|ϱ].Ú)²šÍ+Æ”›’’"¯\X>,§Î!qo¸.˸óº ûÍ‹„/_ŠF¾lùæ<Å«)iJ¶ ág§‹K†o°7 §VœÅYP1ò7—s»f‡E°"ŠyO|h؇&¯ç¼=WNí|ªmx ¦OáFVÉ1Y(òZⱬ`Äì!Á‡é%G[6ÉS^»ÉÛ’~¤&©‚Ñ+øò]»êkk_ôå7N<ï”ãàö©«ë2õÉiiæ`»®ÇW¼—9~3iÂ8¼@‹½¶vì@§Üô³H×9ì Í꺔2ï´3GîÖõëk[.nZûâôT÷­Vl—+\ÏvˆEÏúˆÑr›šLV[ÄÀÕ…·ŽC#Z÷#e˜ÄÁ,{ÒªÈyr²ÚË aY¤X3¶Ä¡©Ë…åÓÙºÈ{ã±ëc-MkXÆ8‡®^²ƒyzÝ‹ù³/^NYÕGK­D´%¿ìíÃÛN±zØ[>œZÑC>da-Y–˜Às¶çŸŽ“²â¬ÈÍŠBkQ´¿yýØØù¸6OÉS¦Ëß§éÌ{ç|ŒØß™(ma{7à«/>sOnÁGOzö•7Íg¼cÄÄh91–c:ž3³/ÏD/húYLøWÄXÑø(à‹Ô¾„G —žgǽ±aÍK·eå^x{W÷­²´ÔZ!H¦~ˆq óúúl¯ªšÖÇCE£ž¡=,‡Ã6ãõzP,:Ú‘Ò5«¡iY¤Ẋ_Q–K*«9˲²ºÁDzÅì n8 óJ‘d_ö´®YËbò‹‹Gdè¸/qvÉV,ÎÛu#òÄà¤,/^SÉ!Y$v÷Ñaï›Nršï'LŒ<%O,bÛ°fÕ¶cçžöâŸwÎ9zâ8ߌÉcûyÊÚ}¨lÛSiþüø*3>ÏoÆ£3 ªêؖ˳.BÌ(ôˆ€wo¬~öéÍGâàùÞT÷Å «ùD“ç3-ÿ}ÍOò cØi_ÓÔ~‡ÓÙ ú’L|K[±h¿V#ÿ¸çŽ{.›õÔ_þéÑ)_ýôŃ~™ôÿ€|!þòžGM¶/nf {mÙè_m{Åö{ãùþê“ó®:æ—÷<6í«ŸºÈßùc*Y4z‚Õ ´è6·´xaÚP5q<Ñh«£­U²W9¥[ÝGÖdì£0g Fº6â‘÷Q„6‹ÖN*±ˆ2Œ55Ôí\~×ï~”¬’ãóo¸‰F{èånIh*#ˆ@& F>lø äC“ `[Z›››þ~÷ï~ý¡Ïüû—~rçòÉŸ¼ðLßù§àU_b½B`»,VµÑ²Ä;pz¾ãõ|ΆóÅ®^ |:CµCîæ†††‡ÿxÇÿ~àÓŸûî‹©©î‹„hL §€7Ëë„‘kZáj'âUI'Ä";ÅŒ<ŒBOXñÇ©WíÁH "?†Ñò±Sår‡Ä2,Ð={|²ƒ Û,B¢˜ 9.¬[…íÕÐ /ÓT~è2ìT2-;‡Ü{÷–ß{ó¿À}ñiÜguu_P¬ƒ‰jjöÜ¥õ‹bboN¢=±hc¦ðpÈ£'164¯iZ1Ñ× Kž‹½7ØÁ…m÷ïÚ± C¢ýã Ó÷"Ÿw|îñƒ™e­ " A Ó#]ÐMƒ7d<½¼óW÷Ï|ÇœUg¾û‹þXSÌÝ¿ìåçÄK 󜼜,6°VB¥ÂÀñ0ü,6ymç‚n›)‰7špÈ¡ˆFÿ}„>Y5JË _ª ½"`cǽ¡‘û·;n¿÷˜ÙsVñî‹.Æ}1ëH÷…‡Þ4É (—:½* oãd×:l¹gÅ¡Ú9‚ "¨©oö9ŽëÖ¨x{Í3Op¼ám؆Ï9Y¶Œœ¦" A S#Ûj±×œt|J¯ÝôíEW ÎÖë÷ Þ™_\œƒqK+?~<Æ-ÍO5n)^ˆ^ãkLé”§ /7» àƒ“´>¼Lm£m<ðÔ<Á–k~0[ˆtO’pQ⽈±ˆ/ ïŸÝ³Ç¬]ºj»æ^O2<>´ÓgÌGPXm&Ë«žs0šK„bFoa:èNôŒ:Ÿ¤“zrv™²M—÷„ÇvÄßã¾ÈÃ}q|w÷E¦œÌ¨É.¶ˆo¸¼¼|Ãk«6ÕWWÓšH÷+ö9GW:|Þñ7Ÿ,kÈ™* ‡¾Ù–‡_ÝtK±hÕßõ!PùÎzK›W»œÕÔÖ9lg__É/DûÒ„Uщ;‰6FÐ~ÖB$ÓÓ€z$ÖÙA Ùî¶­ýºT„<šU_v;ˆI®O%®zzäß®ýÕÖù ײcæðœØóò„"V;8_¯á?œDsx?VA—c¤ ŒêRX˜ªé\ÏêØ¹Àá)Ü’68­¢#ƒÍÿÀ-í)Û{ ¿÷FÚ3¦#`o)NYvƒ´ ²ƒ "Åb%b "Ë”Ï=n§ " CÀаŒÉP§Œ°ê˜_Û”+Œüm­+\ÎjêäÁÉñó0‹ ÷£°Œ¡—h[VNVVÁ#7îU€È¤(ÄŸx(+è²^b¬cŒJ‘Äα(¬Œ°0Fã1VPcy¢×$×{‚ªáÕ«¼ ÔÆ©ô0¿Vtñ<Ù—ísr²LmY ]ËbQQ'9ìßP¶]l+™†&Œñ:xùÛüÖH7÷ÆdKI¦ `ï*ûü¢ d-Å"«¡iY¤•ÑZ¹‚ˆ€dƒoÌŒÊVGfø ¥@äÕR[Ç+­Œv*š‹:[±ÈŒ9¬¬fX»b…ùÙQèE[½Ímzà%†™þ`0ääÓZÿjì¼á F×ëyJgÈ‘XÔkÏèƒEÓòÕ³´¨yú«‡"¬Ç™â B˜VWÆ„ãbë¾…F¶Y¤•‘ó‹”C)ÐØk»¹¹ÙL>*HóÑÏbKǽÑÏ,h÷`91ÒbÈç-‹l·Hë"#Û,òYfÝéðyÇíD@D £dº`$,ûbd[>pù°¥€´a())­Kžr}nn8\7¦dL䨢ܶ¾ F× ù‚`œ–³0F+¡¿5¶Ut¡&i]l‹F<wtYÂì°sGˆã#Ó-”¬º!Áž §ÍÅtÛ’••èè’ñ‚cçrY²h÷ ô ¹sßÓ\_g‚“‹Û­¾Ã¶<ú{o EŒ¦cZáÇ)Ÿ]| ðùe;ºð9f;¾p·±û`VAD@2‡ÀpŒ¤e_Œ|àò VF~‘S²JšB1ÙˆŸ‡Çlü‡ ‹jÆŽ™2¾°ŽŠ¹}¯D¦ƒjg?ú³øýÁ,•ùö+уíáèü˜¾í8zFk#GQMÆ—°.R0ƒ°¾a; &þëÈa¯r‘Y[ñÇ)-Œ¬‚§xfgZi]¥–VÅ¡l³h©í®¨6{Ë«LInÂ¥gõÞ¾?÷†Å¢éÀ°âSka¤(äó‹Ñ>ËhUäzÈXÃE0Z€V0òKÁÇ/tNy4ÙˆÙŽÀelëØ’.h(**ŠW¸ëõš‚1Å‹FC¾@À-*(@û<øôƇ…Í"£hÃØióFÒhÅH1ôÆ…B„p¢€Â¨h7ðÁÚˆŠmfŒ¼)ç‡qðD0…0¢íd{ˆÛuC}z5 ÍæÍíûLuù~3qÆÏòk…,ó8ÌC_îa~ÊÃ&ûŒ6òÙEÑh£]>lNF†›`d)Ù,_|øòMo#f½üöç¼·](/Üš‡6Œaù¼î] `ŒÆâ:¶ÄP·ì²·oíòrác–Cvr¡K¶HÈ´†VFZ.vhyƒXôaqKt„¡uËóáè ­Þå%“¶N[VÚ)óéXúõêq§«@±øêæ]æíÍo™ÉE9hK™•°€¢Ü†²NWùíãòÞÞ}<ŒvëäËÒ–ö"m*" CK`8 Ædb=yð²m#cÔçě٬U¤nÌçK~'§Ùå<#ªUãnÔA‡tfaÛDöNDF¤è÷CK¶·åó:@ Fà–I¬gê¨Wu ß30‹bÏÝo²Æí2 ¹"Y ’Q÷Òm`N¡c#§ò«È6‹¬†Þ¸}¯ykãfS’ã÷:àääæx£Í°ºœå4ÃÐÄ©S8èÛp¤³ð^ž‘–ÖÆÆ–ŠGVBÛôÅcרX«IËšmÙБմ.Æ!¢\íaÕ´í½j[X·VFºÜñrÕ>íúZ“š@}S³)Êvð£ëö†f—]eåfÏöflØÑf0ÒL8ìõܶò D@D@D@º&0Ü-Œ]ŸÙÁ5‹^¨o¨«ÂX®­­mQôY齅ѦÓÝ”ŽÁ©?¨~v «¢gzÄN´.R0²#«ÄH¢ÍE¦50ÒÒØ‘áî¤ušDMEUiÂPâ‘@ !^ßÐbêêê;Ý{ dzÎB%úƒ,0…ˆùpó“• QŸè„$œ" " " ] ‚±ãì·¬{u{ÕYgVVÔ5LœTR@wiÔ~^'Ýì$*ÛkhayÄ:ºÝñ¬Ž¨*¥£ïDðÌ‹iÏËhI°¢¦ÑìÛ·ßÔTì7¹Ùh›È^Ùè •…o‚0æ³³s<âš°¸Ñž0¯ÃÍÏhá¤ó¾-‚Ñ«’®,+kÙòöŽ×Ž6iúÄ"Æ4ë´„u#bŽ.fØÛ3b™×®‘ÕÕŒí¢‘~­hL´”m±·2Û(î)¯6õÊMaÐ5¹p²”K#«üÙÆ4á2׳,R,ŽS‚žía¯£’¬‹½¥­íE@D@F+Ñ$i΋mX½rå13>kêÄⱓKÂi·2ZÑÈ^Œ/îU²aeØV.€jé¨íÑÑšÕÓì$’ˆ£õìßyï­ªGÅý&ÖD¶Mäh2tÎŽHv´.g54-‹‹®brõŽîDí-" " G 0#ÍvžXÄ4ºmÓúýk×¾ã¹qc . Ï*Ì͢ݴO4:èQ.„LÀ«¦FoܘÏÄPU€Pdµ´‹.Û^‡=abôò!;cÏŠ£ãAoÞ¹ÏÔ¨0ÆšÂÂö¶‰ð‹‚ÏKºÎaohMÈ6‹ž>3¹Ž’D@D@D@zF`´¼5)Ù;ÚYaÅ£½ÎÏAqþiÇM6)½^Ó.;VÄ=Ÿ‹tfÍjTÆ„e‘VFþH)DBmc‹ycë^³}Ûv3m\9jü8¯Ú™¢0 B‘DZÙš‘\84!«¡eYìbm$" " F›`ô†äjAxá‘å@°ùZZN={î±SB“‹óX‹œÎ`«§ ÖFTN£j:!©­a‘âQáȼ6‹•5fí¦fËæ-fjIØL3uL,‹cÍ„ ã½Î,¬zf'—ƒŽÓÙù…~.ÓÜégÙÖV" " "0"Œ&ÁHH c"úÒšPkk«ÿ…‡xyÛÆ×·Ì>íÌS;~Ö;Æ–…³ÂÙ9þP>w 1õËñx–Ïï …k[aѪóª>)L¬8ì´Ëa?=±xØR-HE€>Mmmƒ©©«5ÊËM,´Ó*0ãÆ–˜ñ¨ŠGë"Ú/Zë"Ó‘@LESËD@D@D oF‹`$¶cdFZyÞŒ^%tÙö­åˆO¾\XøÂÔcŸ”_PT‚ó>_ :´–vBTcÇ¢Ñì`04ç¤ÙÇœ[ÎépÈÍêÒäjfüPè%dC iÒw%Ë €?¹(´OdOg Ä1p‘SRŒžÏ°,ÚQ[${ \›‹€ˆ€ˆ@Œ&Á˜\- ‰t’èÛƒ:ÔP[ݸzåV,¥e±³uÑþ&³¼™³çæž9çès‹³E7èHĶK„dT{DlþXáG'èÞ0Š>¿' é6‡b‘N¸Ç”›Â‚o9Û,Ú}ús\í+" " "p8Ñ$yö¶·4­Œ V,²ª:1ˆH&“V4Z¡ˆEÞ2VmCÃø#tÛçB,&†ü£_E FCŸÿXáÇ)Å";¯„Ðã9 ±È^ÐŒìàÂ)$×q;!0Ú#)Ú-´8ZÉÎ0Œ^ÛFLÉ…5¡V4bÖ üÍå1TY7ÐÒ…ÑCbž?Å„nn%ÁH } Éb‘mB)i=dûDV;ÓŸ¢õ«Èß‹}㬽D@D@D 7F£`$ŠÆDƒÃļí=…åd˜leÄO/XÎ ç׳ ÝÄ Gy‘Y$-¦þM­häh9V0²÷3ý(ææäxSþ¦”e±¬µ·ˆ€ˆ€ô„ÀhŒdCë";ÁØÞÓ¬–nA¤‘b1•…‹=«cs^aqC ÚÐMšÔ.Õn‘lÒ(}ˆ†tÄÍ©¥…BÑ Ë´T ‰€ˆ€ˆ€¤$0š#ÐÊHÁhÛ2ÒÒ˜lY´E,î\Ë ç5‡Ñ;—ã'Y“iô~>YvˆÆöŽ/Ö¿bò6½?‚öè-Ñ.-/+ù›¾) ì4ñ+ñ—Ë|Á¬œH6Æ%fÛ:…!`…¡ÌQ”ªˆ€ˆ€ˆÀ‘H0¦&DÉ`§‰_‰¿¬ªv¿ßµ¯ä•š9}ÞGÎùèLD@D@D ¯(~zOÀuãÑ–Ö¶Û@*Œ@,[–1N-ÕGæ Ý2•Ë”eË2ÎÔ<*_" " "0˜$ûFÛÝüêê­ûËlßw ¶o)h¯Œ%°ï@aÙ²Œ‘IY3¶¤”1Á" ÁØ7ÒñÝo¿ÝôÚ뛞ݸµ¬1—¦èÆÌÛ‹e¹qëžF–-Ë9Tß—Ì+&åHD@D` H0ö8Õ!EDtýšUÏ®Z·éõûTsÙ{Œ™¹Ë’eʲe#²¬õE™Å¥\‰€ˆ€ :©Vè= íÀý{c¾œ¢@Aqñì铯ççd{Ÿ’öÈÕuMæÏ¿¶÷¡åOÞµú™ÇW cõˆtæ.+cÆ”’2"" "0$ûFÝsÞ]ƒûwn= åå† gMW’-ÑØ7 C½Åâ“+Þ¨ùóßùë³Þw4­FžØ ž®“daêÒñE@D@†”€cßñS4ú!,üå{÷ì¯jŽúM0{bQa8\˜—«qŽûÎuP÷d›Åí{+ÍÃϽºÿž¿.èÙåËïk¬¯ÝL°7S3"«¥D@D@D`T`ì{ñS0z–Æ–¦&³ó­M»Ë46U7FòZcñ‚P0ÌM Ä}G¿ÿ—üwôù>ì«]F9“îøÕ‰1û›ëº3zÂqv8Nà£ë?÷ŵ½ÞW;ˆ€ˆ€ˆÀ 1‚ññŸgÏlkk}bqb_¹F‹ñù?ôÁoFŸìkÚoô˜sç¯O6ñèÓ¨k.êëÙÃÚXëwÌ{×}nþ+}MCû‰€ˆ€ˆÀ@‚ñ‘•sË× ðøþ‚ÚP(ë´ ¿Ñ²µ§i}å?(v£§:_elâØ·ÍŸé龽ݮ´ô©@™Ùº­t~So÷ Ûãg?Ë)¬;5RZúžh&œÏéw/Û1° º“ú›\{ûüNîÉë.¿¼üHiÍ[¶,Ø=¤ÉÅMß»²ÂßLé°œ:WýÏÍyÝc^éOÆN2§×dJÙ¤ŸÂá)fÚõxxµDD@ÒCàŽ éIrðS‰Å+®O‡XdÎñ¶-l´þ®'gñÕÒNš·pÉ Ñ†hUbÑøwWyÃüEK~|ï½÷ú{’Fo·Ù}þ &R±ª·û ôöó^÷±ù tì@§±ªñ™²Ø‹Ÿèãô4ýæˆ{S:Ä"‡koBÌ4ßÒ£cï¬ø¯H¤ª>9Î_´t¯½yK—öÙÊÞÕ±¯úŸÅc]¹èGÇuµmÞ³'öò©]­Ìå£õzLÆ:–ˆÀè"0ì#«¢]ÇýJZ‹Í5ÿôûÑõë@±Ø¼Œ×¼?ô¼lñ‚I)ùŽãû¬1ó_·éÞ®÷yk\7þCÇ´^0òάë3šs×mgbíe]oÑû5¸v>>÷÷·žÓ“=ÑakŸ ÍŽAÿ­yñ¸ù˜ivÓ“ý{³Íx3®Æø|ÿšŸoÊz³ßPm;¯Ç¡b­ãŠ€ŒGìÉ™é"mmW!i·æÅÝø×î]$Úö-ÔÒeg /¾±ôku·”.à¦-ˆwÏ_°Ä÷î+,9×績€¯ ë×Ù´æ-º~®ÏŒ»uÉ"¯­¤g™s"ïqâncV®óøÿ.XàõÖfµ£»§âŸÃE¹Ë[ª[ΈÅã0BZãXZzohOlË9މŸjg­ñ]Áêêy¥ËrÝxÅ%ÙÙ¾go¼öÚŠŽcw釜xpó²%ßzË.ãôŠE×½;Ë팙P(êDÞ‡žãUAÑC7—^Õá3ðêÒ Z#uï7Ž;Þq‚Ï/[|×IcÞÂÅ#[ù1×9mþ¢ëO6Nl¼ñ9{–•^K÷1fÞ‚%çù}>ç–Å×>Çß_)ýÁÌx<>cÙ÷<Áßó]wzµŸáߌ›oZºp—uáâ¨w>:§$oEæO8Áì‡M´™«:ÂWýàô¨ëNžøÎC¥¥ ½ƒܘËk$í!î8L÷¥#&ì:±ÛJ¯Ù™´ÝV”Å/p…ü„UÈW/Z2½3¿ÛJÿ»òêÒNi¶}È5¾Ÿë¾°léÂg™Æü…‹?ÐÕµz ¤þ_¹S÷ù:_¹`ÉT¤ÿ^Ÿã;. >U[ËËÿ`èêºNU®Ì×Á={v=v•þh½“ùi^D@ÒM`Ø[!Ì>‘n(LUÜ<þËü1]¥—ò¿B þŠb±ó6·.Yð'XÊÑœì®q.CUõ­‡lãF–¹Žs —¡ñr×m{‘ÏÄsys“»ñË _àm_]kbîÿ¡ö7Q7ö8„Ú\oyû ¸²è¦7Œÿ½‰›90?s£åo]}Ýuã–}o^3äÓÏ[ãWØ}¾\úƒén,þP܉Ö9ÃqãKÚŒskÌ,GZ›¸{W$Zý Ý÷ÊÒÌ‚X|Œs?ßGŸ‚À¸Žë!<>Œ¼Áºê¾3nb§»ñøån$þ}»/¦‚ؽÇþÿvãî¿ñ7^îßÇöBàÇãF÷ ®ƒ¹"ô4­FÚ? ˜È!ùžÿÝ%¹Ñ§¡aœÁ‹<õ?¶ºµBóú>2çÞ{C}Ù׫„׳-c*~ï­‘?^>ˆ‹ü8ˆô¿\±pé U@^î»íškj¹®(?äU‰¶ÄZg”–þ6é¼¢pÍc¯o9ùŒÅœ1È÷žxÌ}oç´rƒ¹`Û1W.üá;bÑè»!žŸD|C3^à g×™‰Aƒ•’iVÜRºmAÁ瀩kJetÉÑÞÇT™©co8¤çyÜý˜kb÷P\/[rír»ï`NQN)¯tåVמ\Û¹×ÿƈO”Ëø±&_¿mñµžÅ°3¿¯.\: lç R¹C`ßZºà ¿­ØöÜî®Õäóúò¢ÏÂ5:Ûñ9°åâú}´ÛôèºNU®6öiW×cÒOJk4\I§«YÃZ0Ÿ[0 TÚÅ «0Uú?úö·ë! ÷£Úì„TëéjéXT;o¡¥‚ñÎv±FÕöiìó[î‡jÙ)]1ˆ¦ÿ6ñø‚Däzç1Ý­=ø]ÿn;Ÿ<¥µ½´Ÿ‹DcÏ JûóHÚÁÔØmÚÅÀZXó>Ám±üÌ@З]ßyŠýë첚æ¢ç£0Wí7ûà2ÆÞî’ƒùD~]g#†W¬²ûØéÏKÿ³ "dEÔ´“·ÏqžB|"‹e[,ú^œó‹¦à;Û½n÷ó¦~SÍiÄ ze á½ÏŠ»Øžù?¡ û+ÿoéÒ£ìòÁœB@§¼6Ò•XûŽxmƒM>`™îˆÿ‚ÎW·z×\{F’ùµ7ûÂþC™· •É…Ý]«Éçw¢ž ¡P¶óš]>Éœ³×j©{v]'ç˦ÑyÚÕõØÓûƦ7®G{®šŠ€ˆÀ@Ö‚mþðÀàš.-ˆƒ`úÒÕ7ÜÀêÖCBSuÓ¿CC€×nÏç†î€8z:ðcÇl^àOÞ>g'¦!'?ðITë]ÌÈy¿ã»a’ùvJ‘˜| ´÷» Â%<)°p2öýØE'·”é%oƒ—.¬Œî'`Ùü…Ú-¥ß¡5)eHÉJòö@Ø·¹Õæ“S¿ã^ïd¹O%o{pÞYN±ˆßøÜàS&à>é|:¡CïïÞv޳–ªÓV?ci,~í³!i:‡¶£ &Îû„㮦æø/];H¿œ®­ËiÉëKûµíÞ5oŽŸaóÈëOFY½Íe]^«vLñã¥ÓÖdØKÜ {"/œ…²L|äôóº¶ivu=¢CUî›~=ZhšŠ€ˆÀÖ‚–·]Ä%‘,MWéû³ÍÿàÍ™ßR^ûÄWJ—žD‡Ú󮿾ðË‹–~]¤«Þ’_–~»Œû·÷H^Ýo`…yà–ï|dz¢Ö£õ¦1ú-öjö¬e ‘[1ÄÜ5=éÄ1È6t^hú}|lÝ[è¹m ì:ÊÕŸcþAp*^æW¡7ë]OwËoüÚ×(œï‡Ø½‚gØ{{Þ¢¥_BÆå× ÔüÓ‘NÀ< 7/Áqý·,ùö¦‰fÁ+X—óý— ã÷ª‘ß°4åï^÷MV{“!¬_DþïKՙȦí:¾f:‡öù|_fz_þî’KíºA›º¦Ëk#-yðÅÓ~m³G5-¿°i£ÝâlÚ*ê¾€2hÎóçÞÇ|wy­&¬˜[‘Î*×Ä¿ÁïlçŠ2ûzÇ&ý¼®;Òéj¦gé®ë±+VZ." i"Ð!,Ò”Þ &3¦`î&¼¸ê €MWiߺpá p*ÅY4_»'ú|£[­F'–Ÿ¡ú+·-Y@k_G€µ_\úÊû]è¹qÜOC`^ÞÍ{ÐCzÄÝä@ p¹Ý¦»iÐ ²­ÚŒ=‘¥e­ÛTŽvlpwãü=œo°í™Oäñi÷(7ÏßÑS¹»tS­ ²®‚Øuc‘س³b/Žñ}øœüì/—,ÚÆíÉj­‚IDATq\úü>Žû#þ^ö½k_Å„=ŸæoO;°F:¦’í7¹¬½³Ãàò½²èî]e‘èZ%lr«¹þHaÙâk_ÂqoCç—›¾^úó¢#mŸÎõ>î«8i×À„`(H~éÿç‘è Ô¿…²Ú¾ÅzÙ/J¿ÑÑ”!Õµšœ¬w&ôïh9mT75Dš¶#ݸ>öÆb~·¿×uò±RÍ÷$ýÑv=¦â¤e" "NxŸïð÷9÷ãÅõÑtŸÀlÿà·Ý=I—––XËœ˜+C{2¯j¯'ûÙmX-­h<Ñgœ+¦ìºžLç—^7Û5c¶Ûáéo|pÁk¥DØ_úéÞ¾dá—z’^WÛ@Øax¸ë‰›XINAîë?ÿÏÿ<Ä)"Ýü˜ÓÚn‘ì*™Ã–³×yÜTÏŽ›À¾N~Û6“̹cÙ `rnºó±³jýåóQ5?0Öè'6l9í$ýÊ21Ï'Óò|âïoû¸‰ÿ:ÝùBïãùë?7ÿ¶t§«ôD@D@D ¯†u•4OúƒßŒþÕÒ«û å~¨6Í MgŠ”êÛB'ÖrµuïÃWÁµ‹}cØÝ^nNÑ]°vø†ìnÛž®CzÛ¦™úûžn¯íD@D@D`0 { #!ýãGÁÓ0âýÚýb‡+Ÿýà·cèW"ÚyT8é®[ß™Gq²ýÿøBÛ@´¼dýåóðt’" " Æ@ÿ_rpªøVäô˜½2Y…çF‰Åti¬ûì—ŸÀ5³ g‹.4ß“XLI¥!" "n#B0Ê%ߌýF®Â,W÷-øœ›/ùæwºé[*Úk”@•ëÑcü{ý9m´[\¼þßç/îOÚWD@D@ŠÀˆ¨’N†³üÇ÷ºnì7è9}tòòîæ¡Ö8¾ÿüà·°Ÿ‚ô‘ÀÜ;—},îš[ÑsºÇ£Ï  ºލ¿²þsóþÒÇÃj7p#N0’Ø‹?›’S)»N£YM=³+Š8ù øÒû­ßÉýéû¿ÙPÞÕvZ.=%ðλî*hˆ5þ? 7N½¡øRï딡Õãm9Yæk.›ïßz;-¡'0"c2Öü4xr,;/ç™h#–—x3zUï4®oUΙç­xÏ{žÆpÉ "^¥®ëûón?c‡¿×Ýô¸qòpÝ5AHn3>ÿÊO~æ‹«KoìïôX©‰€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆ€ˆÀ$ðÿá…Yo¬"ÏIEND®B`‚networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-hw.svg0000666000175100017510000010001013245511164024674 0ustar zuulzuul00000000000000 Produced by OmniGraffle 7.4.1 2017-08-04 09:20:55 +0000 Canvas 1 Layer 1 Controller Node Database Node Compute Nodes 1-2 CPU Hardware layout 8 GB RAM 100 GB Storage 1-2 CPU 8 GB RAM 50 GB Storage 2-4+ CPU 8+ GB RAM 100+ GB Storage Interface 1 Interface 1 Interface 1 Interface 2 Management network Overlay network Provider network Interface 3 Internet Gateway Nodes 2-4+ CPU 2+ GB RAM 100+ GB Storage Interface 1 Interface 2 Interface 3 Internet networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-compute1.graffle0000666000175100017510000001475413245511164026645 0ustar zuulzuul00000000000000‹í]ëw›8Óÿ¼ý+üöã»­# bŸî>|‹æâØIjŸœól›ƒ 8—ÝÓÿýá Ø€/i“t[yÏÖŽ˜‘Äh43?iþû0v w–Øžûç[\Do –Û÷¶;üóíE»úž½ýï_o>üOù´ÔîœU ÇÂÂÙ…ñ±^*¼}p O&ŽupPn— gë­vê88¨œ¼-¼…á䃃ûûû¢É©Š}oÌ ƒƒ3ß›X~øø*{ ÅA8x ÍÌj_é”ì~ø×›ß>ÜZñæì¾ÂõËÙ‡^—Mß7ùß>¡ýÿ š+zc×úÞtR<…_5ß¼¹q,õÃÁœ$AU­ˆeè Ñ$"K ’‹ª£.”|+j¿l†Ö²ñ91AX~ñ{Œ XýC"ÈJáwŸduËJ<ÿØ Ã‘æ½é›ëLUÛ±Ú“T«æ4ôÖiáV'£²×ŸŽ-7\•í†ÖÐòÿÂò‡ƒÅïßÔXÁG¯k ÖÛ99MµQ_ÚÝsRÝêTZëôõ±9´JÞÚôÓ½Iuæ£íÞæÕžî̱9t­púºÑYÀ´ªEK˜ 7s}Ëoõ=bï‘\@ìý¥ìñžUd¥ÜN-§ »Cþïg¯pf9棗f¿³ø@šîÀ±v¾Ã/´v—Ç©omwgò3JÔø6Øþëî·$Oêy?[††ÞøØô¡ šÄ”^V}ãxf˜œ¾Ë+2^™Õ±9˜µpèùößžk:ÐoÛFtSC`ùÈhÈÐÿ‡ §s…‘õÉpºõCO—›F³ÞªŒôÑéRpïªáô‡%ø{pz»£ÞᥣÃõREi·IãK÷êéõ«ŠÞú4Õç¾TÛÜýÖMø-âÁlsý0ø _?œXj[Ärf‚#iÙ[›:D×3úüª‘wŠqQ‹>H¦Q(ûº¥Q®Íç÷SÖ*·mêtnGϪOmoòÕ8HFç–98uÇí(ËÊG5/ìÐ<¼è‡öõÑ|´üº;°–½Jz©8`}ðy„ 4‡´:¸&¤˜° >ÃìßòpÆDþÝî'—w2'åTA|=Vúлúú®ðL•"AHe2S%YyW T.bŒˆ$3‰¨T¢òוÙ0¯½ä@’Qù<Âj`^Å’éÞ™AF§SõÕËÉÊ–2XÁ|(ÃÇØ=­ÞøŒ ™ï>A±B2£)ûæ}²ÿ¿eëNtá Õ@è{·Ö³4øü9ûÀêzÞxGœÉ¾lCØÀc¿`æÝÌ uˆ´auÆüOãZ1È1ÏŽú¶¡V>aÀ’fˆg‰Às¦cWwì¡»Gkbö¡¥„9Î_ÌäèÇ’¼l×Z}Óɸ_€/ð)ôÇ…? Ëßé››OŠ€ƒ¡D%±µLkxjn'&·+ª¬2 |fªö®.* ˆƒ•‘,Q>ýU©¨F£A(S±Šeé]aÁ­aÿSIÂëÓ?{þ/;Íüì‰>1Þj$˜=}` bt”¦™­Ìñ„Üך]Áq/2gçéÄr[¦¼oYc»ç9ƒTe‘ ZQ𥆨 É2+V.ÖDuÍÌ­À™Ž„Rlœ‹+9ÏpfˆýÆvœmROÍÕŒ¬·J2—*2I!šLdJUIa’ÆVd¶àæpkHcHÒÕdY•U3¹ýûº~HÖ®û2÷5ðÓ—³^OLp>(Á¿P]ôEÿ"D޾â_’‘ÿ’åˆn͸ùÅ Œêµ}€øSßt®¿ôyAÈñŽåÞ˜ýÐóÑ›7\,= ¦…ëþ )€×ò½‡Ç¯»…fûydŠ’L¹Õ!ªŠ40Bï -RhXBŠ"c ~rÌ”"7‰j`1Ñ4ŒÁw#µ¨bª@€„Àr‚[ùÙ=2û¥<²†°F±,+ 1"Äý<2•U¤0씂o%ûydð ÌÂ݉„Yxdá‘…G.WÚzYoë…slEêsýÚ °<ë›wç¾[¡ùuëRW¡3:Ïwo2] Z_ãzj˜±ü5•¼+°¢’ (˜J3 F5ü_%Ìè/‡ü%mùyò_~”ý‘?‰¹òrœAÑòCö3ȶñßg¨1·öóÄçíjymäÌÐ\ü±iƒpN22§¨Š€lÒq/Q÷ê¼Í÷Æ}}ARoÖÛÎÉYg^i¤gã.ºµ*ê´ð¨K.§ƒº¹è‹>vú9i {åŠÒ>¯9Ÿ5ç®7®>ÎI¬féb¤W'ºrTq&CèC¹Gî ^j-»ÛªHi7‰ô¤ºÎûÔ¹¯UŒZw<'éŽÍ²wdŽÏ¼è¿¥#¹,£ÉÑ¢/Py*ov>5F]©1ê×F 'èvVªBCíO—¨sÕº—ƨ?®ºðÿýœÄ¬ž ëê!Úq­À]Ôª«Ácç“áÍI.¤s¥W»àݬ4¡3÷u}q s’æ§pÜêNãöøÖã{»Îé>w¬Ãó‹9Iç*ºÑR“tôúÙÚzÙ®ùç9‰74.ø z´®÷$õÚ±^ïhVäéˡ•ÅïÅÕ‡‰Ë,þ]é,H:Ë"ÿ®6áwmY 7Ì+ãFy¬q%ˆFÿ='‰Š z1nÙã2@ǵ“1ÔuÝŠ©ë僃ۡÞm~ØáßH¯Ýê{c´ )!½94îôÚ½Þf”¡;´{z#0æ$ãûRúS ÿü3ž–‰Y$ú×è!¤¹÷ü[÷ûEüÌè½ ÷>yá-7ýh»Ö7G£¹Ñârx÷‰1Ù&Zæ`s‡êkŽbIâT$¹k*£Ì ỗÂ]bP•I‘%B‰Ì0Õ}W Ä ˜a¬(à´áKúšÔ‚_£EP^) ¼=4&½+`‰™¦RI¡’Œ±Lä—‹rÓ;÷ù[÷Où¾¹•ïï™yñ^fŒõ‹G‡|’ê¾ïå€P”®$Jeµ†fÿqåÆtë`ìV2Éwm¢§æùµ™¶³¯®ìA vDr#û…¸ÐøíÝd·„åß.AödÃ/i¯køU¹È÷ 4¤£TÂé}1‰ Ã/ ¿0üÂð ÿ—7[~öm–Ÿ*E¬Q¦ 3_âæzËý­žCXþ—Zâ•·ÙSaË&[þä-G¬*EUfHB²Šªü®ÎaÚ–k¼Ø°‹ñ‹o9ÖCÓIÝɳl<ŠŒc‘q,ò›D~“Øy­í:·¿~ôàhÁðíÁðÕó›ìD†SÏo‹]™o†häé»2߆Ð&EEƒp‹PÄAŠ2ÉI5"Kß«ŠÂX>BÃ2.‚·#ŒP‰Á\bÀ æ“Ê}¡¤2Š>ûw¬ÌýüŽS ¹ï¹2—ºùšYÞpÿÂMì'qòô=å•ÝQ‹Š¢!E–TwX-òÇ•à?ªªT#bOø á'„Ÿ~â›áÄ“ÝÄ·îô£"£0„5¢ÊŒ±øL †øsƒ”1yƒ›ÈX'kQ“C¨Šd¦!IR„Ÿxµ¥5YÑU¯ØSö{¸Û@ ùS,?„ ¥ª¢aøÿ†Ÿ½œáßüX½TL>VO!vej‘©2 ¨¥†SùÞSêY{ò®°þL>»=ÿ®Fϰí_}À¡ü›6¦’£*aˆaa·¦î÷è½ÂÓ®¦©H¥2 ·ý½WùÆ–¬Êúk[SbkJlMí´5E$y~¨³ HÚuoJÃ*U®ûótÝWa Ù¸7åÃU«@ÄÐwÆlß,mv¯øÛPž„q‘ñ³9¦cAÆb ’˜(PÞ x©y¤!c…A˜ d{J¤¡€£¤® I~B¤mÊx;DTôþ@/¯¿Ò,7; [å'UaÀl)¤ap2Íò% .ÈkP# &H@½g‡zä…2 ùØd ZÉñ¦ƒ¬ÁÐO@?ýô{2ôƒgäYZ"!hgèÇ.j e %ªòÃ@¿yâ_ º{g]¿iO]×r‚g95ËŸ¯gPáΟó'ºsI¸ó® 2ÇT’ %ˆÉ{¢+*qOÂÌv@gû¡+ÀÊMƒ&iˆ(špæÂ™ gþK;óä:.Xèg:Ö<½ÿšÊd»nÀ"Y¸ñÝÝ8}¢—Ÿuö'ÛžN]8uáÔÅæì»9+‰ÍÙoÛœýΛ}ï7>ªö¤ÈøiîXb*8þò¼õÝUuCîÓvwàÝÙÜ7ý™—éžé÷ß7%?ƾi*í>Bhr&¼Œ˜ êL$F²Z¥ŒãY¨,Žgy¡ãY0糈óYÎ8KœÏòZ0ëÌ÷îìåÿ˜‡³Læ½ð }A¨>C~îú9-Òù¹°2ð·,ƒ_À_k XÀ¿ïÿdñdüK›ñ—|Ì‚½®Ï\\ã§š°ãÂŽ ;þ¯±ã›—ñÐüà=L)¨ª®¶NBjÖ‹]£W)­½?^UD¢Åó/àiâòâòbO,à‰¼×ZÀãv¿p׺·Ãþè‡zQ;ăEñí šU•¿ýËEŽ ÀKKD¥ùs~…|wm‡)ÕÍ5I3gÕÔ-ÈÏ­ r›ß_.|ö <Ú@²¥Ï ÇVžb;uǺÙavNÖÎ/gmÖMhöÀ>Î /ÿgQ”g!o‚Ö68æ/æÝn#¯À¦d™õѺe”Õ"¦ Ž 3Ì-#+ªª"ðTÞ'ƒÕ"Jb¢h-{­Ly¥„…^5˳kÙ§ä?aeû}ÇXýû²±_jn¯Øù{ºŒ'Ûòô¡ü“iI"ÒzöH‹ˆHKDZ¿`¤uzgùŽùøZ빊ˆ³žgI?Wœ%N¦‘Ö›B‹2“‰‚e”¥ÈVñU¢3=° —d*B­Ý¬’üD«$‹XëGŒµD䔌œòòo‰Ì‚§’7žLC«àzƒeîN•„Íj8™…]…>Ôä¹–/åZ³šïM'Yfnà6ígl2¦ìrÒ2ˬˆ/Ž4#ªþî ‚‹QV1‡¤n{G³†üaúœü¼›ÝÍxo0­ÙqKFx³N—ýf¤8ߤ(ˆl´TOr"£#Wkž2œÍ8ÜÀ½è"ÑßÔU˜Vi¾µÝÍϳGDžÊN„ÈÞ^]K¤Ix#5'3(#!b]á·/±–‘ç貕)åìrtÉp¦é1Ê?ây“®¦ÈÑÖxéRQ•*ˆQ±•FñzHœ§¶»?`•§¿»?d•§È»?hµ!´ßܧgÃ"$tB+Ø2²Õ²§UF´’§&Yé>òšL‡.9M~SöV©­ #[UjkÒÈV•Úš8òýU*šå³”¡Ì°›© 0ÓV·«å”F˜¡ÿièá߃ÆӹÂhYh}2œnýÐÓ妱,lÖ[•‘Ñ8½@Õ˜X'÷u¯ÎÛ+…½q_Õ›õeaÛ99ëŒ'NGjêËÂQ÷¨Â©àbŠÝpê‰Öî§óQ·–h½ÓÒšW§ç¶jqzcÒjVÎn½V%Qø?Ÿt¤óɲ°G”Qß½ÕW2›§­Qü2w©I´ 'ÕÖê@'w½šö¸,Ô­Š~ulŒôóñ²°;ÔO>Óò² ÊÍ3ïl$Ÿê¿/ ‡}ô¥LyCWËBgdœ^`cÔWÝe!üqoVOuõàÄ]šDòüò<‰å b댜‹Z5îgçjðØùdxÒ¹ ¤vÁ­ ì͘òScÔ•£~m”#'è6Kp›ñ5‘ÑÖ‡%ÞƒÃä¸}·1²€zY8n>¶F O¥‹„/k—ò  }&(a4ËÝ¡Þçw¶,ôh]?­›­ÑÑ©K¾Úâbûì ûpêaç^OšÃŒÂ~3£ÐÒ3 o²ê<éÂv3£ðROz_p&ZRt5ÐQ´rï ÜÚâ²ðpÒêÕ›Õ«¿›ñ½w0ªéð9&:¯ŸüÞÒ G¯%:?BÁaS7›Æç˜²VÑ?6’^c1eO7ny#½«w©èãûZO‡ïÄ™µ¤A[±:9&ì§F¶¼)Ò–òvl…&œÂõPs7üšå›Îd”zqìZÔ.Ø´-´å¨›U¿¡/¡,Ó@´éõåTHŸŽñ7­çîI‘oÁdUà •5¾K¼öÆKMÂùõÆéüz& „,òÞ™ „,²@È! „,òoGç•Vå¤Y¯(£8^¿ºü»_«~î6'1­>÷ëfE;8ÜoþA²r%ÉÎkÎçA͹ëÐÑŠp_u¢+ XÀåúX»£zfTg–®¯¢îÎ}­bÔ8N6c<9¡æò9:I"ä߇r™–µ¿[Þ²ðŒ7 ½’ì<Ç» ˜ÞíÕ®Ö¼Á‘<ƃZÕN€áËæûÆw xŽ}“ì»âs¾:`TNÊÍêq).äj­%$„ ì PwËUì×!_t?P6WíhÕáö$†dºñ8úœBž—ãBÀÐúa)¢Žö°d|Ö3 ¿d>dÊY…Y…5½–.<É*¼€ÂѰãJ¾Lâ » ºn¬u|5ˆïHïžÆã=·i9[/±¸pÙ;)Ÿ?5; „¬ë‡¸J=†/tÀ¾„ÇÓP7&zíXï4õ0¦¬ÔõÞ½èµf²ÎRE¿êÔÇ÷¥^\Øå°¹TMú±20ßÇÛòídœ£Æ´§wË|B$t~63ø„X.fŸËBtȰ(Úhe Æ›ÝÞ~¹Š)'“#÷ö‹ÝúÒ=ú ùË´å7ŽÂÛ/´–PZ³åƒº yNô޾ß²Ëq!¾y.ÄãB| Ê&ÔVr %±¸°Xm‹ ÿŽE†òaé¬pbŽ­(jxís±âS±¾ FýÉO³â±©3y­-Ê^xùeË”›Ó‰WjR¸ç¥7)"½é%Ó›~ü4JŠ%Éü!ŠgKs+ò,I¡’Œ1à{‘æ"ñžoOÂboË ‹X‚Kpb N,Á‰$‘¤"’TD’ŠHRI*"Ie|»öM¦jM÷(’T¾}F|K¾}:¾ÝvHæv|»í ÌíøvÛa™ß |+ð­H1)&"ÅD¤˜ˆ‘b"–DЉH1ùSL4‘bòJ)&ÉAšç‹ØƒÕe¥D7–-0ÐR¢'ó÷ŬCµ¢óõ‹ÏžŸSEî»âçǼy÷çL²ä!͇kN‚¶·#OR*Ñ ~ÎÌ¡•vŽˆ,kÒöZ}3©±ÉûÌ^êc>Z~²ÊÄ IåúèàYÀÛ­¶ò® Y¥^ÿv ·ï™o.ãý)଼¡3(X=`)ô§«µ^ÚÖ}Å&ƒV½i¸YÉt×›¡•1l©c8EßöûÞ±í–í L)%f)-Œè[¸ 3´=7Å‚Š(ÅãDÝ®¸CÛÍê×À 3:Æ×G<þ.§–;\Y)Y4Äß(Ä?Dãç"§Žêس³á½7±wæYWýSß³¿Æ›ë ýÔåÔµøœÙ0νûµ#P7Î* O™‰Yÿ¥¤e™›Ë Ûv¸2Ašî¬)zÄuáÚ_¦VÒ/mìÐå>6áÒìžc]zöàÈz\¿ƒÄ“sKñÇSevKc‘"yq/\›Ú+Ðû¡}·¼Ål[3óù`º}kOÞ‹Àª¸¡í¯ g<˜ów†¹à“³x9‡gKGS߇^Gò¼÷œØa¶|é'­T¼ƒÞgˆå]A•´Õ •¥sX±(±1Z^?Ÿ:‰åÑuPÏL\ŽU7y}õMi‰“I3nhÎòm³·“æúrn “ó.qðcaÄß8H•¨Â¨f½Ç_áá( +Ò䇧Z2µ(%?8Cr]Ï_šobµN¸¥µi6-´zumuQ()ÚÊgÅö,›H¨ÿrFDÖÚO|+N¹}¿MÌ¥5•ì{“G°µ·Ö >æót%4Z-è¼c•«ù„€ôáé@ž¤ÉÄ}sj:vø˜d™ ÅV7AηV:K$e*æ"OÎØU£!±Uõ|ðž»õxŽH\ümƒ»±Ønß™,Ãìß}¾•:ߨÌãœãÄs#GŸ…BùŽ„•H:yýÂ8Õ10yà£9š’}ÒãEÄ RwÁì~ÛÀ<8uÇüNƒ’zΔ»Mãø,­«]X¡¯Õ«{Ѷ?îÅÐ8«íEv’A_”„öªªñsýpÊ*h׫{ܸâ0Øm$óÎÒƒªc†UÐöÓ gÏâ”RœÓÀZÆÕùãTÇ‘*m$9ñÂl³°^&8à¿Þü?„=$(ünetworking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-services.png0000666000175100017510000020042613245511164026102 0ustar zuulzuul00000000000000‰PNG  IHDRï.5ÊsRGB®Îé pHYsˆˆÈ¥†ÕiTXtXML:com.adobe.xmp 5 2 1 °ã2Ý@IDATxì|UGú÷ 1"$! ’ÜÝÝ‹KJ…ên·²íJw»úîvÝ»ÿz»ÛnwKZŠ×âî\DHBœy¿7Óù÷ž\ ¹á9ÜO˜3gìüæÌÌ3MÐÕ«WkÈ%‚€ ‚@À"P3`[. A@AÀ†€P3ò‚€ ‚@`# ÔL`÷Ÿ´^A@„š‘o@A@¡f»ÿ¤õ‚€ ‚€ ÔŒ|‚€ ‚€ Ø5Øý'­A@¡fäA@ÀF@¨™Àî?i½ ‚€ 5#߀ ‚€ 6BÍvÿIëA@A@¨ùA@A °j&°ûOZ/‚€ BÍÈ7 ‚€ €P3ÝÒzA@A@jF¾A@A@l„š ìþ“Ö ‚€ ‚€P3ò ‚€ ‚@`# ÔL`÷Ÿ´^A@„š‘o@A@¡f»ÿ¤õ‚€ ‚€ ÔŒ|‚€ ‚€ Ø5Øý'­A@¡fäA@ÀF@¨™Àî?i½ ‚€ 5#߀ ‚€ 6BÍvÿIëA@A@¨ùA@A °j&°ûOZ/‚€ BÍÈ7 ‚€ €P3ÝÒzA@A@jF¾A@A@l„š ìþ“Ö ‚€ ‚€P3ò ‚€ ‚@`# ÔL`÷Ÿ´^A@„š‘o@A@¡f»ÿ¤õ‚€ ‚€ "‚€oÈÍËOKÏÈÌÎ I¬×8©AdD-ßVa]ZQqI ‘µü^i¥Udý¾òT¡fä|†@AQÑì¥_¯Ú¼íêÕ«ºÐ°ÐÐA=»M6(*2BGú/P\rñûø{xXØk¿zѵ¨’©($8øÍßüÌßIù‚€ X# ÔŒ5>òTp+W®¼öÁgÇN§ÕŽŠìÙ±}|\lQIÉÙŒ¬]S—­ßtâÌÙ>öPHH°«ÅI:¯8›™õ›×ÞI®W÷Wß}Òë¤A@¨Ò5S¥»G@,ß°R¹Ò¾5µVx¸nù±Óg^ŸöÙá§­Y?~è@ï§\™—žÿNPPŸÊ¤b¯Ö¸råêå+W©ÍÒVA@ðÑö6É$Ø!pðèqâÆh$eˆi–’<°GGNžâ¯¿¯š5ƒàF$ÕMtXÑå˶´Ãñrø")‚€áÍѰ à9Ù9dFó×¾ˆþÝ»då^ˆ‰Š4>B·fÅÆ-{>yö\Lttë¦IÖ8¹N³qÇî;÷ŒØ·NĻóÁø™z×øu[wÕ zêþ{Co¬èã¹ ³²sÆФaÒgÎåéc÷Ü¡‹Ê/,übÑòÃ'NfœÏAÖ²I£ Ã7Jª¯HÏÊ^¸jí‰3ç.ä4oÔ°]‹¦Cz÷ voÃúóâ5?I) êSQ«&;µi©xE‹V¯‡æëܶÕÐ>=Uɣĸ:Ü>FÅoßwpåÆ-§Îž+¾x1©nÝ®íZójú• ‹Šß›1+"¢Ö·'ßm,‡ª9FÊVM±hÙ‘“§yš{áÕÿ}Ò4%ùŽáCŒ‰%,Õ 3ouz=yA ÒH©_-™y+V7m˜T;*ÊXoýÄÓº‹5п§¹÷ÐVè¤z‰Ù¹yªVoÙþø½wöèØNåM?Ÿ½çБÖÍšügÆì D&ÄÆ¢“_X´÷ÐQx]EÎ…<Ö~ÔŸœ2îË–Ýû7ij†,ˆº QH@Û W ¨úgO=Þ°~=UÈÖ=û¡.––ÆÇÆÄÖŽÚzhçÔ}çþ{\·:žvæïïN+-+ã bN§g@O@¦ ï×ë¾ñ£mí¯Ëñ^&jfí¶Ä£(­3}þâ¥ë6ÁdJ©_?,,ôDÚÙ¯V¬æ¥^xbjlíhÒ”]¾DúèȨCâOŸK'¾9' ›²ô¬ó¶Ä—.:—^;ú†QÉ_A@¨65SmºR^ä�¸W÷mû=•öâ__i×¢Y»æÍZ5kܨA‡ìYKV@OÀ´€|‰Š°Ù:íØŸú¯O¿xçÓ/^zî;Éõëê—™»|UB\ìƒwŽ…òˆ‹©Ý»sGtŠ·íÝo¤fXéIß½C[„\Ø4é¼àMûr¤ÌÀžÝîŸ0â)¯ `æâë¶íütÞâˆ4çs/¼÷ù¬šA5Ÿ~`R·ömˆ)¹xñ­?ß䨴Yó¾3åcáYKVBÊL7jDÿÞ*Ù_›ö),¨»GÝ-Õ¥MëZáaÐ0]%]ãB]ºtyçþTÒ÷éÒ‰¿»‚”jùáã)yYYÙ¥7>š¾ïðÑÏæ/~ò¾‰ 0>‚}qùëW߆”üÍóOIXª«ßûË ¾B M󦬠uãëÀÙ“zdÆÂ¥|ë?ßûý__ýà“íûV°µùzó¶¸Úµ¿=y¢"ehÔÉèAý¬Ù¶ÃØ$X ß{ô®íÚP2´H¿nyÊò “mÞ½—ð€î6íÓ¡ƒ$«~büÆ(I R­‰£nòúȉS ¤ÿrÉ J»sÄPEÊUỗ“°ÀÚ±ÿ fç¦2ÝÂpBeghï:ä|Tq¡`ˆ éÑÁÆyÚ²g¿N™‚8©iÃdI$"þ>xÇ8­úC.Ȭ°ÐÞî‹Î(A@4BÍh($ x‹@—¶­÷ýg~úÇPÑ€=¢´ì” |ŽŸýãµs™6Áë7,ÔGH bÔߎ­[Øuà1I¼ƒb Ë?Œ™q>ûxÚÙ„:q¤ÔÉt­š61‡ÇDGýüé'ܨdèîèݹƒÎEYUë¦M Âö:jŒ·ÿüéÇ1§Ò‘þSî…|²h<}ºÚ0¶t9[öØK}»väo^A! DHÛØ ÐWxX(Z8ܦËБA@# ’& … €¶*´ü( žÄñ´´m{àOÁ¯øéKÏ?S}[ž"Rùã[ï«$=·òmË¿¾ ŠtX`ÏÀÀغw?œb”˜©·Î­²•nrJƒz¦Bt Z8øÅáé~fJs>7˜Ü|›ÊŽ‹¥­ÝºcOêᬜܜ¼|MÄèìPHÈË ÀNŸË  JÌh=;Ùh)âù “¦fMóF«qrRŽ˜!3>Å )FFüˆ‡‰‚Õþ‹iÜ £ð»díFH´†lAµ¨ìóW®1•ƒ¶/†Nð–ÐÔQ"§¾åú¿$S €eÊÂíÑS§ù‹òý#“~þš,OÇH@n„7s‹t´¼¦PVHè|˜Ø3ªV\ÀÏÍ% ¹Ã_øüÝzØ$ˆÜyûãÏí—Uˆño?¥}²÷À51“#ý_•^éÒ¦;aÌ äÿýß›¿}ý_W¯\E?òˆ§´Ç˜†0ò,Ú£¤?¦Gö·8³!ÚК”¹XZŸÆ”´ZDX6Áa¢]ÚÙ ©¸ê'ØšòˆkÔÜ(Y­0›«åâ‹%FUhêB¼eÌ%aA@¸ujæÖékyS?"€(OqT€W7%HÒ•!š³|UAQ1:¿Éõlú+Øõ èÊ‚­5y‰D’òÙ¼ÅØxGßèdO—c tm߆Òvì;¸iרîå†BÆ:ŒÝ8"-4N´2ð2c¦Mó&ȶ¸Ò§7ïÚ«DW*/¼8(GO§%Ö±b‡¨ÄüUnOžýÆæ5 ¤NÆ2uâ¾åÔØGs`OÞ³c;”|Õ#l©zuîpéòeìÒub_oÜÊ_8RÊAïÞ 1’;®“}4g¾R<Ò1Ê6þâÅRÕ¨H@ª Á/½ôRµyyAàf!€Þ ’#œ¸ÀÉX¿}7îÚp݆iôúí»¾X¼ ouèÍ<óàde„ C^Ì gÁ³ Ú5ÏY€<'¶SÆRú¼H=~-`"Mï…R 꽕PX\e î‘ vÅÂUë¨nÜÜrp7>`(góî}EÅÅèÞ~úÕ"¤›‰'b~ªüz ñÏ<0I7˜H|ôA¦ÀÊ‚Öá^×£ıÊÂ_^CzwŸÿõZzªÌõ# ‚@5C Hx°Õ¬Gåun:9yyˆu”º \øδkq^w:=6V<Í¥hÖ…?^ž êÀy……õâãÑ¥Õª-ƺ8Òˆs8߀X3¹† Ùá5®Ö ;l!• ~_ ë×5žÇùùÂ¥p\î=\ù 4•ÌÒWhH0žfÐqVü*S2ôi(Bµ¸Ê”ïÐ=ñ±±È§LäVª BÍT›®”T^~ú÷Wñü׿§‰ž€i½4Tª¢\õúDZ$T_`üÀwAð„#D?BÊTß®–7*Ñ›©T¸¥2AàG€c59ŽÕ¦Xc÷¿ÅÑ×_! ÔŒ¯”rA bpˆU6Z½¨Ë8S'ª¸I!‚ÀˆÞÌxÈ ‚€ ¢7h=&íA@jæF<äNA@ ¡f­Ç¤½‚€ ‚€ p#BÍ܈‡Ü ‚€ ‚@ ! ÔL õ˜´WA@nD@¨™ñ;A@A@4„š ´“ö ‚€ ‚À5s#r'‚€ †€P3ÖcÒ^A@A@¸¡fnÄCîA@A Ðj&ÐzLÚ+‚€ 7" ÔÌxÈ ‚€ BÍZI{A@AàF„š¹¹A@@C@¨™@ë1i¯ ‚€ ܈€P37â!w‚€ ‚€ h5h=&íA@¹ñÖ»Âââ=§ed^È/(,*¾Z㪙%©%A5‚¢"#bkG7¬W·c›–Q–ÉÝ{(ç^î¤ökÇé†;}fþÊ5W®^Ñ15ƒ‚ÆмQŠŠÉ+(øä«E¥ee:^:ôíÚIÅ\½zuÚìùòó š$'Ý1|ˆŽY¸jÝ¡'õ-èÈÈ©w V‘ÛöX»m‡1AhpȤ±#êÄ©ÈSgÓç,ûÚÔΑú¶nÖD%`Þøhî‚‹¥¥ÆBºµo;°GWóñœç/\з’ëÕ½gôp³tÝÆýGŽé[µj=|×ø°ÐP¹ëà¡U›¶'(^ê%Ä«g22g-YQ+>6†ÈÎmZ6èeXFœ—Jöªƒ@åLqž½¯'ÔÌ©³çü{¹råjB\l\Lmß~ÏÞ¤:åbæÍ8ŸzìÄÂÜ 5kuhÙ⮑C%5ðò¥ã¼°Âì~ê¸s™çg.^^/¡Î½cFTØW\­¤­G%UãÊ+W”æêٌ̽‡Ž°7ƒÖ ¹wìˆÄëdYEy>—çy˜øiŠó AìÒ\/¨ìÒ¥O¿Z´zËöú‰ñ#ú÷éÒ¶5¤ŒëÙ%¥»äæåï<Êî3=+{PÏnS&Œ ñ„•Žsy/Óû°ãf,XºjóÖ:±±Sï×¾es/&Ù­ÈÍÏß¼kߎýaSµ¹ÎC²Îâð©Œ8‡°HdµAÀWSœqƒšÉ/,|ãÃé'Ïž›4fÄà^=‚ƒEçÆ‡aUÔåËWXÏf,\Ú8©Á³M®e•Úî™tœ$•áeÇ!‚aÄ9yú®C‡õírMÄSI­—jSŠ <›â|þj®òW0Á•RÆçàz‘àO/Ю璎s+?¥ô¬ã.]¾õÂãS…”ñS¿¸X삯ע´äbb’Ɉs+IY=ðlŠóù»»DÍœ.Q3³—®l˜0¤WwŸW/º‹ÀàžÝé zÄ•ŒÒq® T9iÜê¸Êi’Ôâ ƒzt«ƒ©¹+‰eĹ‚’¤©–Üô)®bjŸcïß»fÍŠWËNªR/…ò5}AÐ/Ö “Ž³Æ§’ŸºÞq•Ü0©Î”¯Çî¿uÏ~ÜÆX§”g<­ÞÜô)®bewêaüÊti׺z÷D½}AÐ/Öm–Ž³Æ§òŸºØqx—Ù´koå7Ojt†@‡rÃxŒËœ%Pñ2â¬ñ‘§Õ§8?áP15ƒU…ÍE^mñ+ã§.p»Xú‚¡_¬sJÇYãSùO]ì¸5[·Ï]î’\£ò_áÖ¬11¾Nltô‘§¬__Fœ5>ò´Ú#àâç'*vņ“œÊq‘‡ßª³YÌ ê&T)©VYÙ¥â‹élLø[PTk%Íð°k®ÓýÔ7ÅÒ#ô‹EUNÇ—\º|Y·$¸fMÎdз¾ ટ¾ªYË—Žç}ÛHëÒ\é8ÜѺëRȺRŸbZ|&=ó\fVtTdRÝÄø¸X3Þ ÉF¿ÅËVΈ³h€ŸUÙ™Ù'ï‹oAAAµ£"u—.].*)á–HéxxŠ¢$/µÂÃ]ÌRm’¹2Åùée+¦fX®ü}pÁñ´3ÿ›97-ý³A5^†¹{‚O> ÏÈÊf&jÛ¢™g nÛ»ÿ½Ïg³N¿õÛŸSŸÞ~?3;‡fã†ô¬@ïsÑ#ô‹u9•Ðq4à_ŸÍÜwø¨±%ÐyíZ4å@W–dVÐí{½]Ëf¦_¼fʘ(AÿöûOk  °+W\r1¢V¥N‚Pç‹×¬Ÿ½l%Óº³GÇvøÉt…¦ñ~ˆéJ«l`DÿÞ¶­rF\…Íða¿ÎÌ>l§[Óˆ©^T.纽øíGZ6i¤žrò×kÓ>%ü¿ø‘Ë߇³çoܹ§mó¦?|ü!SuÕþÖ•)ÎO T,i2žÖæF33{#yѰÃMáçLd¬Ž÷~¶të <žvvÃŽÝýºu¶ÓzÀ¨6‡†:^U-¦ëyÉ¢=òȈ€cÜ)üFÊ 6åGß)C]¸và¼7ΞM=vrçþƒŠšázÿóÙ‡NœTGd²w|äîÛµÔß¼öNn^%ìM=²çÐaÒp8ߨ!š7jøêŸ9qšb±GøÅËo|÷¡ûRŸ˜¹hy£¤ú#ôýbÑR¤0I°|Ãf|EÀä' E2 {×ÉcG:û.I£/Fé‹–­ÝºƒÁÉXmѤÑ÷Þ©4~ú÷׊‹K½çŽÍ»÷îÜŸúÝ©÷µóTÚ¥««²E£€g¯Îròò?_¸ôè©´gÎ6INBîþá¬ù¤˜"k…‡ágix¿ÞÛ¶âÈâç,PoôïϾ$æ¡;Ç9Kl|ñ¯7mýjùê Ñ‘·2¬OOõÔ"/Tò´Yóö:BJÚK™cSõ!ŽlC¡ªÕ§I:fHÿÛúö2ÖX™adš•YݬrßEœ#{ÿ„ÑšÈëÞ¡í¸!á˜^¹rMöä [û!–T/ÑÙ xíƒOáê·iÞÇ€¼ãËÿùðDÚY<*†üû_ÌÙ±ï ãš‘ÂÓuÛv~¹dÔ'aċ̓Ǝȹÿ§·ÿC á=»€{ôó¼Å)Ö¿{büq±.R¬+„¸?j¯ü2}233^¼XJOmÙ³MÔLïÎ'õÞŒY{R3tjÓòñ{ïd+Ë þð/_¾|™¦ ÓxÙ¥ËÌ–è°"è§÷Ž>¨ÜíÙÁ£ÇßühñOŸK·ŸFœ}ÎD_“ù%_¾ÃͰõk¾^&–è¡+7žål1½XÏKÎZ+ñ¨ÔyÓ¾0`TdŸ.OŸ}è¾WþßþôD2[ýáÍ÷RÛH™„:qÄdœÏùû{Ó´ÁdaQ dïç –b!UÁ¹Óoò9ß%Cˆ,LÓ¬aLF !TzÙt¾7ãKþªJç¯\ƒ?rH(kÖc6謗o}b-^ïNŸ¹rã*…ƺ|å ÃìÏOdd%%© šiËî}$¨°¨j“@S §Î¦óRï>g×ÁC—¯\†² A&ýæÇÓ³s/Ø:åº&uXXHXùžÆYb NfNÎGs–Øœ2wáúí»ÔS‹¼o}ü9¤ ¶`¨ƒÐ†=©GÞühzN^YP_þÏ4f"Ôº!eX¶ùðg¯k¬äÀÇò«œJá|dåäRWß®5)£ª¾sÄÐgœtï˜êÖ¶öCŒôÎE‹Æ)Œ&wˆè$†0·‡OžbT’+õØ n•îDÏgÎ…”a™IiP/ý|öªÍÛ¦Ï_‚‰@|\ ɶï?¨ÆçÄ[\,+íÔ¦•ŠñÇßi³çñóGÉU³LÍÌÅÌÌŸÍ[¼ÿð1†?ìö—^{gwê!EÂüjù*…@ñźõÃ9ó·í;ÀDÊÂþY§ÒåROË®€ÄüHæpqö:C›­»_¾·…«ÖÙ§±^ ÎçäþíÝÔ^ˆ¼¼ãÖ=ût!ÖӋż¤K€‹ÜdÞ̹,›¶„S^7fD%ß{ø~4yfèm¡‡ GD±UÔ«"~úÅ3O0·Î\´léºMØÀåþΔ{>ž»j£YJ²Úÿ©Ý9dxÓ†I£õ¯Ÿ˜€ÀbÁ*ÛÒÅ:÷ÐãÂÂBù ç._Å‚wàÈ1kÝáCÇOS’êî.,.qSZZö—?Ï|}Úg¨Öí;|̈*–¿|æ[|³—~ ý ‰c»o×NÆ4¦0,Ó4bñêiÙTí¹oühØ~˜(žŸNPá±`Õ:6À|Š0–zvl¿i×qÝbzá¬u‹yI— ¸ÉÔŒ¢»ûÄY‹¡ëyÔ¢QŠ",’ëÕíÖ¾ ßÜ‘“§Ð’Ñ "Dzg§öP3dAŠ™XÎËqXò·&ß]/!žG° XÞ íÝCiäàl—ªÿÑãÖÔ Â2²§gbU‚‰‰ É\¬‡MïΠf¶¡ºF2þÕ«EDÔB±ôÕÿ÷"›oÜu,[¿iW9ñÇS‰ì_ß•ÄÌtJþˆÛúõF¤iËîÜ"oLt43s;È2àoëob‹–ÀZ۾϶ÝW‹"A„’šÉdßÔêÃ,¬^$88Xè—Ù/þ Í—l­=ƒbÔÀ¾ÀËçqìT㋼ˆtù*èXbÜÆÇÆ$ׯKž?x›wíaƒŠH5NPP3PBއôQûHÈå+|83ÓGlEø5¬_{Ë&)ŒGÚÉH„š)¹q@]7Þ¦„>yÜHDÌ’jÔ°¢fì_Ùâ ÔÓ²}.v¼lŠ˜ÆÙÃªÔ ø8­V"Cy+ª qçš­;”ñÓK›fM-æ%Ý ¸ˆÀM¦fÖÓÐ ùù|µZAŒÖ¸ˆ(‹2ÎçÚ8áÍ5Ô¯Ô4%™0“!ûrU·J¼J9‚Né,¢Æ à¨dMS®UnrzΙӜ•ÃlË#¾uöŽÆ4lAômÃú•·ÕÖ•ÞÜ~ƒTà|˜·rÍÂUkÕŒ@‡b¶V¾9nc…‰5B~ôyU)ù…ôš³¼Ë÷Œ¾íã¹ JË.Á¾æ‡{:Ö?öR¡ªaXTò3¶‰Ðx[-ÃnÔ{Aª¤ù˜Áý Ã!Gˆ£ßÚ¶:X vÀˆàÌzƒ€€/aXßžåÔÌt­(¡cë–ªä_(F”¯d5P$Š©Í„ !nFb…” 9Eb\\X†s×vmt$à=>œ™õ†> ÓZ)¶î·»Ô“hX(`!ªöÀ²Õâ ´«ð†ˆ»G ÃÇæÝûôêÀc½ 8[ ²smë¦ m«’ºx EÍÀÔ´˜^,æ%^ÿzaò¿«T¼ð»Z’GépÒE>Ö64KŒìľÿ ¤ƒP‰‹‰A=Bi`¨Jt8.ÆFã{pñ­0±ªŒš bêd–$’íiÆy›J~ä¬ eÿ§ÿô“ŒiŒ”¢+ã£jfϽvÛ^1MëfQ’Pgd¢—=°GW{î·¥ÏâàJb´Ø—³€Q¢"UNlíÚÖyÙ0!ØbýÛwä² –@ìØöïօ΂˜Æ·J»röºað t¸2а0Χþ«­!®˜X0`ØéV¿2búý›ïêz­±ÕÉTÀzPtlÕBQ3¡!ÁÉõê1ï£Ìtüô• µPU¼zÆ#O¸}L‹Ææ½Æ¨ÉÔ»KG¨¸ª %¤‡ðLÍ[o¨´™ÙDÑ]ß2Æ•&в¨`zùréåë¬D¥îì­¿@g¹ˆGmnìàC—¬ýFتWg V µq;©KVT8·‘µ"¬§góS¥.M."p“µ€±a.£­3,eÆ$€Fj+2„{•s[•{4BªÓ`#Çá)vIšÌçÖúBkÌYhj%ꂱÉb1^·}—JÏ4ê,—ŠW\q¸GF¨;ÀJݸs÷’µwÑ:ouz 5€¤€kÌ´ÙóQ9âízuê€ìïä™s„qwçð¡¬=­JÄ`z}¸+‰é£Õ›·ñ 9Bar˜yYÌ,ònßwà—ÿ|ão½×¾Us¬$Û+ÝT¶a4('©`ÉZ–\Šîˆi¢5µØo·ÈÎøù­xsÁ˜…ùÒû3ç œ% _„(G?²^ p˜ôíÉw£½ÀðaéÒ¶ÕÞÝu^‹éÅz^Ò%HÀE‚èë¤ê¸ 'ëdÞ?E„ÄT¥”¿”u’©LæY¾r(ew}õòލå²5‰ŠVcÃT²ºeu<›™ 'úÚÂݰüh 7EWӓ߇齌t¥S\Iãe3\ÏŽ.ÆÀyñó‚â¢ðÐ0c”iXUJ|ð%hCo‘2%ó|+"µÔ‹WuéWà)-Dó6µÃÏO§ô&àJ§¸’Æ›68Ë‹ ¤<¼Xë&TU l1ïÂeº‰ýŒ}›áÖ`êOüž˜jZ{ìWNÌÍê8¿ÿffcËŸþõù! Ž™ç™Eúþ*%’Pv¤ qøò6QA¶ç¦i„ï¿@U¯úk½@Pûéôt씢1#a‹éÅz^2•Sõooâ(¸É’&cß@ ´omŒ1…ѰQJ6¦ø oQø…uYa2¸ÜF½ô Ó°ŸàgŒ‘°ôZØÁcŒaÖÎ:¡ß¨ÜZ'VÉ¢t_Œå¶È Ë?SuËÓ[ÐúÌ„¦CHu l1ïû ]»ÀªÁ* å6bزÀuðC Èãqëîz.IéÒq.Â…©')1‘p1½Ÿ’a8ªNªÂ”Éxj V²¸$†xw«Æƒ3àäb 9´Õw·@I/‚€Bà&Ϙt.}Õ^/~ûÔU øoUfîÿ÷‹™üˆ¸RŇ³çsvLÛæMøøC®¤—4 ç"hÌúŠ”OLºËÅô~JvøøÉ—ßÿˆÂÛµhöƒÇÔµ¼ò¿§Þ¯×}ãmíܺ²rr~ùÏ7Éòão=Œ·h·òJbA@,`IÓôù‹+týgñæòèf! g<\FgN«­3úé)[NüðSáRl ÇsF‰C©@é.iç­ˆ@S3l9ÙºÓðÞÈq¦ê,=û”Ä+®¾ý#bàŠsü(‡O%Òc\é8ü>ã—VŸËcªK:Έ¿og,\‚àɺ‹.£ñ'k‘)¤ÃS‘ƒË 8y+çÑÔ»Æó«œº¤A@ðÀ“4©—ŒŽæL¥™‹—sF’Ó´™ßÿ|ö¡'ÕÉ ø }äîÛ5s›C(ßÿbGžâ‡‘S~®Üx¼4Ћ–­ÝºƒÃHТI£'î½Ó¡¿j¿Å³TØq'9{éJN©¨`Ž¢œöàŸ^øîKχ“åQ²ãB‚«Ö±L²^>~ï¯þ¿x; m]'9s6·Žýû<ûǾ’}!ƒ—u xŒ€EÇ!PX°Ê¶’áàõ/?~þÏ?~îöÛs»'õÈ#ÇHÇy »7¡':µn ‰9gÙ*S9Ö]–{® Y:·iõ»<óç?o:ñÈ~ƒÓmþö“ïóTqe­YÏðܾ÷9ný{ÜÊðƒwŒm–’œ“—oj€Ü ‚€  Tj†Ö3»!$‚ƒb¿cÛضøµh”Ò¶E3œ]×­}GNžBQ†¿„Û4oÒ·k'dý»wѶQÄ=•Æ_6‹l(¡`Rhh;§pÇþƒü•Ë{œuR$d|”?´wΖC¦0¼oeo¿ÿèqâo‘ŽÃ†ˆŸ÷8û°„ÉãFÂ\Ov&=ÓX¬u—¡ k“ôP¥œ‡ÌñÂwÝ(i";Oá€nßwpãÎÝÊçŒÕÔã'R’êóèä™s¿xùi³æÁ¨ûÉ“>uÿ½ÆÚ+3¼i×^~•Y£Ô%n!¨z3ê%áoÛ»óî}°^Œ¯}>7—ÛfêȦ)É„ÙÂeÉÎÍ#lÜ&rd«²ú&ž %YVQ¬! /%¿×·ð‡‡ I•Ù4åZÇEÖªÕ 1)ƒzt‹tœ3õo÷2oýÄ„aýz-]»qú‚%Æ¢¬»LQ'A°XT®º ñ:;ÜSE¼âŸŽ'AÓ·K§Í»ö"ÎÌÎá‡8˜1~ßøQ°yŒ)+- ±E]½;w¨´¥"A@p À¦fµsVÙ¬¥+—¬Ýh|í¸˜˜‚¢âSgÓu¤ÇÅDã¦øâÅs™Yú)vO:[;š0SçÓLÒ‘P6ÞJØv\ØU&ÝÑ¢q a¼fœÏ& IÇyƒ¹—y' ´aûî}‡õp­» b”JaÏ µÖ ná´sº‘µ"S¨ O3¢]Ëæ:ž@|l } 3†a»ëà!äŒèÀAÓ|2waÇV-kÖ 2&–°  À’&Õ#ôEÏy“±;[•{ÕCæL†1Žu¨’±GÄJí=®IC[gO®_—0Ü  ) ê5¬_—mÙ’µW\'“€—ØwÜ2åvݶt(zNë¶ïR¢ŠQtœ—˜{“Ò䮑C)Á8Ö¬»¬QRUãš­Û¡ZJ.^\»Í¦F£.ˆ’um2Üãighü ^hK×m¬TãïMûå?ߨu •c«_xb*Jl¤DCÿ{×òË‚€ lÞ /‚íî½c†¿óɆ—ª1vè€uÛwÂÇ~éÕwØåc:¡¼eLuÉÆ´c* Œ¿~õmTjÒÒ3ÔQ•йMk²Àâþó;ÿmۼəŒ,6…<9À±yª±^ »Ž€}Ça(l6Ä ¸kcwŽŒ‰1^ëÚ®5é8×áõGÊ=ºaëwÚÀ_©°Ë Á\AUËžý%%ŒH›^”¾&ŽöêŸ")f”ÁÁMã´{û¶6 cÝDŒ·ç®XÅ®*v_¹&iëÜ SÖEI@nqž7CÿõèЮu³&ÆŽDñâ·Eå‚Hè¦HæGT±Î ¦IäoO¾›&vÿ2]Ú¶Ò»»ÎΖ‘ nlØ)bÜ)ƒâw§Þ—Òàšì_§”€—ØwÜÄQÃÇW gBŠ”éÚ®ÍsOQÝ"‡¥2¾ó^ŸgghÜ7n”©Xë.{ìž;ðCÔцO–³Xt [·$ã‹aˆI6 zwéøðÄ $`‹‚’ ƆÐ4K×m‚ÉÚ°~½§œ$b&žAÀˆ@P…‡¨óž›zmE1f®úaXÓ(^°ƒ¬Ÿoò…qÓéôtì,œ¹ÅÃÁ º51ÑÑØ\˜òÞôw¥S\IsÓ_ÄaØÂŸÍ̤ƒàœaÜdJ#g¤*ÜZwÃ0¿°0¥~}Lí[ “žuÍ_„¼Q7t7cÑRiiYØX†¡ÊÚWçYÌ;ŸÚ¸¿Jà嬄ÀqÎÞHâw¸‰£ à%MÖX'ÄÅòs˜IŽ.>R‘1ÑQü,È#?!€y¼ÑâÌT‹tœ ªpkÝeÃÆ#ç…ïâð-ªÎÄœÊa %RªÕœš©"(K3A  @xÐí—Æ Õê 7Sí;I^PA@,jÆy$‚€ €€H𠓤‰· qq⮪õ¸r7e­iWÕÚ,ín)„š¹¥º[^6xàö1ÐÊ[¬‰Î?ûÐ}·Ø{Ëë ƒ€P3ÓUÒPA@¸Y\áŒ7¹A  # z3U¸s¤i‚€ ‚€ àB͸’$A@*Œ€P3U¸s¤i·$«7oãwK¾º¼´ " ÔŒ‡ÀI6AÀOì8ÊÏO…K±ž!P3(ˆŸgy%— T¢\ K‚€¿àœµ‚¢¢=§ed^È/à´£ O^óWSªr¹×é AœüW;š³:¶ii:Êâ Æ(ÄŒ>òH¸é5sÓ»@ ¸$ ×ñ´3s–}}àÈq,nâc£ã¢#¢"¯/Ün—Y3 ’ úRg_(€×Ò¾Uó»Gk”Ô ÂwoÞÈêL· ³KA@ð7BÍøa)_ð1W®\).)ùlþâ ;öÔ‹™2ºOçÖãjGú¸šj]\n~ѮԓË6íûÝïêÙmʄѡ!2Vë.——«îÈ®î=,ïW€sùòåó99ÿž>ëLF&tÌ îm‚kŠö›Û} ñ7¸GÛÝZ¯Þvðó¥[N§g|÷¡Éµ£¢Ü.H2‚@Õ@À÷Ô RüÝŸIÏÈURüWù'—ry€’âÇ–Kñ;¹#Å7—æõ=ˤ¨_TŒâu)ŽÇêWQ£F­°0‡Éè£K—.üëÓ™¹ùù/LÛ¬a]‡)%ÒE ‡öl×$)ñÍË^ÿpúž˜êŒCà”éÓ´eĹØG5ü3UÊ:åþþߥªÝOä3j†Áyò̹YKWì?|L¤øwÄu OIñ))~ËæwÚ89©âì>JA¯q‰ú…«p^ï5Ò{¦~áJEÞ1Î>Ý)S\\üÙ‚%g³Î )c‘Ç1…ÏLþi ?ùjáÃwMpXΧ_- úΔ{>u=²|ÀɈs°z3ì“©üerýë3žOÀw£^’ú†š¹XZúéW ×nÛ%R|z¡†’â/Ý´ï÷o¾7°G×ûoãlèYùs‰ú…CXÜŠô‡úEdD-û6 `*))I=z|Û¾Ô)£ûzÆ•))-;›™{éò•¤ÄØèH[-—._.+»Ì-s}ÉŲààš!!ÁËa¡¶)‚ý ·„ydjÛ…‚¢‹¥—êÖ©ÍbozäÃÛ2H¹KW"j…ñÑR]XXˆoåkàyïˆ^Ÿ.Ú0´wÏÆÉ”‚i€÷¯##Î{ ½™*eòoÀ÷²jW²{KÍ0ýåæå¿ùñŒ´s"Åwq‡iLRü´ôŒgší0±÷‘ôš¨_x#%˜:ÎOꬂ¥¥¥………‹×mdÃ0¸{k¿yïÑl(¾XJ^H ƒ»ŽÐùäÙóýßüá½ÛOÙ›øÔ“çþùᢻ†uïÔ²Ñïþ=;>&ê7Oß a}:=ûïÍ}x€þ]Zéª3sòþ3{õ±´LbÂÃB(a`W7VvéòÌå[:¶HéТbs¡/–mýzë·~þȑәÿ˜¶à;÷ ëÖ¶‰n‰Oƒº·^±eÿ¬%+žä~Ÿh,DFœ o¦çâT)ë”7˜ë¼ž¯³û;`Þi¹UŸ¶o~4=;'ç…©c@ûvÃäVcªAb%Åɬœì7>œ^ZVæ—¢×Øçæçç£~‘sá2 é8/qþ¦ã²Ï£~á“}¼±IôŒ™Œ¬¬g3Fôn_Ó}µßsY¹ÿ›»&*"ì¡qý!Rê×™½rÛ®ÔSÍS Ž¢öISÕí9l ôlßLÝfç.Ù°×Ø†[óî—_C îÞæ±;%×­óá¼uŸU ._¹’“WÈ—¦Ó«dÙåËW®¥¹|eÅæýÇÏØˆ!}•–]c8+A§TH"ÅøÈa¤1}˜NޫݾÃG ‹ŠíŸz##ÎôæýfĹ0UÊ:åC#ÝßãZ<Èè9oF QLié™,Àž±¾=hqµÏòî‡ïžà[¾ê5Q¿ðÇWôMÇ9W¿p¥ÞGŽ‘¬m‹k$@YYYQQÑÞCGé¾.­»Rˆ)Íš‡0Á>QÙÖ«ó«·f®Üº¿sëF=Ú5]²qïù  ±Ñ{Ÿnšœ˜W;-#‡ t®ÛmäÇèb8wâìù~[>0¶‘m›$}¹bë…B°`í®y«wP]DxØCãûS~ê‰soL_ ‘´ýÀ ŒÐyTߎ°X/^¿'*¢VZFöáSéM“ëž8›õ«'ï²/”¦ (>š¿~ÝÎCPLš'?q÷ªûÑ??«tüLœ›;†t3e©ðp>^¸a×ÁÔ~ݺT˜ØÅ2â\ʃdߌ8çS¥Â_Ö)àµÎâ øÖ%øü©ç¼D‡OœÜ°s/òf^Ìç-»• OP]»mçñÓ×6;Bè~qïˆÞ.vâR¶Ý§Îg+¬Zr±´¬¸¤”™BÝ¢~Á-ûi¨€Š‡KÁ­±ñìé‰Ñ[gu«÷ëÆ”arQ2iˆ¡UƆi!J'ú}•¬:nÍ–h:KSaüÒu›øédtb&¨™ôóÙujGÆzäWÞ ;ª6M¯)˜C¦@²œÍº@-г÷ðiÊÍÊÕŒ ëÙ¶vd­Y+¶êÆè€ÊÛºÉ5Zõèƒzwhž•›×²à»÷hûñ‚õ|ôÊ.GOgBú$ÄE/\»‹æþ1})­wÇæÝÛ6ô¡ÀÇÎ@99,A׫H¸ÖìHE(öÄ]ƒ÷=C¥<*-»¼~×aÚÜöú›êô®x œ¦»]äJF‹4ž8‹å‘ §Jï×)>Û¡“éE%ªj5ÝñWÝ2-¨9MÍf®Lnh›edçU8ŸßÔƒ°ž~‚ÆiÙƒ¢f©|‡¹üé!5:̰óV¬©ï©ß¯T=JFŠ_?>vöÒ¯ÚWo¤zÍ-õ ² Aøé«ÓÿùÑ¢?¼7÷Wo~Á¨¦.™š@IDAT=s¾Þþƒ|Ì&XµmÆ’Mܲ²P˜¾øÚbeŸ.øÙÇ[Çx0⬠”§öXL• oÖ)´Í~öꌿüw¬ÄŸ¼2®! @ÀʬõåòkT>ÚfÜ.ß¼Ï•É m3J£(˜£ßÿûGPäöodcœ-’©Gh›ýðåO£mF ‘,W˜ÅÝà»[”÷é=¤fägçä>yýA¤øÞ·»Ú—`“â÷nwàèñ åŽ.|ò¾¨_ ƒ`¼¡§ù½FÝ=¬Gé¥Ëÿþr%K”ÚÁ«õ¶¡oÑ$)¡nÕN<’É̵h³3… gÚlwàŽØ«Y«wBº:ûìtô¾J'sPd–~Ê­‘ÐñζŽóú$ˆ¢fЛ)¹Xî¬^ëø¤Ä86Žû¯ôNN~‘*WvÍ?·ýàÉ)õêÜH0!²iݸÄ«©|(6bö=£âa¨¼üáB¦~¸,Ĩ‚0}ªÒ„‡…Ú•“P*Æø7<4¢„‹ŒéÕ ª‰Ò²Qý:ŽìÓA=õàSÙiƒÃ³®zwîÀÏØWÂŒ8WŠ•4F,¦J/×)Ñ63âì0l¾Ãô~ôDo†ÉAþ®ƒ‡˜=“âûõ•ªMáפø êÕÝ{’Qõš»ê+7ïg™yzÒmØæ¶k–LÃÒß~ðÖ+65‹#§oÒ ñKËОm5ò‰uj¾tÓó÷Ò1¦€C… {m‰æ¯ÓêjiTjˆ0(pãî#ï|¾üò•«Øæ(% hž7>[ ã©{†Áu€ðzoÖ×H"HߨAü“‡aK —¥WÇæŽí‡vÈŸßÿ ƒç][ýìµ$À}ܶYÒ³÷ ÆOn€2Ã2¹Uãú¦W°¸õ¡ú½5ä +ôÊUϹtCz´AK dÞ.:2|Éú=´dߎê-z´oºhýîýÇÎ`–hÿ^“GõþûsLñíš%A mÞ{¬Vx(¸-Û¸7ëBAûæ Š/Bú|µj.Û´]–öÍ“ŸÊ0e·ÝÚC5r `—ËËo‰éѾ™} ªm®_ÍÖÝqð$åÓËsVnCùúoÿg®àr“uoÊòlÄySã-›×áT©ð÷fm3W¾(‡à»’Ñçi<áͨÍ"ŽÕÙÆy&Å÷ùkTËmRü˜(LÜûT½æ–ú"vð,íÊ mèÐÜfL›^®oѽ]S8®ù…%×$× aH€x‚ågw¹äÂaËí2jKÕ)ŒjªL²@`ž³`ÍNex í½…’)béùå<áµ;Á?¸oTïG“¿pQ\Îé¹)³‘¼,Ë9:»µ¦ñä‚Up‡– ïÝçÈiGë±Ãw«Q÷ê4¾ãòFÊŽ:XAŸAŒ¢<‹º.*,Z9R¯Üa b ûwJ©&¦xÈë§&ÝÖ°^ 3¬¥Î_(„:„±è6 °cø h÷Œè©?S µÂB;¶h¸vGꪭŒ\,á¶Þí9Õaæò­˜Žgæ@ ñ*|ƒ:–ç%ÑeÞ+"‡à»’Ñçi<áͨ¯äB~2oŸ7H 4"¸óðÐP—Þ›Kõš[ê˜S±aVœUµ’• z¶oºdÞ}GÓ fØ(CxéæµlTõ‹Ï—nîÜ2EGJ!ã/V´htmK­µ%`ù@aðHë[ NÁ2‰´ˆ”šÅ™,›kH¶˜äœJ?ê\¶zÊNý¹)#y”WX Û!ÑÖ}Ç'% ëÕžHÄaìæ• ·ö\ÖH¸hÉÀ¶9“™CLšñ4wôtÆêínH¸©_ØWj£¸P* †K4Ú)ª}Wbþñ¹Içs ø šT^~ûÌ ^n1zzûêbןŸ¾UÈŽŸ=~{^A1ì ĵžŠ¿gD¯ñƒºæä¢Ž£œ@¢Ë¢Kݯ?•ò»SF–Ëõ‚ÈûÈíuùö%À4R|#˜dº(è§É#{C¹òÍ(Ó¿W_|HâqÀ¦·t£‚9E)’×-©#Îã6KFû©RáïÍ:Ew«Ñ§á¥L5p´¶™zd¯mfœÜtvõ™†OÎ~*™Ò6c'Æ>Ä™¶Óãâõ»)}5ûùS×N@k›!ë'—QÛìûŽ6¦t+l¾[Ù}•ØmÞ ½«¾’¢’¥ø¾j}µ/‡®°¸Àí§W·Þ]÷š[êHX°@AåV«’¨¯_é[pº v1[ö;t*½WóÎøžá=Q Þà\½×¤áL[B«SØ¿¯òf«…ëš/¸Eu½ ´m4q@jÌö¨fÿS Q=™[¸üÕŠ8¹åV±®„]¿è8‡ê®”p׈aütJ|íò’œ)WgAÐÖ 1Î~>ÕÕ¹@‡}^Mʨ숟øH\ñgMS^·J /¡o½8„yÚ¬yü\‡.Só¤[#Îõò%¥ ÓT©ñ÷fâf¾m3Ôö·&ðíTNŒÛÔŒÚ&"ÈWˆ?ZÉÖÜhNÆRÊ­?*Òe²æQ©¾%€z)î¿ìõIi*'|õŠmZômÝkîª_°Ž°Ù´÷(¼˜1l¸õFö â$Zh/¡@´I r( ”PÈ(¸ž@I з€ ¢õ-lyá©«<ðšÅõhãÿphæ®Ú±ûЩ]‡N! cͦX ¯6ì>L Z>][7†:aýC‡–Ï]eSnu&XPÞfÑá€ÿ„]±"WÂJý•”¦4Éõëò3EÊíÍE ¯°ŸëmðxĹ^…¤4!`œ*5þÞÌœh›áäm3TúVo?øöŒeÔhÔ6c@›Õȉ™®Ù :nâ"Å5JìÝáÆð³wÊŽ[XG¨µOÖKRÚÜ·Ê%ø/-ãç‹’|Všà©ýË7¾ÀÛ2~œÑLò¬èß½;›m€gy%— ¸MÍØööåì÷wøÍpðH[ù²Tµu™öentŽKðÄ™,~hcà]íØ™¬ciY¸W'¯6ÓíÛ©eÇ)˜é*Kc£t¼­<=ÂÔDì[crÊ®ôiž›2™EŸŽÍíÓû0FQI v‹Õ½æe97àÖÌèÚÈCùU)ÄP…FÝð'ŽÿÑÃc±ÇþÏìÕZA }›öÆ‹ñ¥†ØÑ5?P;íåB^S^6%¿¦r*ÿ¶yJÃæ»pØqañk¤qªÔøû{òëPáFðoV³]b¶ß¬ÆaåË^P[ùj_æ(s TÂvר0¨™å›÷c{Ò¬a"‡ÏaÁŸ¦S95=¤ôlH¯Ít  3ù¢%s4-sÝÎèŽK&lrÊnœ©MÓ±)£Ü Ž_;†f-×ǃ3B"\<ã’Æe^^῾X‰Q#qìÀÎcúwFû­ˇõj·f{*·˜ŒM0€£(×î8„a->Y´‡1V‡ŸÃw¿üšc&##Âa…Âdeù1É‘±¼¨:Ð6¨ê4FZ"ö¸Í›±/Â1&+_Öes5,k0¾Åé>s%?9•s6‡fº¦fãîý…‡Ç6à¼À[‹J*°[ëÖ¦ Ù_ýdÉÊ-û7í9j*Jn­@ý•ã- ¡ƒPεH |ˆNºFØôã~ÊAœ]ײQ}¶xæpuŽ€è۹ŬÛ<‡¢<Ì•å›öcÜÞ½]vÊOôîçоgü¢8¯ q*¤ Voº´BS n«½Ù‡¯p«çŒ!fíáVÃäf½¯÷ÓZPÿ7«ýTï ì*Ønæ;´^˜4i~ZqaŽ/sÜØ£Š8iTmµ[7Ž“ç:±íÃú¿YÃz¥0g®\éÒ¬‘òœ¯egêIÌt)}.e¦kzÙ A$†„}/ºÃ˜æšoQ‰å¸Môl˜Ð¡œðeâcÿ]ÆÊªQ˜‹zÜìòNŽ Ý©U#Þ¯\Yû ¶<ò ÉâX–a«Ï¾Z½ƒ/ÿÏÞ»íÀ ¬½ö9b¥áY*‡çHpæ¼ûÀ­¾aTëÙÎ8Fv¤žäd%ÌåXwûtj E©3±M^JÝj§$ØÒ('©õ/)1[?%‘÷8œMQ|±Œ¹ÚUbɾšîÐ6kÝ$éÉë6›Eå´¹*R3V¾ö¾ÌµÕ.xqγF͸Ô94Ó5:A×fÀ,®:£}@;eçTwT€Gôésºµcݪ[< Õ/ 5ßþ|9Ðýã‡S”)5êáá¡êìe’’è)’TGе5Ã[Ç i¥Œ1/+(’ -^Ô)%à?ÎQð>ïe;“ ’r„=Fãñ¨š aÊaOÀNX pûtjŽÓÅå›÷AîôîØÂX5lœ©z:ÆZŽlÌ+a 8öÝY_CÒYŒ‹%÷üßG‹ÿôü$5ˆPœDÁde¤,)ŽS™!.›ŽÖBRoí‡B} šš‘ѧ:ÈWÓÚfz°P²LwξÿªHÍ8k«Š··.sf®f*ÇsS[SA5jà§ä³EY/a±•Ä Ê4;Øåˆ̤¾R¿€I€M¾ûNý•ï`Hï®$«´4Hl—lØ»nç!ÜÁQÛvà8U'ÆFCÊ`?ˆ'STgÖï< “RQ3ºaºÎ,¼`Í.N/79ŽêܪTe¢}éïÞ79”ŸÎÄÖíñ&ðÕŠÕdŸPåµgЄyhü¥2ˆ=Äö'8œ 'û3êñšzülr½:;SOqzëÀ®­g.ÛÂ8EÛ©OÇ&ŠøŽãi»æÉwëOšH¤‡ûÁÐøý»s Œš&%<~ŽÉÉ;†UYùÉ›Àݼ>œîWÛÌ]мL_¥õf¼|7ÿeGNûw ™0¸+ÎÙØ˜ú¯®jS2»C_©_àÑX-œÕRý¢s›VüªN¿³SjÒ0–Ãlàdì’Ò²Æô…7ùð„M’XÒXáXÕ GLm†ëÖ«}3Ò³4š ìÖ ·@|µR¦_ç–í[4ôã™Ø¦º=º=v:ŸGY+5çÛälU.ò£â.­?zÇ úí ä³ÜB8Bâ|¼`=Ë-JL9ùêÇ#‹çèÖ•[LÓ—­Ú¼U;ÈËÓsç/гwëŽR.ÚUdé%›`‘«¬ìÃP¹Ûæ@·I#{‰ò“B†¿>œîDÛL£j<ÞŒõûTÚS6¬®8 ®´öDE¢~ÝdßÈ6M’øÁ↕Âò¦°SǾ ËA&nå‡Z>«ÒLÕ‡Ÿ.ðï?¸_…9sû·ÏLÌ*w0}íôÐà`{9²Î(€FO¬ONÕ¹žÒÌSXÝxŽ| 34è(K8d(e«ŒÌrªûÎdäïÐq<ïˆðPpåŒÆK6‰ò“Æ\¦; E儚©œ¥–¢~èzHöªH¬ež½,x¦¼ördS¹µFQ ^Cž8§$MÈòà_>vç`ggÔ+ºGé®)Ýš ëÚ*ºŠJÍ%DùŠBâ/Ó#ÉRmà¨ScDùI"Ó†¢ÒUEÒ„êz üØ[¨—‡AÍ-ÛÁJÃB*ò+JýâsW3â@c¯~Á}÷¡Óöú†Öê0·)u—Þ™ÅqÙÌøýúV~(¼´¬ŒŸ –"«?C{µƒùdÁzÄFXŸÍX²9¢ÜFÁÅ3–M1 /Z¿gÇÁ˜m¢ ¹S7®6Ò%ŒÚ8î^™š–g â< ‰«Áè3!àñ­LwCçqƪ›9›™‹ët^‘<vÑ)ûÅ럣ã çSyü’•“‘zó¾cœ€èñnµrÚy³jQê‹ÖíFý–5š¡Zýâ‹e›Q¿ Óñ´Æ„¸ïèc#•úôŠCõ 4‚Q¿ ½R¿hÛ,ÉdÆo,* ÂÌšG;¿5鮀h­4²J!€ÌwA-w…4 ÇèJ«OQ¿dã^^`ÿ‡~ŒÉpÉøš=ƒ Ã{œæ£Èé¿0u,êý&ìÖ–Šöéq‹d:X»m“$4àhíß>Xð‹oÝá¯ó´­;3@žŠúE€tTUi¦Í¦ãÒ$&:Pù-›zçøÊ¯Ôã™ëuΦÎúŒz‡5Æ^×ûVO!bì7lÔ¨wŒ¢üd„Q¦;#~ W½õ’HÐ#C MC˜3Ɖg ³9çmÝ[hÆ '”G^ÆÑ n·p@çè“áõ RãO ”ã”Y¯½GvkKEûôhù˜ÖNˆ‹bƒBÃÛ/æº[0õRòWLೄ ‰>PÖôÔt‹$ñWoÍ$RL *á–R˜­„ºªHLhSF÷­"‘f"Pµ¨™¸˜Hìý ;ÐAÁñ¹j=õ?}l|Jƒø¥÷¦ŸÏËÈÉSñl¦Nè¶»ÓÙ“í>tŠƒ²'ì=ñ¶žš_bï‘¡“°Tä¼ÂÊRvNaI)·Óo_áZØa2|_î?vö“…ë¿X¶%§üèJ í‘Ý™I°±twÓ3óê-¦± W>löüûð´ nåÛµhÞ¾esÕZµ²ò—+48¸Àë£t¡ïßýr%.Cž™4¾æãga=&Ö±™ÝæÚ´¿ZµE´~[ »†8•zïÙ ò‚“#â%vÚ¼u°L09äQœ³9,™±€+64Ϻ·m‚_àÓ98jãK0¼wŽ9š–I-l`Æ èÌþßœ5][7Á_- W|×ÂæÁX†za”ê]öî—_C}êÉk¾2¹˜pY8‚û÷ÏÞ£¤NƧ®.-\·Ú÷f­:v:ãOÏOvVõß›[;2â‡SÇ8KpSâGôïí°Þ°ààܼB‡\ä@%8(?{|B“¤ÄˆZ¡p%M7QT|Êê*²æ c =0x!a!ÁØý*ãùÑý;’ Ýg' j  qXrZf.tFÂp_}BÉö °Ø6Évi݈» lõþøí<å FÐãúÁ˜Ùsä4tOqiÙ±r­]±e?¹,Û´ïBAqhh0 žšA5 ðЃr½shwü#¸…Ô6ÆmrŠý£M»l¤Lï.íIŒ P9 mÆ¡ô÷ ïeÑ:«%3è{NÔÒg)«M¼ÔŒy÷â&xI×&‚:ðÊR…þìñÛ$¡ZÏ4§bןŸ~ª¦dOœÅtðfFþõÛ_¶,?¾ÄÞ#»µ¥¢}zøóº‘ú`mÚð»gî©ÎÚ Úã“¿>…Ù'-r©´  rƸý6×”ŸuˆwTÞHMÔ-{ ÓÁ¢èUDE†+9£1 »öC'ÓÑÚî[~0+eStɯ^…·‡ñ¿N ¹ŒÞ7ìsY:õk¤ã}¨ðÅ]¬ŽrÔz:'w7Ù˜¸XŽJ†'4pƒ&àöäÙó W4ÂØè2Fð,Ây¸+$ §¤OG‹ˆC%ù ’§Ó³ÙØÂÉu§/ÞÄ©[„Á–BðGâ°ä£i6Ŷ͒ùKuXùB»ì\µƒÌ΄óœ°ƒ»·áé;fjÈP5ÆOœ=ϸƒÜ9ž–Y¯NmÊ×­ûŽaxŒ}ÊÔ½ V%5Ã,™o.°Å¤ ~b‚7…HÞ›…€ã©Òq¬‡m„ÛÁìÁ×Xa~ëiQcZ&œ•lœýÐ6c +ÉxQ“§iVÔJf: ÚŒ¶“!#Îø Œ>{7߯†o(®ÂÔþIà95â=ßÛõ—rñlS²¢âÒÏ\ÉlÈfe—^›éí õ#‡ÓWM‡å¸ Âane©0qHÍš~í8Æô%›Pª€\´g'g'áZ†½¾ëÖ†Ýf½ Œà^zûËþ]Zî9œÖ©U Ìø÷—_ÿø‘±¬”¬y/¾òº,uJ]r5ÊÊ.wkÛ„O¢Y²ù¬Ä t%Ô@\l”+)­Ó(:†Ù*8881&êйó0$pÈdËâ)s(3 Â\ ܼöïÒ ¢ô0ZˆÄúWß±yí³‰tË©þ*Z¡Yr"‡–s«.Ü@8ÂËÁâ©ôìß=i¸Ž×ûFõÓ¿SiÙeHµr8,ùWO^;Ÿ¾Ë[?DeŸ:~ÙÉ…Ê.ºÌ}:à™‘.ãñ´HW:´HáGGÇFGªd¿}z¢.Äã¨BŸÝ1l0„#°{\Ž}F¿Ž8ûênÁ‹©Ò'ë”C0˜‘¢mÆÇf~%Šž Z¦!®äÄ:ˆÞ”¥e%7ú©lÁ¹yJ½e[Ÿ•_ñè²ÔðWÇ¡%Ê.¼eŠM»Bí§;¶´éTBjØ«e ~Ä#­–d µ ~[ØŽ}á‘Qߢ~B ˆf ë™ô*Øßc!¬8 0o Ðð /%$Õc9Doædúy84€Z6® 4gBèÒç—XxVòÙÌ,~*/Meq…”©U~ÅEGƆcØ ÏóÂÉÅZÝ 1ÎHÊxVÄÄ%-ÔÙ]/âF":¯ °“†ÜtøÈ”RS<¦xÏnË™v›Ú7oÜ$9 À/¥ „–äçVá¢.¿Ž8·šT];œ*ø>Y§ê„¦+ÓMMkÌ6ø?kÙÈæGÍZÛÌ~öCDK$SŸÖ6ëÞÖ&es8+j%3ÐÚf蜡m6idoŽö^ÛÌ!ø7åKs›7£>ö.‘µjÅGEÂ÷ÆÞÒ·[™›DU«ÔÆ÷Þ¸7Ã6EZ.oZ¨JPýðÐÐèð?u\DùAZqg žÂñ.#ût å®¨eÀEÓM¦Í[kÒ·`ý³×«À‰ÐÉsÙJ•ZN”Û¿D†‡AÁ?s«`"Õ™_ŠÄD¬œÐ ÇŠ âÆb¦k‹† ¢"+Ö€v¥O5Ù¨Nôáóù¯¶3fÌè\)AÒX#)óÆgK㢣Fõëáàöé§ŒmiSi#΢ ·È#‡S¥Æß'ë”C0àueZ«ÞÚfÁ¿Yž'¼µ(2æ›×O@ íÍj}5®×Æ÷ÎÉïÚª8û„XÔ½F ¢kù©ãÐuhݤª¸ýß|7ë®!ÊÇ ›·Ô2ìõ-êU šÇîD%lƒÐ…¯Æe*^™ø¢{ÁS(o\ž¼òÑbLšøxˆôù'¤ƒ{tõI¯Ñ<ú‹%:¦vù•~¹´••[ \óù+Ü:‚þ}ÚtEÇ èS»¶3j&¶v4?·©œçV“ªebgS¥ÆßûuÊ¡N`º;­U?m3gàß”/-Hyȵ¨ûµiŸòô¹©STÒeggŸ9sæôéÓ[Í-)ûÑÃce›h¡»XËY«’êÄŒîß+999>>žÅÌÈž1uŠÃòiL½–‘‘q$#§ðr ?uê>¬Æó{¶ÐƒHh£^…} èëq–¯>Ï>mƒâQ4–ýS/cè¸L[ТaÒä±#ì{Ââ¬.û4¥¥¥çÏŸ?wî\zzzVVV~AÁÙ‚’‚K5êÆÕÙ·CçÖ±rV ÄÛ#ÀW´+õ$:^™¹ù ãc‡ôèÜ099!!!::‚Æ>½+1ÆŽ«äçJóªegS¥ ï×)&4{m3 E³Ð¨mF ®—ÌÎUÀ^[ᬨÛiÔ6Ó‘‚ÿú‡ŸQ &<.܃ŒžHšÔfQ ò›×«³7-SøÞ@ï, ŸxF†…tmÙÕfÑHÊ8ËhOvc¯Qfý¨°SùýÔqþP­Uo‡^?‹7E´lAÊÑm£ãX u8¬gŸôš~M´€ù`Í@Öpœ4gð–]~ñÅœü| øxᨙ:µ#1b×¹$àÔÒq 5ƒØ1&"¬wËÆÍ§ÔML^@j‡¹Ü¬äçnóªGz‹©Ò„¿÷ë”Ò ó7´ÍL…¸^2¢vS^u[ᬨs¡m¦Ã^,À÷²d³;FǺ8µ.*A>|ï&q…Çr á% X4¨{kúÆ:»±ŽÍŒñ‚gÎg€Þ t ‚jö*—OÑeeËÊ Ë.—eSçÎ3K„ ¨øâ×8%&¢AØ:q±ðc¸âââ€V§5Žž:ÍmóF6•v/q.åA2=â,¦J#þ²Ny²³,®€ï,¯_ã=§f” 8‚§Æ—.É/Æýò{…ïíA‡ùÞqá!X'àµvm@ö-5£{ j†~ý+ÅY/KÇyÐe*‹±ãêÕŽìÕ®eÝ›XЛ^»‚…S¦f8Ð1Ô˾SqkˆQýXû:ÃF%ðøuªqF@ãí€Q™»³O`|ÅÆÆ"„”QŒž:C`Á×kyôìC÷9K`¯VSqöÈxcqÖS¥ Y§¼]åu|ïëò O¨¶/¸¾`”ÆÄÄð‰0Ÿ–••á=¯¸$WøÞnv‚‘ïÔ¤vxBlí8fÙØXàd v¶_t«*S¯•””@ͰøÕ º]v9/OnÁYÃØqQ¡ÁmëÅ5Jª_§Žo{M·IQ0¬ÁèJ¨%½L^¼w¦L³m„ Ñ é )ÜXáPáááðϸè,è¸2 •CÅ€!fŠ©ðVF\…¹•À8â\™*MøË:åÚ¦Äî‚oÊ^9·žP3´Lm ™X™Iá~É\a»¿TtùjYaazaáY·g€Êyë*T ÇæÕH Š Žg‰ªS§¬o¶Œj¿Ô¾j®±×´úEù,_n;¸èj±tœËXÓq¬~uÃkÆE„׎Žò_¯éÑSô ë.Ó4ë1µ€šáb BÐ(RF E£ðRFAz 4„Ž!ÌÅ#•̘Ñû°Œ8ï1Ô%x0Uñ—uJ#éAÀð=¨ÅË,®”L ì• Ÿu‘F¨O¡ee,Jº/s«³îÑ3, cÔ±ed³)“˜˜X¡ ßYÉñÆ^3ª_Ðk uú1¬\'Czֽ͆æŠú…³’]Œ§|*ô#_ :†t —êSŠjƈ§î5pƒjá:(…¡ÂÓ˜Þ‡aqÞƒ©»xº5Uñ—uʃ¾ð|ªó2‹‡Ô µ2)(A>S'a>2n¹” _oýebuÖCêCQÐ1½²S„”Q‚|6úŠ1ÃSgÙ=‹×½Fv ½æ.Œ½æŠú…+Õ­Ú¼dƒ{}ãòß”‹9š•˜¾ƒ‚á³á/æ.Sb¹UÐqê7FºTŒ_!¢"fEº†Z¨NFœhƒ¹@ôÜ*5þt*¡|™úÿìxTÇÕ÷ Þ ÑÑ›è½wL±i688N—ÏŽ§8åÍ›8É›b;qÛq\÷‚Á¦÷Þ;B„ H¨! ¡.šýýv†ë{﮶ܺûèYÍN93÷Ì™3çü猹N9Úî0ßÑ: Êçº4#F&“©xÏxÉLC¾ã÷ø°,1ÀX™Ø)ZÌø· ùr ]ò8NÓ‘œ„,½&j7{ͦÉ<0MðM·×_HRvÇO[ÜQÚ‘fH•-¡7Áá'¥fd_,()+¯¨¬úæÓŸž†»·ÆQc¯Æøh nÝ<ºG|xMVýë}® FzÊqú±hgµ] åxsÄ9Î+eN£É%W Å|«s'›1Ò -ÏÌë‚ï‹Ä”´œü‹—ËÊ+«ª-¾IÍsÚÚ.²š¼yaÅç6»VÍ£{bÅ÷öÖfôhŒì5ÖfàÉiçüÂÒqÎ;ØðhSëñ[FÑqaÖŽs ~áæSÐ)ÕW®¼òÁÒKÅÅÏ.žl^Žæ&?G÷ïÚ.&êµÏ¶¼úÁÒg^l_CãfuW8ðT3K…]¾‘WS¥ðÔ³K'—§JÉsª™ó–÷Ւ뎯S5Ç1æs$Vü·%¥¦qÎWZñoÍÿŽhHynIx`7ÏfT9kÅ7–Sºð ³ãt˜|«×H·ÎIø…MMWsiâ,ˆ2œ]úxåºì¼‹¦(£Ë"×" Ÿ;ö¥÷×a¼[^µn×!«¢iÅ¿Å]‡ÿ;kÅw˜p MøE ª)Ù·h¦Î‚*D™Ôsç÷?¹`Ò`S+SSÏ8—?ï?€+>F LhÛRüéê P|tá}ÎÑÕä6Gœ†%ÎE¸3âÌuÊ9^kr»Ã| 1ã# fÊ**^yÿÓ¬Ü|ÓŠïrÿÜ+¾ ¿p¹¿dAOtœ¿Ÿ¯¤/ Ò¸ŒbÕÖ]À~Göë,ã à‚/ÿR)ÓVTXP³ðè[Í[×8äësÓzåê5NQÉËÌ /—ắyd¨ª1äáÒ¼ýb¾¡aW®^÷áÆHCªjwö'7æn=têËMÛž|p¶, ¡6ÒÙsÄ9Ë1m~—Gœ¹Ni™élŒËÌw¶"×òë_ë8-¶ˆ2…Å—P}cöÐDãx{êuNaŇ“—.0dÕeëSUu5UZáfÇérÉñHeÇ¿0¼ãXÈqø[PT”š•=n`7ÐiŽ·Í©œÅ¥üϪßþ{Åß?Úð?¯}ñ7TT]Âóo~ù›×— R¼<¿zuÙ_?XÇÏ”óy¿{óK~þïëËÉx6KY]AqÙ3/}ôÕöcÊH—Ãi  v"å[U¸LM[N7 kÒÙ4|öhSÝŒ1Gœ› TWŽ8G¦JsR1ПÎ2ߺœ*ëÖ´ÈÅŠVæñ¹ãLÕ·S|·“YXñ³rò0ÞÙÉær½&áÏ3;ÎeFª ŠŽËÌÉ~¡Nsï7ý…bæÄé³ô]ïÎmÝ#f³42ÓÛ+vä–ŒÔýÙÅS†ôŠKNÏYºáÀÉ„®í‹Ë*s Š)|>·¨¬²º·„¿Ú~|ÕÏ–LûñƒSÊ«ªÿóåNˆØ¬@‘P}å÷Ø)",A®xS]îH6Y•Ís?á- HLI5¶ sÄËOIÍÁ©þ›ë”dšQ™oTuŽÐqÝÒÄ+’‘•½ûÈqÓŠï£ÊËrÓŠ? ¡]«§ÊÚÏ,&V~aŸK.§Þî8ð G('§¦“­[\¬ÈŒ|À-Z\VŸs± <$048À".äAL9—S8°{,ïÅ;µmž•iÿÉ´“!»lÚŸt2-»e³ð“©H%†{¡Ò/\ŒkÛ<:"$Ðß÷©…/^*åòR»ª#”=o¯ØŽœ‘6-"™3¦Yx0‘o-ß~:#'ÀßÊ 'ÎÊ+zó‹m‹Ëü|Æ è:cd_žÈÙ"ð–ã œÖv¶ ü戳Ã÷“n8S¥¹N¹Ïd[jd¾­‚Šw]7ƒ!ÿËÍÛšG„zÔŠï¡Ç®ûd±âDÀŠolS]ƒ_€¢8s.—æÚõ¢= 'ªª¯2SˆŸ×oÜà'ËD@Äcsá§òØûÃ.\DŠŸ .”yj S Ê|+sC«”1†iaõU‡ôîwÜ–½ù“§Ë03!Ípqvkox ¯¨šÝãZIÊÝc-á¼Â’ö-£"Cƒ„s2í³?Q8싱é'ûäOÿ]… (®Msï6H»Ÿ=•ž3âÀ%÷ ǵnÏ ªØŸ˜†(3ihÏa½;m?|:5+ÓþdzÑÀ×î>q©¤\¶Ê£ü)—”X…k#ÎÀÜõ¤ì8sòè `Ÿù­ZKÜEi†ýbqIÉ™ôóãvõœ_Û܆#¬ølÓ¹{Ǩ§¦×œ…_PäƒÕ{žûÇÒ¿}¸þo¯üõkËÎfæÓL €ØÊ‹¶}¶ñ?YrÖí>Aó„ˆ_½ó8?•BFnÁebÀXpÉ…üKüDPã3"'}ºaRZ69YAŸüó", ¾óÕ.Ðò§±GÎ<ý‡ùÖõÞ>eÑqîÀ/pФôÑ$¤.s­¬®ò×Ûoã©Ö«ˆ)%K„TŠóD|'tküq>§P˜™ˆüÎŒáØ˜Fõï‚IhÕÎcÈ4ÊâºUNÎh9f@·Á=ãztl}ìL&Bð±”ÌØÖѳÆ$ÌÓï¾?3:A3UË;S¥¹NiÙelŒæ[‘#Ô\”f0ä?•ÂæØsV|GZwçV|q¡!Oêübãþ¤]ÇRºwlõÃû'Γpõú7—oÃ. Ö3±Y§m'S³ÛÅDŠ#0üÄ¿jNÁe;m¾TZ±q_’6 [qi…TùÈ èrX&·§Ðz‚)(À<€fPÌà)Qÿý[$t©ÝJ4ì¿·¤±4 ‘×)© #ΰghx„´S¥§×©†Çc›O¬e¾Í¬žLpZ7Ã8o ßžl˜MìKT™èb)lµÊxBRSlŽ(Ã"-#­+ÊíuxK 3x( V2íôêTu²×œ‚_`a¸ÀÞZBIðV,°D…JÎ8›•/÷ë²U÷Žë(xŸmxoß.í:·mšX2“¢Šå\HNع*’¬2 –=±Àþ°‰U‡Ÿ€¡‹Ò–ZRJx2?ý¬¾nIµ”WZÇ Ý€v©yì|ëÂ/ìä—I³&ŒáOþäYn~jAœ±Ö cÛ4P‰2²=Úüá¨9/eAú 5èSC¿k70ÚÚÑÓͼ·b5Ž×E—‰yÒ©ç8}3§–Ê©RòßÓë”¶ 3FÉü;ŧu3¢Ñò=mÅ×b/xA?\³ã:³M÷Ø–Ïõ×÷×Á8°è 8ê9qHüRpî†Èç¿wÃÞÄ•;Ž=ÿØf[­‹‹ÿíãžqm8•ò*¹±ãÛw¶ã ¾;s$‡8^]º ÐëÑÓçɆcâà>\»w÷Ñü¬cø@ ðÛÇæ(•’ û1,a»;†|ÙkÎÂ/& éÉ“r!J ìJ€Hpx/õU¨gP›±ø÷ëÒ^õ¤øí€QèØTñÊŸó&üÃ[_‰ UH6«vñù@Z4%žß-nÞæzm¶a#°ÒãÈ 'Îfa Xª‹Ò`Žƒ!lË žCoñ²4Šcþ“´ªwç6_D;ÿ¦ã\ëµ–Ñ·ehÇ«3sz”øÅwоË#ΩZÌÌJ(§JÉO¯SÊ8¦yŽß"|CÈ‹D˜åØq±¨9^]-ä”̯…ºlUá´nBâE¡õ¶ˆ¯Å^ddp¾†3œÏ ²ÉCb)FôíŒ0‘šuCáårD’²J¬E¬[ìm€'nì=‘ zä£h0Ä¢NÜÙ…1`AT° q×ÌÕëé qPT`C!ùbÖu sÆ$`¿ §åP­g>bQtŸ¶è5v*Nu ’ÉC{"ÇügŰœÈ(?¸wŒHB¬éÔ¶@0m qbËÞºyôEA¬u§§@`Ðq÷Žï/Á:"Š“[í>–²ãðim]"&8ÐË׫K7ÓÈaDê¢4ÆèÊ¡*ðŠm‘"'+}âÛ"ݾòé¦VÑávrê'ñFˆÎâ[ô+2cæ€!ÜvmÄý( ˆ½Ïå×Î:%«s<àìý!\6òÌ‹¡#Uüþ­¯8ñçxuµ“SÅüÚ©TU‹Óòíæ[EËØŸZìÅáSçIY‡øC ÕcÖh‹Ÿ%,Й=ÇSq†Á‰e$#§0#»pPÏŽd°úí@IDATž@ŽyúId@¬ÁðÿÑš½òY¼µ2mDoD„$D¨âÒ›ÇPŸ\0žsDrŒY[ĨaÅw“Û–>s ~-´‡VxL0›ˆ†ÊçjÓ"Reàˆ"BÏŸžš§ÌO±@Yä©Cùyî?`Úˆ>Åe¨Ä=…¸3áORø &Xm^^ÞM›H"hâ”S™7xY>‰ƒ¢"J,yœK@ô±AµD›ùw¤A®H3µÖP½à °¨<3#æÃøö18Ú/çv_+¨B`)°w`›À n×ùC‘ÃÒ(ŽÌžƒ‹ àÒÅ…ê)‚|Ÿ}p n386\YmÚùôoGê?>Þ¸íЩ'Óíä4“L8Ëeë7±a³³¥ŒÍÿâ»kðÑÌßÓ/~ÈŸt£­…Ó^¸¹cç M"×>_l9¬›dFšh€pöþÁ¢èˆ`eí:¶iÍŸÝ,ßJ·|+Íüá(§JÁvË·ˆõL.P¦"åaX1ØÅR¥{ˆ¨+<«ÈN*­…"Jæ×BuºU8›Ñ¥bl¤ìþ[çM0‚VKAð¾/["¡Äè‚'þñÓE2³gôìÔFÔ– l)0. ÀãuÇ%Ç¾Ü ®Â>ÜUVdLÔpB[(ºG&Äÿð/î:z e$Û Dü?8}8çç1çóD/¼·ö—ß»çDJ¦2U8PFCþËW>dž‹-ø‘9£1Û©ˆóø}æ 0†nf¦ìƒkÄ7–mËÌ+¢Š)Ã{MêôIÆòyêèÛø-c)›Ôäý!ÒºêþŽè*ï‘Ì*ʉ]|¬ã¬‹5Mƛɧu3²ä ›PyªpKAkµà —Èŧë÷ÿîÍ/õê2® ëÓÙsº—Y ¿ü«ò Sž®´]#—Ó ¨-¹‹`5Û,W8á´>pâÏß×[•*øs63ŸÂÝb[Û?ž’¥%B¶ {¹©g ìP¥åUè{¸õ‰û%÷ê¸bëˆ4n‹Çd¿nÑKÝ:¡ƒøÈO HŒRß#²¡‡M*ÀÈI6á [òMDJ]— )S=ÀdˆÞ”ë/Måý!x à¼$:~îmàM•s4»R* J‚u3Š«î4 ×M¹%ƒ{vdŸý‡·Vr‹Öˆ¾ñÀËÄͲ¶RÅ@¡à¦Ýl³Ç&°%àd˜z–áç–LC1Ó¶EeèƒfãBPYQƒ pg竟n¶ˆ2‹&sXÛÒ ^„4£¼»i·p]¢??ÝG_A œ‡#CI50E¡´îÜî¦7/bhÈå0ä7z8ÌôÊ*ÈI<ßJ"HÝMß<¯S÷‡üöÑÙòñ»thiJ‡’ª€)ͨbþ4˜FÁ/HT÷c;¿@ˆQùf–7ø &7z`£IºBL¸ˆ¬ÂëÁ‰Tœ.♬°h€k‰kÛü³M¼oÝE uìP˜óuSÑÇ QGŽ”É@—ÈëŸo¹víÆƒÓ‡]È/~í³ÍÜ…(ƒLìS@göO‹·+aP™•[v@iÆØ‘Ñsˆ ÷‘±@See¬*ážîÝõh\Îå\È»Ô,"8·à2˯©æ_Ÿm!'ŠOt9ÿøxÃ¥’Š9cû¿¹|×bõÃuî¦pÜ¥;”¤Gu|a°]Áƒ×#÷Žáò×_üó3Nl Âò ›¬ÞsèT½Ì9Sn–E¦кzç1|”2$MJ6\_Òl—»Ž¦<»xŠÐ!9òõ1—x(ïñ¨P×Ú\çp3*‹/ãÁb®¾Š´.ÂVÿi±‘á'Êò YY ÄÈŸNØ^°¹„œS¥ÌÌpÀMø…ê~láZÿÑu¿/zÆÇñwÇÛ‰v„K-–o9ÌKfŽ@®Û.&%ù ï®AY,ƒû)Y_Aã™UV›ÊSàõàÝU»¸ "®MsPöºD¸Ü•ê~ñÊço|±­W§6\%Äj¸=}wå.ö²ÊÓîgÎeçðWËU#ˆóÚsá¼Ò×¶­»ë½›6¦SR/\ä‡c¨st[ûû7¿|ìÿÞå3“È€ÃkjI9ŸË4yú\.7¹ OèYù—æNˆÊ…ö 7Ý¡tõÚMê=âZ#?aDÿ’”ž}åÚu\ª ‹‰—xdÅÓ†á­U~4@yc‡Â}’ Ú>ª°Ìð_Cþ-N!:ã×[·ýf¤É[¨sºq6Í}ñ™ˆ®)çr…™< ¨ÿý×Ü<€æY>¾÷WïfoÇ<@RÝ;¶–IÌ?ÿçgâ'K)&y0áØ¡—m>Œ»z Æ2§ãT¸uánmÇ\§r2a¹ ¿`§¼›çr~±~o¢ÊtbHm ÷–kÛ†Ùþçß4Ïâ*}’2H÷ø½H6¸KЦ ­8ËWx¢c4µÙ•ºwœ eiÂÁ¾ü©…Ó0r‰Ô6¦!ÄpdòÚ€ ÑQ5 lçîúØÖÑLPˆ˜lñgÊðÞIÝqô —ÏÀ{bÓ‰³P“ O 싨A<³%w¥ùû"ªžJÏ=s>Ww(Iê¼ø,e*Ɔ€K0Šf"ÀDÍE³€¨úti·i—ë5š3®?B0Ì¿—m=‘+2ÏŸ8½Ž›ß&ç@Ó͈¦3§1, °çY±íªl”¢ÕW®r¤S‹÷f¢Äm†y®z¶ ¢¬¥ÛùSpänme~3,9 àlÐr‚‰¯k‡–Ø,d°•ŠãæZòÐ;,¥%eU`8TDèeú:¦Y(ð NœeåÙ÷­ªÚüé8(E™Ÿ+ º©ˆAR”Et³©UUƒexüg [2czŽ·–o—Úe;w×£ÁJËÊgδ#Í`ÐAV௙¾M-`×zuj‚%&*×´¢„Z8@oÒÄËÖP’Õ{tlÔ"è êa¹RF~Pàýâá€~¸áõÛÊíG‘œH/’ÀÇ J‰ü’ ,nL8Â:*Í L˺)ÍØ?1t¹¬|¢å.¡½öX”UF‡QÄQšÎíZªt[„û'ûDäG§Ía9ÂÇS2ôÒÇø0ÅÃé“~Ë-§a"ž9 €gaîûá °Ûøñß>ùÉËŸpÙ²ª^ó§à€€_ÀŸ¿¼A¿àškðJ÷VÀ/à°€_¨R‘S9œx6 á•$æVm6àXÜNüð¬QÔðBÇtïNášÇÑë|/ÕFý¼½ÑŽà5£Ï[É*QÏp<7âªî®Gˆ))¯JÍÌïh[7#‰(ºÇ2 ØñL}xZ¿÷$"&`Þ‡’¸²7v«A=oÓàΣg½ ê‹S"€8…Åeø>%~ÕŽcfÜ| i˜'•Uׅ⣨på&&A‰yp§y,@L\lÉÜ!"ÊB‡BÊýÆÔ uÎÒ$˜ÛºŠG^ .¯F9•aÓbͽ€¢È¾­¦Vô¹TRÎv‹*™lõr’ïׯ߸zý&æÚµëb÷ÃÁH¶# & ®LŒÜ1ˆRºwkcÎÀ@ÆÍ̪kŸeE 9 àlÜñY"à¨XÀë>–~¦3%ü§#ÚT(îÇ9Èf†äÑf~Á™Uàt„€_ð á6—nÌJèêçNT÷ûâý«hä÷æÝ>ÈP÷Ûl¶Ðs˜=&átFg¶»¶o ¨…ŠÐ¯pˆW{w½€ËpܬÍ-ýŠƒ­õ‚<ÁQ nÚ´ñß>XÇbyÏè~„ªjJÈ+À~­Pªo¹w£Á‡’2þöáz,Yعe¯yDíq_ïîãg9é¶`òàº6yÚÁ<ˆëÿT¼ÝqøtÕ•k8¯WÅ«~î8r†}òo~0«ET˜*‰Ÿ!'¾|~úò§¨¸ð‹’[KÊ©V:Äe´k˜2œ*X§2×Ui¦Uôú=‰™¹EˆmZÜÔ|ÚbÙ÷Víæb&Ü™ïÊœ8ìâ•&Xú=Šâ“”3LÂT4º~g5¤Ø²€a$ÀµÞܽ¬$®º[k±2Õ ¿Ð½Ûø¢«Ötï—ª+5\VÇÛW6/4ÈrV¼Ö> ùÕ!Iüú‘Yʪµw×ËC¼ÿ÷ä\‘sñôaü)KáŸ?Ãá#¦ .®GÓ,ü¶Bæïϯr"mh‡’Ò£:y°#+Ï/œ<„?âq7Š#Ô«W®]㜹¨Ç TQ\VAÕ¸2"¡JY\d»³ß<;–;ÐÊó U5´ QOÚF¥d²s“Ò +ú+Û’ZØÔd U¶‰È²¬wˆ2ìYž”Ò µp7³rÓNUS Hê›F"'»vÄe0R¦4#9lXÀ*Áx¡êD¤°/³£Õ|ù£õ—Êž5R©&•MÖ»W%à¨T8[”ñ¼{Ñ¡"+ 2ƒÔà…ûwngw§Ô2AS˜–•ñf¸F’Ðæó&ñº©ÄË©P”Õͦ´NÖ¦ ©å·1(/™+™Ó-j̯¿ÖŽ2ê¨$ÍœVZá‘&° þÏ«Ë8/÷ ²Ü+Ȱ¸3”èHÿR‚?c|uTªJïàOyÒ aa%`iPù}ؼ?ù̹<âñsøØÜ1àœ@U³…FMõÃû' «}¼nZ+†‰¸îƒ'BìPe[¿ç¤}"J> İug¡ÄýÁýS qCˆ»¦øø4å@ÙÁ¤Œß>6Ä·ª©@¿_]º ô!8yX¯‰ƒ{¼ôþZÂøf ô÷ãzKeEõ(|ÛìR§î;Þ´'3AZàŠTüa<â˜Ý¨ Auï‰Ôœ‚˪¡kQÀêN²àà˜ˆ9:ˆù±š‚hÿPr*uóþ¤å[ Rhi8/ÊŸÜ-©ª0šh°X³ë8 3nvö—w×Ü­|`Çr·nZXtÑ‘üú‘™ØƒD÷±‡dsÈLx·ö¦ãÏæ­ŒÀ"³6›}"ÊTáÈÏg"’c.l ”VØ$ˆ$[M½+1uW7£ì<Ï…y‘ª…(C-@¢ŽŸÉ|ñýµ¿ý÷Š7¿ØÆ$‹¦Ñsµ›”%ЍL„ìÝøùäîq±ü Έ7“o>¾M›^6îFâì‹Åï~µ«OçvÈ+œöÂbËiO6Â䊺²àR)ŽÎ0®¯ÚylÑÔ!xq¥k,è©·WìhÒØ‹‹Ø °ü£’ACCƒ9yË䨛¯ìYødý~ôí ÂÉÀ)64=@¾Úvdö˜þH6$m9x "T±ÿdz¸VýâÛ.­¨¦øªGÁ×á2ímG¸Ï Gšý»w@Ϲeeö©ªJ]øÉ㇪¯ ‚Îþc‰ü¹@Ð,R¯9 ‹y°qXÝ ˜3šN9”ºoÜ$xd ä \ÃŽ’`I7MòS›ÍÊ(›D$±(1^p Í®›?®½eqäÔy®àW¾€ªùm4U»ð².—3è;Z»•¯Nÿÿ–PY§[Z[Ã`Ìe1™ùEáÁ ¶$d½¶êo õ¿X½ó8 ß\¾Y€›~(#5;d î³øz_(.eºTú¹×ÍéH$žîŸ:Å ~˜¸G‰+™€J$§Y,îkwÖ·3}ÙöAíÝU»»uh‰7Ž×î>v_±â*f‚ZCšAךý¹‡¦ƒëÄ/pB44/¾·M ÅŸ]<E™qùŠ~ågMØˉPÔÚyE—Å6²¢úʹì2sTM 6HŽƒ2ÿTF.ÇP)NÕ`h-"9IU!Ò¼EÙ©sˆò@b”õ©áü­;µ›eë&╘ÝÃêÛæž,nC‰âóG/}„[,´þøÐb€`W:ƒßÁHÛT”¬’” 0{tlµûXJ³° ‰CêëKnJ3²Co°"·‰äO`þ¶ËV‚À_ûÐ À¦\\ôÐÄ öTÂ/ºµ' x„ê‹lÊo–[~²|ÊH”:8ƒæ ¨Œ°¶äQ賎jSíQ·ÿ“©Ä~S-jë'"ÈŠxwÄ.î`Y[ÙxF¶‰8l°0¬Eˆ)èÀ1-!}l9˜Œ5ýžQ})Îy<ýHLGe²?1í¯?¾ÿÀÉ4\¾’ŸT4PÀin~Aĉ†¤‚ÅÁ'yš4i‚Ê ÐæƒÉ(>‘Yéëy!!Ù€¹!w• ¦ ßàˆV‚O µ`obÀ˜…¼Ë™‘øv-¨…RLâTFo¤,)w>ð–ÇéÛÞ"fÙ;Åýñ¦[síc´‡Õ¹,sTÿ.W¯^çý|áé¨9”0Ù F0p”çQ´Ùìæ:þäo·ô—Æ=büçËÔňfæP5•a¥‹‘ëcE¼¹È/‹ÉÇ0"`J3Fp±^ѨøÅÊGÙÿtÉ46÷ 3°;°•Ç4P;ð T/¾·Y‡‘ìçë“Wh9æ¦Ä‚,™1¼¾À/„ƒ„LÊ0È·)ÇØ„ãÎK†1x#›NÄp0ÜÅBa¹ Å žµ±ÀâyòOÿ]=*!þ±¹cqùÅm‚8n¦,êhPe¥åUì\h$F" `ÀB›òÈœÑá!Ad³DæÑûÆLÔãù7WùûÝšû¼T¶>(<²_|fÞ%NUÈçB´b{ýú׿þÁ,"©…ï°€ÖÑܽß>Ü¥ªˆ,ëB€ãå›$whÕ"0à¶éÙ:Ú"†8mf 2Ÿ›)¹áÓ´©±h3IœFåÞŒ$z JQFA¦—eE@7›}"* ÊŸìå>]¿‰E *FP;bÈGÛTeA¶µ{”ll1ßV~Å»5'z¨M&YÃ9 ^k¾ùÔüBÜ$ÇÆšUjÛ¡Ó£º°^Öü­@Fv!¾¤YòY‰µX;¿p¡g…(ƒ4çEEq½ãHŠ t”EP¡¡zºËmØœ¼ ,¼fEGX¼ò ŒÁ±5ˆÐ ¶w\(ñÌK¡¿ùÎôá`bÆ ìîÕÈ TbÓ1ZtîÃÂo ù×?ßÊ<£º €¬·“hƒ8¿Í-°œžà+’Ê”a½ÎfæýïëËÏ^°èËËÈ5YŠM$av«B[:¸g§Ö­Ý‡OKêÚ»èUYÖ…\Ř52¡›’¢¨šFÀ:à q.Y¿+8& îÞ«s[é•«®¼pu»˜ØN¤dnÜ—Tp¹,:8`@׸f‘0Ù–4âü­“žqu›©w¦uÀSqÙ´!ý)ÊA'ùÛ<ò`Z–ûÓÝy¼º]«-æß‘Vמ4ŠG9/—”Wâ[‚Ýyúx5 ôóÅðß2:Œ[× W•×ÈI1°k ~A{ð¢lUmÂ/žl޲j¤ÞÁ/è;3,´!!!•••ÕÕÕH3ˆbͼ®]»QZZÆ…v­Û‡4à s¦7ze×ë†ñfÄ ‹H3lá½›t‰kÓ<"<öÂdX­\u)8é¹çHí 'êX`d­#ÃZDE2¿IFÉÿˆ È_CÐf ‡±Ž<©-æ;RÖyjCšáÈžF“Ósp „è§½šÕÏV§hæ–Ub×`Ń?x ØDj­…(HÄ_ ¿È*³Üˆ#7ñ¤ %ø³ÿ,~¡Êc« j~¡¢#àªHû?U@û™Lel׿p”*Ó4‡n9æúuÎlYÜíЧ_õåç×ßTÝøæZEE~EEnƒÚ$¨ØäØÏÆ^šzy5ómæï{a2¬vŒL ¹<:âj¨»Á$3â˜Á|šö‰kGß)1O*þÇF‡'Y3»?Ý5îÖð v˜_CI%3tm5/gŸ®ß·ëØY< ÎÕ»[‡O¬"¶j¯ƒñœÈHÎÈÝu,üÇð>æOâ&÷Û.à<Ú`!ÍHø‹æ×®åWYà\´9¢_g´´mÀ]LøÅÎ#)lþ‚ü|ìÃ/aBtÄíûÿD~6šÌÔàfc0T£˜1ž®ˆ¾~ÝÇI<ùÅ·#5œ<¬m<,ß|`"‹å€XP ¢ Ÿ°°0Ø “IÕeKZæâ;¶m­›ªiŽ8]¶)Gœ_ÓÆ½:´ ffSY •ü lV‘Q\aNwîóßæ»_‹ <(Í”UT¿¶tnµcõè`.–tÂÜàž±º·ß2cõ®D\=>o<€VzÎÙ"bl›ð gùf?¿³ð ûÔDêüi:hSºÝ§TX’…¶†£‘ S”±Åa˜Fl„u,{,~ˆ2¡¡¡hee„b†T[Å×íØMÒ‹æÛÊ 7Gœ–'îÇ(G\˜oSîþŽÄ‹up°ó¤â?Vڶׯç”U™h3—{Áqæ»\…;=%Ípv÷Õ¥›¸…îsF¶m¡sï‰;®ïeì†ö“{ø»«öÂ¥-šŒÅÓŦӄ_ÈäÚ_È †5XhX¥`zåÊ44Rmc 4’i2ÓßXá¾¾¾(fø€•a!D+c‘rÈ#‹¨(ÄT15þ4G\,r*ƒrÄù7ñjì†@ª‹yRñ¹ŸaÒªQ£ÒªêËe&ÚÌ)Þs øm´™#ÌwŽºA¹=²ˆ2Ÿ~²nW}š¢ŒnBÈûÎô!ÿþb¼Z<}˜™Ô§’ĆžéÛ„_8Å7Ý̵¿PVÍB²î2M³¸`¤>X h„(c 4*¾ñî!ÊÖÁ=dAˆC˜Iž€æˆSv„›aF\¯F‘Þ^A>Mü­=X#æIÉDqH›Žög¼v½ÒD›9Ü%.0ßaÚ†e4^ša&ÍȾ¸çDê¬Q½M­ŒýŽ‚?Ó‡÷\±ýøð¾ðAç‰ùTÙ¦röô&üBɧ¢ƒøæ3™+…_8U63õR)US/]‰v9†™9†€ÔPÊ”f”¬ƒiü]†ÔÂÖ!Á ~*ó6GœûÌ”Ý'zP5袢¢ì`ž”üÇKcD €3ïk×@Ћ!cîlu“;Ì·EÓsñÆK3̪_m? ì?ižk÷]Cy`»§Á1Àö TO?S¹ ¿p‡ÉbxÃF:‹E‘ý=º.ÇáŽT½ýÀa²˜`+³fhcðÍLÍŒ,>¶J5ðx:N|à›E¢±~DŒG9C=æˆs“Ãtà¤vСžaÂaRuk‘ügt dæcÎtÙ¥t‡ùZj1xùDÔ-.-Ç+È_Åz´éwqtßÃûÄ}¹ý8|ãž*>¯&3K ±M1áŽ3¦ ¾1-º¿p¤®gÎ’ÍŽ4Cªl ½Y^Y™”Š>´ ¤¬¼¢²ê›oÜuñìH#ëYž[€˜Æ^¹";,$¸uóèñqàe| :˜S•ž2GœŠ'Ný”¯ºî C=‰h‡É¦KVòŸT(°1g<]FéFºÉ|]šž‹4Xšaˆ‡MsÝbc<×軌2¼Z¾í|ѯ ƒÍÓO'‡7Ò úv$~áÏÅtÉ7s¢`Ü«ø…­æeåæ-߸5ùl:#ÎâpYÜa¤?«Û¢ÑÀâ¿iTPXšzîÜú’r¶[Ýã:Κ0ºML‹¹0mÌðóØÊ`Ž8[œ©1ÞA'øÏPeäš3^<— a¾¤V #¥38¿ÈÎ/fV ttÓS YÇ«€Wp ¾Á=6žVÏÀ 1¼©È„_8þnȱ ߘùÔüBÛȫ׮}²jý®ÃǸAbÁ¤AæÍZÙgM¹Úýw¯¾5¢ßÓ'©nÄToߪ¥*Æ©ŸæˆsŠ]2³QƒÎä¿d©ã£˜ïxnæ4RšÁ~ ñrYEw‘çB—À1ø÷||Œq¬îH„4ƒüdÂ/a—ÈÃø&d¾EŒãDÜÌYRVöÚK³ò."ÇŒè¦ÈM‚ °8AŒLè2¬oçGÎ|¾éPvþÅ'Í㬶GYaŽ8ר{kÌYlvî :“ÿ.ðß(æ»Pµ³E —f®UT_åž_gÛÑÀósçsE÷ïX¼† /©5nˆ7•AÎ4ü"9휀_TVUš«µfÔ›Šnq¼yYàÁA­œ„_¸ù¤tJõ•+¯|°ôRqñ³‹'»y%…›¹ Š#Žîßµ]LÔkŸmyõƒ¥Ï>¼Ø¾†ÆýG6GœÓ<´:1âB­#®'€'W'IÉ|4%žI½ñ&ÚŒ‹ÍOÛ7â<`#¯ ÿP+Ú¬g—N.3_KÞØäæYŽˆ¢çÛØ&6j’{p’!W›O­ ¿¨ÕÔæÓºS—BÀt{6£j“ð G*÷óõÕÍÆ‹.íã•ë²ó.š¢Œ.‹\‹D(||îØ—Þ_‡ñnñ¬iºDŠKJ‰ ÑMu6ÒqNpìÖ #ÎYÀ“¶"˜¿b㶤Ô4®¹6#Ÿ9åi™u+¦ ¨,õüù »œC›Ý*]Kÿ “fÍÜ:)jOšá*¸\VZ^ÄSZ¶›W¯7iì%ýáâ×T£Ÿ÷•«×˜ f\¹ze^f#’ älÚ¤±¸íˆëø(Š$ ‹ÔÈEŠ õë¬1§*wýq+8:£.Zâ( k<„“hSUÕyè§ ¿p“±ÎÂ/©nñÌ©ÚlB”I=w~ßñ“ & 6µ2Z¹?ï?Ÿ÷%kÛRüéš Ðtá}îÔBYsĹÉ@wFWš}¼jÝ®CV´ÙDmætW¸Ã|§+s¾€aÒ U †i×V3ŠJÊ?X³?§°DdèÜ6zá¤þ¾Þ/´‰2¿xh ñÿÓ»ëCƒü¸`ì?—n)(.ÿÁœ±­š‘ôòÇ[š…}÷ža’¾Èбu³Gf ò«'¸ÿñâ÷Í”yì~ÿöîœ7¡¿ýlÚÔó¹Eo,ßùÀ”½âœ¸ˆNKGÆðàB”‘1ž˜ð ÷9ì ø…®n†w£ººzÕÖ]À~Göëì~ËM *pñêÖC§¾Ü´íɨ’øÉB¨t6ÆqÎrL›ßåWVQñÊûŸfåæ›h3-WŒq™ùÒw3›Ó: [õ±óWùêæ!éãõ/—è÷蜑 ]Û¦d^üjÇqÔ-=;µ*)¯Ê+²èr/\,FçÑ»ÓmaåŽ(`ti‰r$3ï ÂçrŠTZ™ª+×TeùÉÍÞZjäTF¢zAO¢ŒaY¶yd¢L»‘2Š‚ˆ—Dx|hÊÌÚ€ämª±1TQU] ü¢Ð ¿:`"IÝá°€_`ý)¸t ø…!+Ÿ²=ì@ˆ¥feØ œ“2Õ Â:qÜ€®IgÓðÙcA%sÄ)¹á~X9â˜Çjq¨Äe ‹/1HÍéÎMþ;Ë|7«s¼¸‘ºF,ÐTºÄ”¬üâ>ÛLÞ‹ ZEå”=“5ktd—GSÏœÏkrú\>©Rš  @Ê9”|~`÷öºdc¢B2óŠs J"Ã/^*k}.·ˆœ—J+Ðe\öñn2¶—1ýã‰ÜtàÔÖCghd»˜Èùû‡YTZñçw×]*­lñÐŒaØ­>ZwàÔ¹ÜÆ^^Ü<ððÌá({þøßµ-£Ã ‹Ë /—ǵi¶dÆÐ‚KeK7^0±hP«£g27î?UTRtߨ~<Ú²-GÒ.ÄD…&¥çÄD…Ñø5»¹Egîø~}ãÛê>ˆ…fe ~²a±ÔaÂ/ 㦂#ð Ev'‚ôŠ™§ÏÒw½;Ûxœ gfÕç¼ýhݾĔÔÁ}zêçp)Öq.±­æBrÄa?zpÖt[à?h3´2&ÚÌ‹\ˆwù.Pv¹HímòÐÊÐÊ.í›Ë¶Æ·³„/^*kÓ<©åô¹<~"Ó´µþÙ¢Bƒ†ôŠ]¿7 ÐŒ,¨ pb¯ut Î9‡ßôs³çxZIEÕC3†öëÒnÝÞ¤ôìÂÒò*d”ûÆ%ä\ÞqÄâq•OöÅËwÒ3ö|iÙ)™ùgÎçÏÓwñÔÁ9E’2ÈsåÚõ³™õè€\‚J‰°ÐñðöíâÑMˆŽÞwÒ’5  ÒÌ€ní©k™£úøûyÓK•wî#&V¿(ÀYc[ª¯^ËÈ.8›™Ï5ª"3Ì,©v‚¦õ<ÖuW„žŒœl˜ø©¥ÓÎçVV_Ñ&9C-RO&KQ£­÷Dæq9¢ Vð°v(ÀO¸ºóÐÑÌËËìÚ'95?YÅ æUVVæ\, ¶ˆàæÇ€·ÀB9­m qFœkµóžä^>•‘SPlQróS9"W]¹=$ /—åÝ´û‹"â[ d*#]CG9-¸LG· q@aÎgçêf€ YÙ»wpºÓ%bFêr Fæë–ò\¤aº1Ëómk²GÕÁc(­-,Ä€ÿåôÉÎcg‘.äOaQÞÈÏø]Qál9tFƨmc"f*ª¯´k!“S³|}À™r>/ߺ ,œ8¡§}L¤\tc[E¡/i¾71½úÊ5äïÞ3‘åð©L ¢Œ4‘½¸‚€ŸÈ+yE%H`"Þêà ê’ºvˆÒ³ƒˆGÇCËó/•H:M΢§Á&RµßðMòP›jTŒ³ð‹ƒIé­Ý'f@ŒÓGö™2¬WfnÑ_Þ]ƒÉcî„4,%3ïo¬Ÿ5¦ÚVï:>*¡ËÂɃ‰_½óøº=‰ÿɾ·pÜD®Ùu|ëÁSe•Õ¼ñí[}§(Ôžn¦yD0-Cí!Û‡A¦Y¸%¾w§V K0Çî×Jæ!À˜œ8¨+¦([»yÀ+H3 (HDAd&37 Œ‰ 6¬g§¶Íã*Ž/yqíÊ-ðA¿å@¬8,†ðñæŠ]•ÕWGöëd‘³n}|½-’ŸÉnÅYþóÔü±uc²@‡ôÊÒmB(ô‘,y¬ßžR¬ý0›$§ày…—ß]¹+ÐßgÑÔ¡LF­›‡3ÇHÉŠm˜”–-ádª%пÛM1wd¨£tŸ®¸´bÕŽcýº¶ÿõ#³æNp*#wŶÃ2gIy¥}û7â/’йœY„’–`¸2Rˆ°Ê˜‰+3«šÁOÇ÷©ô¸›ð «ã‹Û›!ÍpI^euµxW•M5ÃÆr ÐßgKZšCúöâOo?ÆÙgŸš­Tjy{ÅŽÜÂ’ñƒº?»xÊ^qÉé9K7`NKèÚ¾¸¬u e9¸À.BŒS®¹e2úÙ’i?~pJyUõ¾ÜéàÎ~Oêhe{Ä)Tù“Ù´£R™ÁÀ°q¨3¹¦LE–‡*.)9“~~ÜÀ®rÂWå1ºÃ;Ìw‡¬ke ÓÍÔX}«èp'Ii9K7B‡qèÔùüKeC{ÅŠפF†žLËéÐ2R»Yç:è9tkA%SVQ]Qyeê°hMȃ^¤e³0 3ÛEcH¢. LTŠ¡gõ®ÄÝ;¼³jO÷Ø–sÇ'h ²_¡‡¦ zÁª/ÑfùV ¬.ÝŠêá™ÃVíLD'¤µ€|«Àûá,üb×±³È…(`Vƒ>úõ¿¾ØvøT¯Îm˜%7îOâÇìO¦^hß2**Ì"•ò‰ þ|Ó§N?•ßéÙ˜™"BÁPÓAá!A¾>–705+ÿíåÛ™v™‡õîtÿ”ÁL=O¿ðေLBüé¿«8–¼íÅN·aïÉ@«{FDÏ7-Í9Ç ôè}cXI…ù¯~º‰&……>zï6—Ö§ÓÏÿùYÛ‘èÞ`uéóÄüñ¼ ¯Û‡|Æ[*è+ÇVØ@øK‚fÀÍ|mEn«1f¼³øú^RµÏ§þ=º9K‡üÎŽ8ª bʹœÂÝc1¦ð“%Íþ“i &BvAÑ‚½e³p©Ä –N¿p1®ms˦Äß—¡Ê ÏH÷¹µÁÓm»Á·WlGN"µM‹ˆGæŒa#Jä[Ë·ŸÎÈ ð÷…2JÙ¬¼"F%¸v¡ct1²¯.5c#ň;~:exÿoUÿŸJ¡;Å fl¥&5Á[̯}þÔžn†g›?¾?fç3?X»”Lßø6S†öÏÜËzŽ©wçÛ O™Äýp÷Œ´¹1pò´T˜æŽKOóòÇ›—n:ÌEšiÕ,¬g\+jÿײíÁ~ãv‘ô•„®íÿö•'Îf3Ô1~)SµajOèÒvωôß¼±Š‡‚#ä3mNbî r†mгð t3,çñícij0÷!²°ä§Øá%¥^¸TR޵^üÙ˜R™òÅOåw|»áÁË·~ö¯#sàü8®Eù|ÓAoï¦Í;¦—]ÇRNŸ³˜ÀÙð±‹âi¿0` ì Šx’ƒý¿;k$h€u{NˆœL¯L^“‡õB³f·%ÒAâä„°ƒ‘ýâ‡÷íÌ#œJÏAÉ´ýðéîq­æO„ KTQã·ð ž&0)£Tc•­±j[ c¨öƒ*%–­ÚkŒ·à9¬4A&ÉÌ'Rþ¬µ¢ ìr¿:ˆ8;â\«TláxKeñî±–p^a Û ±ßàçÉ´ øÉ falúÉß>aŸ€¶•ahgÖdw?ˈ˜?qà’{†—‰·?1 QfÒОlB)ì6íOfˆŠh`ŽkwŸ`~­ò\àæˆË»¨ì8Á\M"³™h³Zf¾çª³C¹öt34‚…ÿû³G°Þp<#žf”-›<¤;ʘ/º½¿ç°ÒŸŸœ£L%,3ü|‰ÅW ¾üËq$”¢ìòÙ%X-š2]+Ë$ï·0-ýæ‘"‰ãH’þÿ<<\Á·ïgÙÐ+Èl20yh‰Cº_.­Ë,ÈΟÐþ a%Ù§YÐ$wê#¶øNÁ/`jf‡ˆ0Õ¡óàÑÓnŠ,¨jäsÑSüD†Ðzâ ðûícsŽ¥d?“‰È‚Ä“‘]8apwv–sÆõGƳóhÊáäsݬӱ¤)ì/ €Ú;X=kt??_ïu»OÐ_"¿¯Ï“ ,¬/­¨:˜”Áî qQÀÁØÝت‚’A'$TôÚBƒØÎÒ6‘­ÆoáLʰ}Qc½ºŒU°’ý￾5 ]2c8ˤŒQv>çV;e¤6üòG@š‹xlÜ „L¹lóaÈýâ;Úüžq‘ÍêFÑeµxæoC}¤~&tkz†^zŸèFäwf GdG¯‰€²jç1¶xâ>ûND'g´‰3À¢£Â¸|ìLæýS†0–±;Ï“À[:¨gGv‰ § ft8™†BðêÛÚÙÄYF\i)<—%Á"M´™x+<÷­e¾çê²C¹Vu3¢H1XT¢Œ&º“Äò)EA‡ñÆîDȶ(3ª•¢Œ­lÊx&ûd•ùk?,ƶSð Ζ³Ý"Z‹¢c‘âgB×gÎåîÈú¨|"@…ìÉöLSF¶nÝ’ºuhùý9£ÿòô|T5{O¤‚&‰›ùZ'@e³!‘lÜùÖ~°'"ÊϬ-Wz@'bFŒe–·®LŽþЍZTW^iÁZqnŽo§æD^r]ø… kÿ» Wt7¾é š§à#[¾ìS£¬'@¬ß›=jâàeU/¼·ÆºhYÂ:Š"M6‰Õ“‡ü©J•ñC!øàôa<<˜­VF·¬•3ßÒ£P»5¥¬¨Æ°·¤‘”âBDÈàˆs„¬6’H2 Í"o0ö\búwíÀ8Z¶ù a±ë`‡Ö„Gœ?qÐoÍ7>^u•©’ _ˆG„ 0Há&~)qžƒóƲ­à‹#†öŠS÷t˜‡£ x.+üg’$#Í€'8 e¾'j©‘f­êfjl™ÁC`ÞcÛ)øÅ¨„x`)¸{GWà»qïIš7aðMã {¾õ{1Íà[SÕl޹²Îq¾IÏü3¤cúwÅvt.·á2¦YXëæè±ÛÆD¡µæ0¼˜s±j!ý0Én9‹Xb•1.ƒZ´!ßøáÊÇp}âlØ'ˆ«ÚjýÉ)Œ ûNÒf  ¡ÒËb3N~a3·"aö„1Š_Ö¥Õ²¼²ú¸¢4𨂠æEþÐÊü{ÙÖ=ÇÏNÚʼn³"‰üáýÖï9‰¼KÃÿññÆÇæŽQ¥ªîwôön"앬”ÿýjgr†Ÿ!>˜œTeQ齺tùž>O ‹¼oTôáš½´6Š{xö($¤[4œù¯Çæ÷V¬‚ÄC÷Þã !ãˆs¸**“.ícŽ>ÿÎW;Qp2”PIŽîßEœ%$€ ªÌIB"g°q_Œš:¼7šŽœ>ALÀ*²û“ÒÏfÝ<´('¡[‡•ÛîKL ðóÅhÕ§s[¤ü^Úl9˜ )”£¨cÿßüñ¼owàÿùr‡Š § ¡γ«”ü·Ê7–= ÖDÔCÝ,Ü"B–¡MžÓd¯…X'ß4½#ö¡°TUGž+·´6mÊU<¬¼–ª­û=¶€Òm,ƒ…C þ·Ìªfxî§d¾çª¨‘²<­±3Ãç€xÕœ…_ ”€õ{õnŽ2ñ vÑ-wn×B<€Ùèðà‚Ëåýº´×> ÆfU&e§@gM8p2å©\ö´}È0glËÇ8 sâI˜™ÆèúɆýÀkÄ"G#¶GÇV»¥4 ³Üð¥ûA©–œžÍÖ“í¤!‡ƒÄu©;èß{ÇΣg˜¸Q×ëfÓdâƒí.\¹¥KеH]PÅ…üK€*Šp(¢€UP   D7@,„œ‡Ç$aT!ñÂþsµðµ¥›wMX£'4œ_Ó¦ ›…|(î(£%ȲÛŸ&R© Ó–iÎmé x!©15ò¶à8‹ ÝÍ{ûŸ;xiAŽKún¸aÞ) ®8§ªPf^rÏD@zMÈÜôàì1·8°=ÀW€îD†Þ£sǬߓˆó4^@zº÷mÔ,,±ÞA”Äz}6µÇSªTÙ`sÿʧ›³øMÑó%°'‘¤¥Œ4CÒ´½‘tym\ŠK+YVÙ4s’‘?dYéA@qêÛen+kqmÄ))8Fø{æIŽØ‚ñ8ªÚšƒkáOI-¾] ˜ä`»Rpyð)ÅŸ2¿?÷ÐtÆ6&i¬gNxþñ9…ÖLbäÒw(f°3^¾‹„M·"<òŸ^ƒç’´à¿k]IYiŸEÝ…ò‰·èC3G ‚CQŒOzÇtîÙ>¶IDAT?Ø÷”‡ÞáÏëŸoáÐûK?Z A<¢U”EÝ+Än쳿{ü^8IC’YY^`Œ}–R¢ÍT©ò  z`êú‘ÍpËM†Ì£[–§£:ådÎ4 £ä„,‹;P1ßñ‚æ¬ßÒŒE}wÕâãG¾ "F~ã’ä굌qÓмjâm³¬=N®²,‡lÅŒå˜veqEé­ª}¬V%«Q•â' ]e¤ãÄ•¥dX… ’ñöºð ûE<‘*AC{wôU  N¼«@  ;µmœ‚ü8ø½‘B·y§­°*Žû¢ù`õvðØ"Ó?ߢÊl?•Ì(ÕðêÆ(–⬤`«¬/%È ìÈ ðŸ8¤‡p%‰ÔrÀçrSa–{v¨±z9»€iÇóƒJûr§p*bÄÁyYò_ü´ÃÝ$Ó>«Ë;‘JæÛÉæÑ¤ú-Í\*©xáý ’Aܰ=o|cËX·7y_bº<‚¤Jm ?Åð¶|;+Î4þ˜NJ²þüÂ"ÂÍ£"eŒ;*˜îqÃSp¹ŒM!²)@% ,È"÷€Ž„G·®)4Ü œAIY%ûQ½ÔÛOÖA+ãŠäËŠl´t»UŒ&_m;ÂY›ÛÔïDÈqµÍuëŠ*+ü—? ˜öY§ØeÉümæ;]܈wàL“Íþ |ß?ià¨~8‚/tÎ2¥¸¶ã¾k©  ŽÓžƒeµ™Í“åÀ²õ[ø3° @ÚµQ\lŠTA]*P»m@Ü_àZTÈúõ{e0 =³h2j`lôôÒG‡Oe`棞q­¡ðÚg›uS|FËr´~D¿ø/¶þ¿·Wçµê }3›Éj´Ïr@7œCïÕÇ¡w+êßñCï8ò±sNeŸ½X\ Œ‰6Hû,v´ªÂƪLUµSØgqWˆßaŸ•l•öYÀŽ”UÚgy"7í³²ê;¨ßºÁ5"öîÜš?´2ï¯ÙèÔ9®8xoõ¾¡½:rgä£÷Ž$ ®3ó/q1ÓÔ¡=¸Œí“õ¹h '(ʲ’¸pìËqOAìÂ7Œ D(òàšÀI¸FÁ,U   p…JQFðœmîM·@z©ö‹.eݲljå%º\‹œ2j˜kk³”Ç€¹X…§¥Ú|„:R—iŸ­#áT3î*iÛ P°àž‘½Ñ¾(ÙÑ/¾ ÷(qEåéóy\ Åu•«vžEx¹¼’?p)&Ò E´e•t̰ƒxñÝ5¬÷ô’Ÿ% ](ãé–ÅÓîÁä NRà#Z›Á£—ck«3cLÔG`û0ÊM3·¤a¹CÑUù`H›=qèû,¸ºÀ>;Ò`aŸ=”œ}çXJû,wëbŸÅÅ€6ÕÁ§Ã>ëHYz³,öYÎÊà&g®ûï`u-Û]%ͤfÀß-£„X¬^‘TðAÌmJH-Ø¡ÈÌFöŽaA– F÷ïÆM¬Ú²u­çêK{ðÊuïøø åd þµàó¸çè(9&6\ÿÄñ™Qýâ…4£J• ¼Iy C7vkæ *’¥Ü?(I™“uœ¶Ž«‹ê°*U>£k#—¾¾Þ}"IÕ¯€iŸ­_ýEkïiæÂÅËûOf``Ú›˜èß.&"=»P·'ÐWs·%™qÒÕ¯‹ÅÞ¶yw³¦¶nŽ™ À­ƒºÍÈ9€ÆKåF&Äÿð/î:zi†Û°Ù 0QröûÁéÃ9LûÙÆzá½µ89‘’©L—W#‘¨.ÇV!º×ì9›™{œ^LÙ‡9Úÿ°5>§q¸ÞÁ8b&¥Ç­›f.²V –3çr¥›æ§NP¥ŠÇ=e¿|åsνãtê‘9£Q$h³iGÎpßX¶-3¯ˆA=ex¯ÉCuüÖÔ£.1í³õ¨³î†3M\$’Q¦} ÷ÔPîݵ=Ñ/¾-¢ xaá¶hXŸ8¬QkvŸüÇ'[pºÕÛz·¶”ã>pú‚Ȉ2 —àg\ Vl= ޏ(UàÚå™*UT­º[K„lö$ââöùÇæ`ÏBËZZ^Å]†è„fé÷ð¬‘øYNÝR=(?VQwˆ£ßÆ!B§ý&¡mµzM´™ œ'm&»ðÕæíü¹M¦V H7ÍÚÁ‚[  !ܦ„›fmªhåÙÌ<Ž¿á|?„ÇS²t³iGV<¿=1üà^Wl=‘Z}f³²Ìãu3µ¹¯TÞP­ìÄ.í[(½ËÌ݇?‘¡}ËHeH·9cúÞ3²WiE56̶h*é{"\›|óDû¡ÉšB÷wÏ=4ë*q©™_Tàoq³+›­,בú¨Reâ•—c©Í†ˆ«³ÿþÑîLÀ/0.öq{o”XGÐ<=:ÇB§^Áá·©¿ô£…Çð½ã(]”ÊGÀ1kÆ…‹|jžŒQ¸õù7V,œf4)í‚ðkŠ$„'FO?©Ißä0Lš0¾½½WV[n®G û;|Gš Çà›äáiƒ§+Å…ZnAÉàžÙ‹ÿá­•-¢BGô/«¬ÆG¸²j[©µÄ!7žÂÙÇé’²*-.âf%.mæ*¨GS~ùðŒ:åVù˜u$Ìù>º7ÙâݳÕ*ô$Ùòµ­[ ¥ ¨n’2rDßÎ8‰!†[½ð’7ïÑâ–þ믥ßgtlí[5£ëÑÁ(É¢‘iÚ´ I(ÐõJÊfXºi¶5¦‹l¥ ô¡Å¾ÕÓºn6Õˆ{nÉ43Ró}ï8|™ãÒìM“5rÀ0iFÖäïÝ4¯´Zþ4Žp ¤¼ºEÈÝym=®}p[uõÚ¾©l¹¦„¹Y“Àµàÿ³M¼›ÜD^Ã(ìP,Nº©`{•—c#¦h³q=ʵk7œ>ìB~1' /—×5ÿ°Ž¼ µ“9`éÆ\@ÃÑw×ȮRäÏï¬æN`i.¾üázŽTpwàköœJϹñõ7xØÃE˜²yhDøùëGf!ýöøÜãåU;Ž!š°øàxjáD<‰íOL{ù'ðýñº}©þêµexO˜>¢ï«K7s“3o7B øú€O×ïG$å\ר˜ÇçŽB5\€üôïŸæ^R`l¸ì H ¿Ø´ŽV;W6µá„µnšÏåj‹tÓlk<¢Á¿3×q3ô€ÎèfS8U Ê0î°OÙ{<_A ‡óµö¤lè žö7!È£|ä–@Û<ÞŠ»é¾‘Ò œåè{&ç2 Ý£¶Z†š1𪤼*>&D0ð.c{5œÆòh€f–ÌÁ´ÈmÍ•¿ŒF¶nœÍæ¶ë9ÞºCÙ0i†F0*ØøyGzï8zv`÷,Ïî4®!”e« ¯à|ƒ{ð°!<5Ϩ½Œ—H ‰ÐM%ƒÊÕ¬n6m¤‡üÃz¨§ª®xvÆñ.,Mq­›ó©å¼÷f9$Œ©ÍV×-‰Ár´ù`òñ3™«wÇ`±hêÐŽm¢‰ç]…ÏlÄ‘k¶LÆžxϨ¾ÄOÜ-ÚÄtô+˜–þúãûÅ’ÆáÞÑý»r+$gƒ‘ž˜?ŽÌèPd ×¢¨ŒD‹2€Ôª+588SRcš…‘“í~f~QÏŽ­ñ•‚%à¥èÉܾ¥gu¡Á_#O›kù£ð\:|Ò¦Ò<¦#)ʈÖêfÓFÞ­ŠyÓ>[Ë/­SÕ¶v Q¦iS®˜÷‰‹Âû Ó™SMi˜™á¼‚cð î5(¦aöx]xjŽÁÓŒ£gÎcæÃè7Áê£é$#§‡=8<$J”?¼½’³ô?~p*2J¿ëþøCãE†èˆP¾QÌLÑ› “ÐÏÿùpïÇæŽÔ#²˜ü,€0¬BÃzw£!%¼”Š#lˆGB§’‘SˆÙ‘¥w¨d>p2“½¿ëËû“`Îåu° +ˆA>€ ψGOŸC´ló!Šx¼xæTþ¨Èü4dHû,à<Ô™Øg£Âƒ¬UöÙOÖïöYÔ‡ 1t%ÓDØß‰öY€bÂ>»h꧘5í³Ãûvf3;¤w\âÙ ØgÈT!ì³èAUöY°kè_Û´ˆ¸›ì³†éf0‘Àoo??ßèð fù%+wž@ë(LÙ‘fXÉn¸„KÍÃ1ø÷à!œTæ1Ã& ç&¹ÎíZ`èáSÎ÷fâå£t3¬Å[~r¯7i#e£’Á7‰ª%Ñ‚ÎX«» ˆÐ „ÀÚüÚýÎôáhV@é’Š„q ÈÙ4œªƒ ŽBp‹€ø²f÷‰#ú×X×ß;¶?þÙX D6„ "!umÜ{Ó‰”,ÖŽC’ç>æ¨ôoë eÓ>[Ç{Ê0i†çD¯àããíïïÐ%&øpfÉWî~hÆ0S Ñ} eào“oºÄ„À1ø÷à¡nf3Òä€à˜ÃM`ÄÖMyÝôÄ!=ù“µiù—§ç£qÁÍšîÉT,ò.kQê¹ïN'?jFi¡èÞ±µÌó£Å“%qä¿xÚ°ù!1`üõ#3nsY¬0Q1,d6”:ü ¿yt6î.‘Ÿ$ôJRv3` .n2ðn-nÚgëxÏfiâ9­ÒŒËrpP`hHp|”ï7ׯ¾¾lûÞiLšuœµÙ<¸Oà üKð ŽÁ7Ÿ+ÍÀŽ r E+ðÏÚ쎺\—XYù惨[^e$¤­â”íp0u‹¨0]QF·²4¥(£›G7’Š„?lÖh†j’Š e€ ùûùÚç‰nûë]¤9âœí2Ó>ë,Çj9¿‘ºæoï¦~!!Á•UUÕÕW:߸žqéêŠíÇwMÙ¯S·1.LsµÌV‡¯ýäŒÜGRŠJ+CšÞèá Çà܃‡m@%¾f×qp¦/>³àÍåÛ1HÿÞa¨:ÛxcÖ£SG]‚ˆºñª›jF‹󷖭µ¤ö=Aä¾õûÖ!ås™#NÉ G¦}Ö.ÝÁQ-þ‚×W|P_c/‹Â&¸¢²êꕲ«®T]-ª,³8—àSÃMs"Sýÿ¶*òYä›x}ÓÌû›`ŸFþc_B”‰Š  µàfà[=UÌìKLý|ÓAüR öŸ9¦ß¤!=Wî8Š×Ÿ.™†’€š¢kÙu4Eé"¸(Ž×ÞY¹‹ƒ*Ýb[âÀ”#ÁyZ¹z ±ï§/ãõµ ÐþS‡*ßÔ6Z×´kwŸX·çðUœÏþpáDûß[ƒ¬ƒ ´Zø;Qú«]2cøû«w %ÃÐ>8žÃö›×—íw25gíâÒoe½.„!îB)U«ãÅ‹ 2*$ðl^>sGzì6"Uí í'¼åîÛÞ¾ó÷´#ΧÌF8Æ á32!žþZÒvŠ6ât9nÀ(ü]ì³ßú­÷CØgõRôã„}V?Ín¬ÜŒ1Eä¾Ýì–#÷Øgíçq-Õh6»Ò ƒ¥šÀ´ËñÎU†w:æg™yeeU0ÛÞë×9T&>®´·~–¹µ&yYN°ûx- ÂÀ„VQÔ ,ò(bFÌ΢ÆÂ/²/¿ûÕ®a}:s…³²\R=²oâvÒ.*±äç*ApÏœuÜzè™9·‚††.,.c6Dޤ’fäÑG’„kZŠ|µíÈüIƒ}}š¾»r×–ƒ§fŒê“‘]ˆ0Ý=®õ¨„xä*•¿ÚÛŽäâòdü¤td¬‘ýâ­7{ßà´<çŠ;qIhÈ¢(D˰òñ öõæÐ2B‘õs(ÔÝVóÂl:Ü¡u 6÷[YË#Ž%Võ’k25âìŸ~ñípƨ:±üѺ}žq@Êü}-'9/ÂÒE›¹ÿnÜ}TÌ¿Sh¼4ÃËÄšÍ LÁ [Á·•VTT‚¤¹výšUmÓ€øÀG‚p¤#‚ šLK`eø&L #Oí¼ÆÂ/`pfpÿÔ!l/N¦]àV®dÇeYÑqÏà;ihOÜ!ðtJ±»Åo:ŽñsX7+ïÒ ¾öÑ©<÷ÐtœzáNA ͋ﭕÎgU®iq_ûÅ–C€±òŠ.[åçFÕW …b/xǧR<¿)ýÕz7iLÕ[GÓZZHŒ€ç¬îùquêÈþÆ‘n²¿p¤¬2e¬B0ç÷½cBΔ€»>|•9Ͱ›€«¼‡÷ŒÉ85VX¬…Ç{«|Éu22âØŸÔ8|JÒpµÆ"nöÅqÍÂtL„صó‹-Þ¨Íç8`‹ùž«Q—²ñÒ Õ°0[!Íxûƒ¤ ²À‚6\»Š F(oÄ·nËîŽH!£ðÍiõÇãƒì‚¨‡„ì—8•] ¢ Uˆð tmÉéÙƒztD”¡¿°!š ÐFýÃÏ܂ˬ8¤Gªu‹‡4Ü„Ÿœ (à –[–˜—…Òô ¤^¿qCé|Våš–+—‘]ðˆ6:ÜÙ‹˜"<ÜMH D+¥¿Z”1¼uñíZP ¥º´á2z£ÖÍ#ŒeìÀ/D“ì/]»‘ ó¦Là›.ã½aK`yWüü‚BKÊá7æ˜÷ùÙg£S©(6>ßt [lÛv-c`8lwª¸næ›ãÍË«Fœê%§=qŒ¯‡ãˆ}‚ô(’µÆ"º t<’‡»¨pËUw²”àh`ÀéÌ2˜h3ÉcºÌ7¶ ©yDš¡nÞ$¶8Xš|}ÑFø_A’ÁÎ6Ø©áõ¶˜¢Ä·ƒm­ÙÄèâ[èfà P2_nDŠ!deQ-<—Øb£o ü‚‡BRáš]œW"Ö°$<¹`rÉÿoï̃£:ò<@W•ª¤’J ‰S€Á`nƒÁÆøh»}¶Áži:ºÇ½ãžp{bc;bwcÿ˜˜˜ØØÞžÙ؉±{=ÛÝá]ÛcÀîq{ÚÆÆÛØØÜƒ·ˆK7’J·ì§”ðx~%•J¥¸¤ï BäËÊÌ—ï“•ï}ë—¿Ìd95´[ê°Óä™Y€ä¿¿²‘A–ˆÝðÑ®’Sçq™"/.2¬ Ë<¯Ý‡N!\¨${îP î `±[[%g¦{XyÖ biZ\d~þûW-šù·¿}‡¥i¯?ºX‰Ÿ{èW,ŸOdIÇzµ¦E|én¶\Þ¸íë©ãó˜xÉÚ?Ô3V 1@÷‹ÊšZ{MŒšq¹\A;žÛ=.Ós²6ðâ†-; ;¨¨Ã|o_Ú°ÅçI»ï{ è˜-Šî&æ&÷8Ç—|{\·\ß}V{—áSð7Ï=¢›Óã¸Ê¤qù»¹(ažx¹þÝGOÊÛ,Ìwu€ÁÖ€F}°ÔŒ©_&Ìã¼³ywc0VƧƒZÆ D]ñ¸Êhú˜Xhx\rXïæÜÊõ«ÇÒý‚Lì΃Ÿï¯ƒËÅŽ$Ì$Fs;ˆ L5«W-ä6{\"–¹Ÿ©üíÛ[ÙË& 3­¨å«Á¦»/ÿáStîòyÓæL+²Ã ]š–-~Núì«£ÿõå?âÝö“G–2P…mÆ,kò:Ö«ÅÕ÷Ôùª7>ØÉ7£Î’ÙSXƒ¡mÏb¿hñu¿àê|UxÅ¢c¼^oSSSkkkA{ǹ@pO­Z¸ln1ã"ý­¤Òü®Ú¶ï8ûrg¸]ß¿sc¿½©™g~Àö³?"~7³Ç9¾äƒ×ã Ög÷¡ïÛW”¾=nWÉØìLÛ s«m ,à~O¿¬ämf‘‰a ø¸ë ~ /yQ }ªŠzm=ްöiShggg ¨ªªZ÷þ–ì,®”^ŒÛ¶“…ÓóÆu2«ƒ°ŽŒM­—Ÿ[ó8o2t¡•ËÑ(V¼=`OC+·´´ÔÕÕ]¸páܹs§Ï]ÀýâéûÇÄý‚I× !&ìS¦í5!Œ€u,K$;:ºB-ÀÄ£ð‰qÂ)²ƒrKÓbóÃë×Èš¥Ç BH¨>—hë1oøH¼ž×¸ógO><£x ÄQ%{£ôVNh0¡cjkk+***++kjjMM›ÚšºFäø¼«ϘU\È_o*>”ߨƒÇËÙH¼º>Ÿ•±|Þ¬ü±cý~¿ÇãAЄ¦$ÆÞpC»ÇEÑ}¢È sÒ˜÷è]‹fMŸ–••e:ÿáã'w+‹Õã.ÂZ “d¡ð_|}÷n †›Éapm3Ü ß*F—pçÌé ­â8îÜÖMETÝcµDb’ ´¶_ ÖmÚÉœÔL¦×±*Ì]ë#0ÁøR 5ƒòMw%/œ\8±° ';¼@uL( íE÷‰"K$ AôV”ë/kìjæ÷ƒn–/×ë–·Y$<û•¦7øý*$†‰cÓu{«Ö¾LauåUü@÷y],ZŽ[ƒùÎõ–qèÅ×44ºP½õ«c¼×§æ>tç,¶T¿³ÃŸEÒ¨¹_X@bˆÄý‚‹Ò.ý½4&=žÔøÍtûœjM “O=íÍ—;Z[ªZZ*ØÔ±¿¥ƒô7ˆ'°ìSBr⨂t×˜Ì ÖzÂÃáóùÀ dÀFÍC=.jtÑe4=Γ’¼xæTžlFÍXEÙŸxÓ Æì--—·™gà0ð£xÄ ¼>”0XjÆØcZZÚÞúxÏžÃgØ#÷Ñå³µ³v)v6ØöõÉÿùÆæ·=µr3›Œ&&Íi/ÄíJ­®»dŘ¾-÷ H ‘»_´´¶åd§q…9Fû³ð0s$ ù°‹&¿Œµ†Üh0Øx¯l†•ÉÓ(ü©ùùFÐñÚãå‡1&##ƒ± ¤Œ1Ìðio…”œ(õ¦¥ŽÓ[âÕãÂÀ‰íG×zÜæÝîäÄÅ·MñedðdëQ͘'^FFúÔ1YG/ÖÊÛlà ~$¸×¡ÇEÍðHÅ$SS×øû?m«¨mDÇ,š9A¾Š4æÖÅ·O\0cü®Ce¿ø†¥ž}dYvV:Oؘ«|éÞ§Ë­VçG'ž7ôíôôt|h˜.Ï[p܈«¸_àçññ®¹_X¬"„º_ŒÎÉ2¨{ü•_ߘ2¾0|ù«¿_h£`x0†ó˜ÆŸ¦½ëŒYÆiØùׇ‚ê1h†z…¦a–%þ1ôt VÀ†ïƒ¯½³qÁ¬áÕŒz\ðciïqÙi©wL*ÌÍöÓŽ¡ÎñÄC÷_¾\V]¯Ç]Ô- üHqQW |ÆØ«#eMÍ¿û·Ïë›ZŸ{â®Â1Yá+1Ü>EØ-™Å ™ÿ÷½íPzþ©»YF/üÃ4 Dcssjëê~xšì\BîQì1Kî´-B»ôX`Ÿ‘¼iAÞ»<¦y0Â5ÃÁ¨îðYÆ©OVöp㔿Hƒz¼üˆŽ!ÌÁG&™=£®ª­«khìS†ªÇYÄ#`ïqiÉ£¦åúòù铃u­7Ÿ'ûÅOa ¿®±©º©YÞfýj£áðׯ*…&޽šá‘Ê*yز·². )JÜŠAäýäá%ÿüöç°zæÅüê¦ïYŸwjp?˜à>wþììì0>Oö'ÃTÆÄxššXɵ±½³½­Õx›ñ‘ÎB«¿ðþˆ ­Cä1±|}rUž§'Ï\ÜwüÜcËgË*¾%àóðÒÛßùìÀ‚é§N, §q„Ïù§in׌ɓ>Þ¾{Ùü9V±<Êå~9ÃДæÕFÞ‚ýr¿ kдíZlh ÓΙ«O ­É›˜  `00ð—'5 Æ=fQ$ g¸M÷abÂÃÁ%iÓ¶³§Mq¥¦„O©žOÔŸÒLä¥ÅB;]ff¦1ÌðiåožxôS§ <1EŽC÷!#Ÿö˜]‘‘Ãïï#.æl{~\F}¾8d|´ûn¿làu9Ã'㙾8P ±¢±Ù¹šé¬¬kôy‚KäEQ›á™Vƒ.G(BÞŽ1ä€{Åó?ZýÒëoþúw¯>õ઻æÏåw¿)ßôm~µpQ¹_DÈœn¸MÃ_ZFŸï݇U)C+ôæš‘šé/Ÿ~ò¿ñÖßÿþÕ'î[yßÒÅ=&Sä`è—”¡2êq1o‘ë}.hbé³Ó…¹ºžxaàôöQxøyÄõvŨãc¦f­çÆ::;Ù1g±Žú&¿ûÓ=©pƒ !É(†uf=_>»vý{®{wÓ';vß»dÑìiŬÁ%Ì7•NÎcBî}2·ÚÅêáVŒ#/‹.8z|Ëö]•5u 0ñ{½_RÆ”æMsÿò/Ö¾³åÓ?lÚ2eü8˴渖NcH€)¦_:ÂFfþȃ©)}¸ýöv]õ¸ÞÈDou±>;]$…›BôÄ‹„iÂÀÉ#.ÂjD˜,fj§~â½€;:½^O„—W2CÀš4{Hòû#¶dx•®}ì¡‹æ½³yëº÷6ýËŸ>ðû24¬^Û ©4–¤Ÿ³® ~cÌ`úË5OŒË ·€lxbXhž¼å=‹df¤[)yÝnßÀ:%@û®~pU–/ÃD–_¨x÷“ϯ\½± 0ËlÜ·ô{– NSKËïnbn·½y3¦/™;ÛŠáKR×Ð`À‰çñûî±b6±óhÙië”ûÿø±‡™ne"ÑsÛöì·ïµÀ/kn'×mª •Õh5f–[…°&úÊ% §Oš`bZÛÚ_ÿ·¬óh% 0kêkÑN7lü°Ê¶æ51c²ý˜!­,¸ñ:Qjp¥¤üèчRS®yܳÎï';÷4·´^j Ô76òªc®µõ·gŒ<¬9+¥Œ/±}ÄÅðÞc¦f¨“47aJÛÍ3¥.¥§=–cˆæ&ÕmÜ .2x×åµúŸnnm=tìäù*öÿ ð½´¿lïÒäd^Æl\€hÈÏÍ™9u2KÌÆäÆíR&Xà€-w×ç?ŤvƒXÈMûrÒp£³ýÓ&M@ ¡–b¥òÕãñË¡¢oAzÄÅäVúömî¹$+*ªªªuïoÉÎò<¿æ^Sì XªkêÖmÞ˦?ýÁ’˜Ô¯ÇBþ׺ÛÚ;ÿóOèñÓXE~¸£dï‘3WÊìâq+æǪØËyåÝí ϬšŸ“ͺ–Æ}L²—6lij½üÜšÇYSÁîOãh”ËT¤ˆ€ˆ€ 1[« ^Æñâ.©HÚ;:ûl¹ÖöNœSɰШ-ÞÌ0údï±¹Ó óül?túb­#}ìOùÚ}ľd•(" " Cš@,Gšn-¨íKßÿòPg×eFÚS+ç] ´¼ýéþgî_0câX¶x|wÛÁ¿zò.Wjòëïï:_]Ÿœ4êžùÓîž?õxyå«w²kÒî’²Ÿ?y×ÿ5Ïü îœ=éïÍ`×ÌßœºX]?>ÏkoPWè‘@Ìl3˜¸@иÐãu9³yׄË/V¯ÀÓpÓŽ’éÆàƒrøÔE®|ät…ךŸ›¹ý@iCs+£`s§‘æÔùv¶!ï—NΜ”Ï ’UÍéò¹köžÃ§_{'Ó§gM)°>¤Ü,†ƒt +" " C’À±Í$%ŽBÇ*½°mÿÉšú€+%™SÇ9z¦‚}8JÏU-=™öûæäywJò±3ÁÈŽŸ©?6¸Ä'ÂeÑÌ ûýhçaNÇõ¯YÜ €•Ñ8g*êiB*£CD@D@Dà»F f¶™[{cõM-ÿ¸îÄÊ”ÂÜ Ý…úÌ)×ÔÒþå×¥,JwÇÔq]—¯`˜INJÌô¦åù3ºóö)…£Mµ}ÞàÜ“ô´Ô‰ùÙüãSÏÞÃgfüô‘;ɲïhù­½A]]D@D@D 7ñj›aU›Ï÷Ÿ0w•šœ„¯L{gî/H™÷¶4ëµ0Z„ùxÏÑ1þôÑYÁå:Ææøê›‹‹rÏWÕ3_‰-¬;G7P5Ǥ‚þ™ð§{a§a•W Ç£ÍÀ¯CÒÿ" " "ð#¯j†yI¿øÆàÌLwÿǵ÷ççú6lÞ‹}Éræb]sk{š+e椱˜UæL½¶Í ÞÁïù ¼ñî¹£x/àÛdáŒñøÜ¬ÿho⨑XhæO/ê1™"E@D@D@n9¸T3ÿþ™•¡àþzÍ=Œ7±çÓ‘¬Os2½œÍ™Zhbò²3ž}tiSKË}ºSƒ+N?æW/Ûþ3´÷”œf²R†ç[K²zÜ‘.çïM‹4¥ýº ‹€ˆ€ˆ€ÜLCJÍ8À͘˜wû¤ü ùÁYK:D@D@D@†*¡¬f¦E¿ÛßPmoÝ—ˆ€ˆ€ =Cd†öÐkÝ‘ˆ€ˆ€ˆ@„n¥šaO¥òŠ:ûîHVÚJ\B¦©•Å|­˜ddV”ùgß³‰*EQš¹hk{•‰°J&" " "+·f¤‰\^ÿ`×ɳU—¯\u¥$1úûwÞÅ-¹Xûþ¸íÏ\8krÿvØñMé{Û®Mðfú6?X6‹ÅiþÇkMÊÏù³FQ™M;³£S3¤¢(MYD@D@D@"$pkÔ +ױР¢!73ýÃ%Ÿí;1© wjQpe^ö*Âdb¦OÛï“ ÛØcö§#eŠÆÜØ²½£+%ùÆMõVš)gÕ¢éÙ>ÏɳջKN³°5‘Ûqû)²²°ýÖ§h²Ißò8f(ŽQ£n¥̪ž" " "0T ÜxñßÌ;d»Gö†ÌÎð°JïWÎEOd¥'WК›é]µx:ÖšÐÒ÷8¹ —-™n›·÷ðé’Síjæo^þ›!~÷œÌô46I +™" " " ±%pkl3·MûŸ~òÀãg—W}u¤|Ïá3Ï=± k ^´c³3—]`P‰ux/Ö6p·è˜÷Ø2h…”¤Äcg‚{ /æL+lkï48XÞš‡—Íš[Ñmó(§¼².´´üŸIoþnü2è:ƒrš5%Åü>Ì'5õMüÃZÃnÛ¹™/½µ•¼ë?ÚÃ. °Óð×~¬\0óLmC3CiÔ„¤ìŸ*," " "+·FÍì:T†%fż©ü«´üêÕw:͈wÅB½ìqÍ?†–<®b¬~‘8·OÎ?V^‰dt–iRz®Ú€hnë ÒòP5r$CN=–fÒ›¿?~hñ¸Ñ™W*{<á„„jH £«Ë|dÌ3&|£P÷ÁnÛ\‹`èªÁƷƾ͂ɢ¿" " " ±%pkÔÌ鋵û–ZÚŠ‹F;]‰PÈð¤²m${*µ´u`\ÙqðTMCÓ’Ùw;wZ!O_;‹3Šý#¶ÈÎJOÛ~ ”À֝ޭ¼ôó'—õYZZj û:Ù˱˜RÊ.Ô0õÅ×'‰DÙäfyqfX }SRzѤ\>ï†EçÀ‰sVvD@D@D@n[ã7óðÒÛçN/ÜùMÙoÞú wZÆn—A+üðžyU—ýÚG¸Ó2LÃØ“ÛààÒÞÙem‹m%`‚Ã:ÿüöçl_qÇ謌>K³ò†ðó­ojÅר>»ŠYÜX\>Þ}4Û6J¸5ðB+«Ö®§„aðO¯­çÓÖ>mÒtvvªªªuïoÉÎò<¿æ^[|SUuí†-û2|é?ýÁ’0eš0xÔ5¶0›‰a{⺆ætOj¨”±§é1Œïí¥@ f{Þ¨KÃMW`WJp«mœ‚¿9y>3ÃÍ„ðƒ'Îý˦ÝÏ>zgqapVùÀdSC}ãš{çææø½^ORÒµ]»_Ú°¥©õòsk÷z½V$—s4ÊÀ+ D@D@D ~ Äl¤É(þ&%lik„‰{ôÍŠÖa–åïir\:êÒpÓ±¯pƒSNIÙ…C¥r|žÊº²¯sã¸hO!7‹a³+½ˆ€ˆ€ g1S3DWRbEc›u:”ÏÜ·ðbMýùê† kÂX?â)Vw×ÐÔ6&=èò¬CD@D@D ¿bö>æÂ˜82ÒRŽ]¨olníÍÁ¶¿Uüî¤gêS~n&ÿb[%X1…{j^ºÛÂUšˆ€ˆ€ y1sd5oâàûÞïÅ æð©k³~†<Áß ¬ 7èIÐ œ§Jnb¦f7räÈQ£F¹S“²Ò’>ß¾@ËpÃùýB Vƒô`y^¥€@ÌÞFÊ$&&&'%OÎõ0YzסSBÜ'(Á bpƒžMŸÄ”@D@D@b¦f"Á²œ””šš’›éÉq|wÛA6Br\O§vð¬ 7èÁ’ö4 ‹€ˆ€ˆ€„'05òÎÅ®œœär¥¦¥¹§åySF]}åÝ/%hzkÈÀJ°‚Ü CGúÌŽÏu*" " Ã@ôj†q‘¦Öo­+Ó­f’y-{=iéÞ©Ù)W»:^þ×Ïv,e“ÈáNÚvÿЀ dà%XA nÉÉÉ5áÔäk+éÙ PPD@D@Dà~ÏÐ6ã üe‘ÜÊKõ7JêöNJJt»SÓÓ½-­­mmíÅ—»Êê:ÞùìÀ¶ý'ïš;…}%Ó==ï‹d/g‡›Z—]ü|ßñÚÆ–ôÄ˲’ý¾ôŒ /Äà=‡0[ræŒóDÃOCø[¡[ ~«ëzén×Ñò – ¯ÛŠÄ555KCG‡¯«ëò•«WF&4ÔZkšßÙúõ·~‘–ÊÒsîîͱ­\Ã!ÐÒÚ΢2 ÍmŒ¹F^.t_ñ{]™™~fV¦bpƒžl/56#wì‘ ‹€ˆ€ˆ€8|ëõéø¬·Sìc³3qW=p¼ü®yÓ¬”˜pþÀ ä2Ûu.L‰ÉÁÛÜÒÚÑèÑÞÚQÛ¨2®Zù†t Ûów˜Q Ws’®z“G¸‚ˆ¼Œ/!e²ý™¾ŒtˆÁÍa˜-œ'äñwHÒ͉€ˆ€ˆÀ€ô[Í…LBï]wjjVš{ó®’¥sŠí¯aÞ¾{Ùü9–‘€# øÍàÒ‹c/¹L OI)ÁEen8Ç “¥PtabÌð“u’ŒÇãAÁ e²³³}>xl17¥–¶ Eh—0åë#>úV3°xôÞ÷›ß}¶gßÝ‹æ[hðžÁr`\ak 1 Ø<>?oB2Áƒ—1 †w0a^º¼%áŒy¦½½á§î™Ø7,4Ý™ôçË*:¬2LbÇ0ï ˜1&£x,jeçÎÓ ´-bE* " " Ü@B„¶–Šù‡ß¿V]Wÿ‹µ«-A;²£Z/ÍÍÍfð 6§Œ‰ð‘)?« “Æ0…¿H’2hA3éÝœòQ¨”yñµ7s²|¿|v-KÕ VºMè“@¤j†‚ÍÍ/½þfùÅŠ§\u×|VÜ¿1ÑáÂÐ’ñFÊ@ÇpïòJÍØ[ÂR3HT –-D FX¼¾2€ü|ï>¬2…ycžÿÑjoZš½4…E@D@D`˜臚šõï}¸mïþÑÙY÷.Y4{Z1«Ñ‚è#bø‹¸11üæˆ{»}49/FÓð×ÄXYXWæÀÑã[¶ïª¬©c€éé‡ï—UÆ‚£€ˆ€ˆ€ýS3&ÏÙ‹ïlÞZr²”yK~_‚†õ‚- FÄpzCÇHÐXt¬€mI=â"†VûEʰ® “±™Áôتãòä+cáS@D@D@nˆFÍ˜ÜøÈ:vò|U5/]^½ö½œn¯PT؃ ˆLÌÏÍ™9urZ·«uT%)“ˆ€ˆ€ }Ñ«™¡ÏFw(" " "nxòÆCmUGpšqѹˆ€ˆ€ˆ@|š‰¯öRmE@D@D@œ¤fœDt." " "_¤f⫽T['©'‹€ˆ€ˆ€Ä©™øj/ÕVD@D@DÀI@jÆIDç" " " ñE@j&¾ÚKµpšqѹˆ€ˆ€ˆ@|š‰¯öRmE@D@D@œ¤fœDt." " "_¤f⫽T['©'‹€ˆ€ˆ€Ä©™øj/ÕVD@D@DÀI@jÆIDç" " " ñE@j&¾ÚKµpšqѹˆ€ˆ€ˆ@|š‰¯öRmE@D@D@œ¤fœDt." " "_¤f⫽T['©'‹€ˆ€ˆ€Ä©™øj/ÕVD@D@DÀI@jÆIDç" " " ñE@j&¾ÚKµpšqѹˆ€ˆ€ˆ@|š‰¯öRmE@D@D@œ¤fœDt." " "_¤f⫽T['©'‹€ˆ€ˆ€Ä©™øj/ÕVD@D@DÀI@jÆIDç" " " ñE@j&¾ÚKµpšqѹˆ€ˆ€ˆ@|š‰¯öRmE@D@D@œ¤fœDt." " "_¤f⫽T['©'‹€ˆ€ˆ€ÄÿTñè¿"IEND®B`‚networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-architecture1.graffle0000666000175100017510000002463513245511164027652 0ustar zuulzuul00000000000000‹í}isâHÚíç©_ÁíO÷ޮ¹J©™ž~C`åã¥L8â†T„Y¼ôÄü÷û¤PJBH´íj»\]éŽ.c‘)¥¤\ÎÉg9¿ýÏÃØ/ݹ³¹Lþý .£_Jî¤ô½Éàß¿œŸí¿üÏï~û_»ÇÕ³«“½ÒÔ÷æ‹ÒÉyås½ZúåÓÎŽ=úîÎÎîÙnéäs½}V‚sìììýRúe¸XLÿ¹³s_vd©r/Ë‚ó“Y0ug‹ÇÏp²OP¡Ü_ôˬξÖ8Ú÷z‹ß?üã·‘ûø»¼œ×sðýŪØo;ò8|íÌfŽüðßæ‹´ÿw¸\9O¼Á,XNËÇð©6snn|×üm'*’*M«Œ´…X”0š*òÛŽ:uØ„êÌ ¯¿ë,ÜøâQa‚0û„ñ'ŒJØü'%ÿd¼ô+‚Ÿôé⓳lýCg±(5ùܹwfN¶Ò¾ç»gÓ«:ËE- ·:î½åØ,²Ê›,Ü;û³ßvÔgUoéõÝùç 7rûÙëo\%,}áͽ®¿Ñ¬«½v¶|}ì Üj°„kÎ6[³Ñ˜ÏÞdTtöÍÆ:ƒ‰»xNù94£=t¡Òz/Ú‰‹ÀH¸‰ú[Áû6?!ñ ±²þ‰Ø?™™ÿ¾W'r7_¸7Xº~Éž ä¿_ƒÒ‰ë;Áfõ;W¾ÈgÒ÷Ý'ßáQ°pŸþ<ŽgÞÀ›<¹ø ¼Ê tãÑü eáE}rÄE£Aµ³] ‹`|èÌ ªLjHǧ¾ñg‘¾ñ7 ¯êd:X]á ˜yLJv{“ðn»Ìrä\¨b/þè4ü«KŒÜ/¿S?lÖª´êí½¡=<>Gw/~oP…¿ûÇç¸3ì\ø6|_Ýãgg¤qÛ¹’ó¦ËðÌj@GëtoáݹŸGwVŸô݇¸ñÉtÒÓ½ª+™Ýÿºœ/r* WB3)©FDN½ŠÓI\0é‡ ¥×KUŒï$**KÍ“ï“Nÿô±„þû±ôÎi™L „‘% ÊÈÇcVÙQ1dbÄÙ×FCtöª«}ÎÉ#¨Ò¸œ—«ÎäΙç4zã|õÝôÉâg°þ¢W¹xLæùõ_˜~pŸ*±VdUfwæÜ§Ûÿü¾~±³qÅ,¹¯rÔçôÇÕ»Ÿ» ?±O®žý®ë¯QóÕ2™ª¬ºCØÖaRÿeµÖ&ä¤ÎûÛ–³Ê3iÎ#Ày ð—ã‰í{ƒÉ³j´§N®”ªxÁÿÂH?ÄÅw½ùpO»çø9÷ <~J½qéߥøóæÍEƒb.YEê$Él¹ÙÃ7Ævjpf” ̰ Ì#Ó0éÇ’àeD-jX\PL,‹ÉÑoÒ²‰)1Æ 2RæØ2ƒºÜ`ÃÙÑŸ?üãë‡?œGd XGTù£ÞCÂ26ˬ ­ ñÔcÏ\v}'­ÈœÇSwÒv&óOmwìu¿__8þÆDÑZ/»‰™ê%y³ÇÚT—tGš™ëÖÈÁZTÏØ: Óß~‚È·–xiÔÒ_4Cç¼àÏ÷ÿìýnv‚L©U±îz‘è- ²°te* ¿[L2žµ÷¢j j[3‘5 n˜šrkÏŠj[Æ€ÚÈ´2òkÏažq³gP˜u6èf_E¶'oöëÍ¥/ïÁVÜ ÐøaþðY§¿…ÚX_á rLMÂ6 ¢œ`“>ë 2A(§#Xb&Ö¤¼:A„àœcƉñoP=¥ÙÄ:}o9Ï<¬ð’býÒë3ÊÆôræ>,¶½Ó³ýÝÌ›sŽúc‰Š +  %ûŠM¯&¨syz&?wÇ=[©·êgþÑÉÕxê_Ñ–=ì4÷¢cgQ‘¨Fů·*ÍÇ‹“‹´ˆµ„ãwQ‘níxÏô¬ó¥AœË#¿:¶H×Ã_»„£¨HolÍ»µ‹ÇaЪ:ç£Ê>/tRÍ…šÈ¹´–W_Ün´÷ìËÑî(h_úƒþqTäÝukÖãpÖ>€Ú»]òp¤ë\Ý‘½ÿÕ¾jíuÆð_p<˜Þ^ý: _[hרUmY NµZxòa¨ZeØïOàÿ{Õ–ý#ä^>øÑ…¾À…Žú_N‡Wã_µ¥¶ÿxuÙ¼úR Îé)ïÖÎm(Ú‚¢­¨ÜɰCÃ^mÞŸwZÕó¡}ºÛBõtmIWwRéMCŠŒ[íaò«çüì¢vÁúûpýhÙN«òÕ®íÙŸ[•ª]v×®Œdµ¤3Œ«¹=¾¯umø= œ/öÀh^G…7;£ém{ÒhîM¦¢ |}(_@0mŽ¢"£ÛËötÚœŒn½öm§y;º]¶gæbtkÔÔYàáÏ KÏ;#4´/ºGg¯_ZW²)sU~vvx݆~Í$òwß®LíÚ¡jîUË^Ø{u»{_™Ûµ–}v_ñäMÃ]>Ä7}nC7ò3wUK&Ô‘;Ñ$Ház¶¸Á×€¢¼ðŸÞt€ '×½ 8ò+ŽØêù²  Š>üçú Ù¢ë_ß ë›ù½7Ÿ_ßô†Îlî.PéÀõïÜ…×sþõ_(Ù“« ý×õÌíί3×È]éÂïð AWÇaþcLÂÚÿ÷Ú}˜:@îûñyþuÝ›`Mœy=˜³¯{”pf]÷˜Çð‹ZDV½ž:³þõâþ…Ó…¿`‘‡_‚üÔ~Qzòca¸i´ª-8,bÀÒž×›MœÅræø×·=y`!÷gÜÉÓ[3ôáƒ|,]x0Ø(]÷nHéøâ¨T…'6 |ß]h»³;¯ç^çVz«Öw“O½¸uÿ}ß}ÍÁ„•M C¹¸$Ŭl0n&ã&’KÏ1H™„ nbbÀ”äÀ‘,baŠ8gÐyÇ<§›ÔPÙ´Lñ&ÆÈAEI%€Å“üJ³¢J60þA‰ÓÄê/&VÆQ1g0ò¨%žG¬¼È"†Å˜I‰ñLbEåŒqŽa,s¡‰•&VšXm#V•XÃbׯ =E¬èŲcO÷®Bê±ÿG§5½ëùWWàô*pöì±ß°O½4ìîñ³Óšÿµ_ócz6ÞtCê±?¸+yÌœ% œ°vñ3 wó.­Û²]W÷5uÓ•š¤TÎnÐtÆ'ÁQ€Žì_ôóí²J ×ÃÉC ”å>Q‘ÎÕêBûp¡³/èê²1ï\¬øWL÷ïÿ’Ö³=¸#E¶bb•p.hæ^ s~_·S¬)¼“/  g#ÛoŒí mÈÞ©"îÁéùÕex£Õ¹²ƒÚõžŽˆÔ¹â:=U¤^;LAÿèP|>Ø“ŸÕsY¤¾ês|–½+ûJ}½‡’ÏûŠM¡ZX«á\VFð–Ç@ÂëNš*Ƈ+Æù¸ííU.æè°vÔ¯ bJdW‘ÝшðŒƒ+ùÙµQLCî+C»ŠìÖ r'éGGRè ×UgiÌ+ãûjÚSóïëuxNWãÖq{Üi:êÑMÅ6ŒÉpƒ¼€Ûf{Úhú£Ûø¹Ly3ÝŽÛ·ÓæltûØžušËá ZÝþ?ºÏµÎQH Ï'ül¯8aT$M ³œP5WQÃ=a_ $¹Ù½VK¦ÚÒòºé»þýoMªÞ-©jËŰ+‰Çõ‡]x;]gþŽXÕ¼[îw_…O™€ë i7¢Ø"&³°$BŒ²ibƒB°aZ†!ùTƘ$„¶}OzÃ5½Ñv#Mo4½ù9éÍ~HoÎÆÖã&½ÉÚ–]ÚP¦§Ö® íÆ´ÝÚû²‚ðжÓé=ª³>ìMF!_qƒ†ø8â1ñ…šµUíþ±²!­LL‡1w±O¥Åh<0ªãéWc×9ÚmÍyd®ýkÀÓ§ ‰b"±%ìru¡Š¼.·ïuk!R hÝ E T‚fVö 1û‡Ò6¿£ðNö¡¨4–uvá hÀ P–°Î—!º€ë‡7::Û•ÇÈ0d³Œ9çB“ƒjT¤ªžËW;9t›úüŸ×HRˆEòõNôY…àPÍ®Å_¥>+{ ÁhòëÀÝuЖ•Ü´:ÜrÓ¡¥e·ZZ+‹[di‰ÉÀ¡µ´Øv:ÃU=*’€Ž´°TkÃi¯Z…ç4nŒvGÓ±"&ãi³Ms'’v6kòx£Ûv{ÚiŽc»‘Û¾m4§£ÛÛö-oÎG·¤=[YÅî’G÷0>kK¢†Ç+¢fWïC†É#jŠ¡EE¢V9±kuyÓSh¿•Ëšçõµ» ®4½y¿ôæZ¯èÍû³Mdëú/æ7…ìâ³7q¿™[bÿ¸§=íKÌV¸à:ýí ªgVãBƒGTŒ\òqÀÁ§pbD\Ò\è@øc #£L$Æ–),L€JR‹,Aå@Z10Y„&µ°an™fq}lâ2Àf0*Í…¹?–(pYyyB-Á‰…øÛ—MŸáb§á—£øo¾Ê[@x,1è{„Ç!‡¹ dbaYÏ‚ð &ãЫa’ ²×ƒðrÛ³YP°%òÍ´ŸÝÓ{ÌT¹qü¹»“)ó^:ò.3îñÆD­ÒŽç?¿U—^ƒ†Ï<‡ÀÅ_6ïÑçÎ{dÛ¼§Wš¬4O^8Ìo[80<=G0Ë  VXL0³˜MfL·Lü9Þ#Äbe!O` !° ÜØRŸó2,  Ö*„©¡½pè…ãÇ\8ø_ ˜_l‚aÌ, “ÁlX`“Å —MJˆ À0¹H‡î°,Æ-%‰(S&çDN#,ÄÏn‚yã‹xãKÞ2!_MÞ;«úÁ²Ÿ÷®Þ‡)!*„a`Aåñ¬é˜S“7c&2 † Dž5›ÒÓŒqÊ€EbdQ¡)Ú˜¢)ÅÆ”{~йä~|Þ¼8¸XÆ^^ó‹œÁZÆš3˜:KÍ¿+r‹)wEÎ`±{PmO9ƒ`qÂN¬ƒÖÎàq§ù²+*EÎ`ñùó"g°Ä 2)rSmÉà¤Áâ§ÝIŽ3˜2=àS¿ÈLíŸïzµ"g°Ø…«vXä ¶á+¶é ¶æ+–ï Ÿe¿Uä –¾ég0Õw¥©¡À,¶”$¦†¬3˜22 ܿ۔¼âê ¶2,‚žjX°€B-nãº')7†¶Úº¯AsïÜëgËÉÄõç¯ï­rH¸‘Üƈ yi9†¼È² \×nÑ2g”"ƒ3̆vOz#÷$K»'i÷$¨5¢þ)õƒráNkûƒÆø“œ7»´óG§V_ôÈÅcœÄ¯GEv;I{6~]¨T{6~=*’cÏÆ¯+÷ôT{6~=F±I{6~=¹PÆ^èÁ”„±Åp¤Âسñë*´"Æž_y’0ölüúzôÅ}ižŠ_W1ë©0ölüz6fý&ýů§cÖ£0ölüzT$Æž_Š¤ÃØ³ñë/ˆU€Ÿlüºº£0Œý1 cÏÆ¯Ç! cÏÆ¯ÇîII{6~=*’cÏzUÅÍMÂØ³^Uª×¥ÂسñëI<‰cÏ.IíÒ»‹²˜÷»Ÿæîìn{äzÒ{N|X-ešÓ\Œ²9ìþ*ó³>–°%ÊŒp ìЂ¥Ý2é“Ì+®¤ÍO&?âeä#mØŠ„WæV‹ F9PúçY iÙ›1— ž…„±dB06¥ÓâšËh.£¹ÌëÀ°Ñœú½1÷ûu'f!¡U`3D\šR˜=/D\š²{é™qiPä Ç* Ì ”Î¥ÒºP^ˆxNŠ®lˆ¸ä3Š„ˆË[P®ó!âÒÛòCÄ×ÂòCÄ7öõ7CÄ×öõóCÄ¥9 ‚’".ÍiCNˆ¸ü;*R".ÍqE~ˆ¸4¨"!âÒŽ£ÐV÷iˆ°}]ª%ÎÑîüßædIÈ‹½,1þ67K&Vn’ÒÇ’ †dÂf‚ à6ÁÀ6X}™¹Å?Ÿa« |ƒsÃÀ#Óä@Yàœ–të@Bšh¬ÍúÚOò-€Ûßzk?É¿ÜO2‡Îê¹üÍæròÏåæbåÆŽ`0Â!Ã|æ\nàpÈ‚e€CBõ\þæòÈÍ’a˜Æ±8±^âfÉÇ[Àâe/p³„k2 È<Ìçz6ÿ!fsñ\¯wü*^ï¦ý3¹$0ã‘^ïÄ,spØd†Åù[V&reTÆ1=3‘«”K1Íg&r•†ñ]‰^º›Nßh7]gYÕYVõÞ¸ÞÆÞøâèl·>8¯=àÎ#Ž©ª­¥N¡ªS¨~‡ª:Ùè»u}9üLJ'þrðÉ›¼ŠÃ_‘ÀN¹Ê(H)‚ô…r $ûPÍ-P$Ès¸Ï(È[ˆõ¹ó ¤AT¤H‘ ›§HðuSr «H‘ÈS$HI)H)õt  ¤A|ÓùŠRŠàåÚÏJ‘@¢mu»ÿ½ïúd¾p&=÷u’ëÀÛ*AÃĹÌâLî¶ÆÇ(² –Z$¦œ±4A5´}}hk¼ê¦ý™Gc_}5ö}ÿYq«÷K¹ }:Œ“Hî5[—}_"¡·ŽQ‡³88½ùÁá,ŽwP8œÅÁQ‘4ÎâàðMe®8åÍÅf,j„ƒ·äÖQ8XE’¦àp'/ †ÃY¼lWp8‹ƒ3Ø7O™+…}R_³M웆ÃYœ¹Pž2—zt)8œÅÁ*‡L ¿<‡LküCl@º G¾•ë6´^Ç—~ǘ¤o”ñ ‚” )à„M“0Xø)ðÓ(SÓ´8†•Ð&Å(5·>eŒ¡LP8µ„ŽIúnpŽq‹›Üà‰çÃ9"ÓQ¯Ðx œcÔ”žnÜ$: éÇ/åo‘¤çþožûÅ7jÿY«„ÎRÖÁ’ÙL9÷“2@&KÃ0ê-LÊ ”›a*£Xen8'L" ˜ü-Nõܯç~=÷ÿs¿ñvâ}[<øJú®…-j1#WQ´`'\'Yü;á–vòÐÝz£[ot¿?'~»K/sÉ'2¿»òw8U*ñÑÂ1W{.kÏeí¹¬m!°mß{‹Þð-¡-A¬Œ,¸àÇpXCÛ÷m±Þ¯ÕûµÚjhûþ -¯\‘#¿GL—I‘»n]¥ò3Ör}ľË+Å›¼\ñFßJñ&/×Gœýa¥x“—ë#*"S~åúHï´äúHò|LŠr}Äâ4iܼžë#+Ú““ë#vo^)ÞäåúP.R¼ÉËõgèX)ÞäåúØH²™ëc-H~®ø,+Å›¼\é›.Èõ¡ún¤x“—ë#vL^)ÞäåúP‘övÂØ(Ë´}&c†”´ “O`‚ËBX¦¨Á<žŸ>Bï͒뢓ë½Fr=€RÈÐë ~Ar=10§ÜxAr=ÃbeÊ-½Y¬µFÔ[E#Ozµ‹åéøá®Ï¾hd^Ö<-©E#5’~‡á}€Óöýàþ] ¯çeÇÊWÒuB0-DQ±Op&“6R™¬–:=Î…æ“ÇîùÐ<›:šKL®j4—˜<*RÍS*‚æ“+6QÍ%&“uäCs‰Éã”ùÐ\brÅ&  ¹Ää¿á,4_ßðÎ…æ“onx¯Aó´L4Oó€h.ÿV]ªšKL®R~@s‰É³)€æi_ŽUBëƒtBk•ÉZÉIh­2YGEòZ«LÖQ‘¼„ÖÊ¿ZÝtNBkå_­Ú’“ÐZe²Ö„ãÇÉ'Ò.}zPþ½å²î“›×JeM^6¡±1 „Móc‰¡(C5À3 ¬†’Ñ`³ŒÒ±†T Îãìû0š7޲/"ùê9á=æÝ|Õ›õ$’ß¼ë÷ñ1áw}ÿ,ìú­MÓ93žªÎþÒ4%Û'þmI6¬(Ÿ5,p“š2AeD1³Â -ƒ`Z¼?aQ€5‚0Ê,Ëä@º¡¾ÅÊBžÀB`Aù–$&* ÃÀ Sl“YH¦‹e$ôL#Ô0» Æu’Žï°€£ú¸B¶Ø³fyRy@?H €–õ¬Y^Ð8}¤ƒ$ÓY:~ˆ,üí’tèÅæ…‹ÍË3:ãÛŒi™ ÓrU‘³ PXk`–aš°Ž˜ÄÚ’Ð)G°”£,óûqB6LËØRŸó2% ÖXT(cëµF¯5z­ùQ3Bѷ˸m¯$ Ú¢†ÅÅD¦«“{%Ù£2£%)-ÔGB’jlaþ#ê(3˜Öeäʳt” VÓ|¦Ž2ªaßÙÜ,^¸dêÀ&ؤMÁÚüþ4ŽO;_Ni—6f2UT¤:ÂÃîxÒ¹Ä~wÒjvÇ8 vòðÊòKƒt¾4• Ãe:¼_YE Ò~‚ 2ê]™Ó dx¿Š}*dØð±Üdáý±7_A†÷…G¥Âûcce¾ ƒ ïO,×¹‚ kžšù‚ ™˜ýÍÝÙ;{W®²\Úÿ8£Ä²e””‡‹6vL$)â,4,#iJøë}a½ÅÆ ]ˆ¼V5üuD§ŠŸºóüå[ŒŠ ¼#ÉÏÑ®7¸þ«¼"Ú¾7˜äob ü]÷)ãutuɇr±è\råjW¯q¿пë—ZÆk]ÆËÎCvÝ©ÄP£¹WÙŸ?t|4ʦM9ª ¢*uèA˜:´ÎÔE®mû×6éÓ¦.”rm S…NW©BeŽPu!*4 NúîÍÂéÂJAù:T(næ­VìBÀâ"‹?_µ/a‘Ë[—²ÓÑ7„”—æ&3…›Æ–Á´Ø‹À¬¿—ØÊ`ÃL ¯W¼Ä`cÓB2Rƒ/0Ø0#Œ­Â aíön\À¶†Ée’©IûmNð[8Ÿd5dŠ V6 cH˜[„šøÍ'ß”*í…ø¢×Äï:iïS¶µ_ ~ÂØ1;Ì€ZjŠ7ô¼Êq—œúRÌBªXØDÅB%BH‰YD*Õp;y¸•,E¼æK²wž«7‘‰×ÊÓ›ìL/ ô&¤ÐD*ÍDžÞ„šH2'z éMTo¤ÐD²1>˜ÜGzPDéMÈÏJPBéMȯ•Þ„üìÚiµˆðÐMR[êM¤óËCJoB~Žô&ÖóYäêM¤Hd‘Þ„d‹Q‘"½ ç0[×›Ø cüª_m'Î਽{¾Ú»O˙įq¥j’•3 îkq aAJ<-˜<ºÎhzÛž5š ½­þî¶Õ “<•Vl8ʓ滳ÒQÐwßÕv–s¹K-h9“%8•gÍJ…Œ[è³EËœQŠ »ŒaèTo”êbêAû÷hÿ ×N¸î‡©Tn×Éy³K;tjõE\<öÇ ºGEv;ù:tkž9ù:t±%p=W‡N Ð)gŒº¿ /HéЭó‚\º´Ð\]–äèÐeírtèÞ/P:tëÆ¥/ö@¦vð:6_A÷I£¹7™Š”ƒÕP¾€`Ún/ÛÓis2ºõÚ·æ­zº·Ëðn0§Å XƼ3BÒ%)*rtÖPÂ#ó¬KRÜÜDx$ë’¤zÝ}Å“7 7û`œÛÐ|­®÷£¥yxgù‚»y¿û}x€¾”1¸€à0)¢¾éÃÃDð`hžW7í£]x^ 7VZ½±EœËS¿çU–±Ó÷ÊU\aŽÆS¶yc{ù¬cÅ4HL²ˆ­aÅ4HT6ƒ·¨°)¬˜‰ª-Jp H,ö Ab쟳ŽW ±Z´ìj|G§ÝÃËN¿ñ ‰ÓGé{üxœUh=IA"L¦u»& µé´S™ ÏÉ,¸óúîì]9ð òÚ^%ÍD&R„8Í‹LR‚‹²É憤è°ÊëÍÊ7Û¬Ôyiõf¥Þ¬Ô›•?éfå"Ù¬lWZñ¾Ýþ²ÓšÞõ'‡‹þäˆ:_N¿:µvèC®6ŸZ{'yúÂÒ‡<É¿š«/,«Å-__X‚Pµ)W /œrP/ÒN_¨@_8«Õ–£/œM]›£/,}È“xÅ\}á‹Ä‡½H_8µY¤/œ‰ÌÓN#é ×á"}ay˜ÉÕ–«þR /,ÿQyi ô…å6_œÅ5__XÆè%Y\WN ȉ!yÉ ¦#ãÍÊõÄtä¡™Ä4ú÷73ìªÈCEÞ2ˆéÈC­šü£mX–N|gò޶,?ÝÍC=çþûRèVYŠqp™åÇ`ØÀ:”ᥡ ôo–Ï–¯²Gõe”Ê50hˆK¥ÛæÐ¯ð³€%‰……ôMDL‹ëP†"”[¤¦’cX!1MRÆ,¨6¼º „kˆ6¼šB¡u"ýb€~rºY‹`>ùÙóÞØ[òÞÈfº¬^cAsÊ¥(‚Y:Ķ–üfét¢ì>ÌÚã/´{ß¹3ßy|GfË(ËŒŠ˜¢pËÎ4 )…½Þc@~,#L³‹3eÒAÁÒ«óÓ{áòlèåù=.Ïï2ÎËCW?8ò{“ÎôŠœúÄGNµrr1b'É*Ò ×;¹à²AsïôòÜ«µy±–lsgX^8^1òw†åŠ–ñZÈË.¢\Y v†åÚ\èo¬Íñ…òw†×ýTsw†åÚœÚ×ÍÛ–ksR$^¢³ksâV›ë*×fåšZ¢³ksì’°é*׿½Fj/U/ÑÏ]¢‹7 oæ”d󕕤G¡×sK÷ÞbX:¾8*…sóÌYxÁ¤ô¿w/NÿÏ;Xó¹„k¸%Ê,ôc0,j–I sV3–˜ŒÂ(b†¡süNM+@ç й´=ÿÍwPØ %*«~ÎÆÖ¨sµÒ•µ[µ5AYu–”®l((I¿nóÊŒ¤_÷“M–"é×4J*~í&Yi‹¤_¥æ«*R ý*5_ã %ýZ½‰¤_eb€óLO‰¥_ÃÏÁš¶«<¤¤_Ãσ´pëêH¾^I¿ÊÏñY”ô«ü:‘~MgÙ+~•n© ±H¿JÃt Só¥_¥a: é×G‰¬íà¾öÕn®Ç´¦,¬ö§"á5eáæ £«¶äl£)Œ®îHêÊJ…Ø+Aßuž°ó}²hªðA1üvj‡T|›Uœ¨ú&gTúCcd”‰D€Ø2…… ÃÅN¯0ŠËp fPd–¹‹1§À>žÂ¿RSl©ž¹:–œFˆ²”´D) 5¤õ§´þ”ÖŸúô§ÞP~J/5«$ªôå+‡õm+5pY†¼dÂ4.%¾°ÁÊ YŒ™ÂBVÖ•\ŠYYNü t“ Ä™ÜÍŠ”s‰€ÕÀ4LCÏüzæ×3ÿ0óS-sûÖS?y;–ÁÐ7®—…°LnÉô0³XÏ£T²!Emamá&ƒÒÒÙ,ÃDA9KHÓ†^-ôj¡W‹¿#O ¯"SK˜Q–*µªbdRÍþ,¦jPÁabIç­dÄP3šä­äÐkMøÊb:übÁ™N\©mÍ:\ÛŽuâJ¸R'®Ô‰+uâJm ׉+_DˆQ–û*œ‚ Óâ¤H€Š`RæP jqX왉?–€}øC@ƒ0 ªJgD;•j§RíTª‰Áwp*mkÑ;€³µc¤-ŧzL;•j§RíTª¡ôËħÆÓåbåS:eøšÞ" ÏV›yýu˜šŒãmXC M©«E²X šZ[¢]ã¯Á¬à±ùe£Á«ô>Áý\ÖLW)Ú\Õ˜8ÓùYðÄ:é§š Nœ›ZÜŠ¬Caá¦ëNÏ‚vÏIƒÎ´éievrÝYú”©ýé aC/†‘•¡kÖ¬ÕIƒÞèOŠ9ãÜÄHa{J8 œÀõÞ´˜-×Ïzá¹÷E%¶u1¸j°\lïdöÄ; 7çµmøÉ=oÖ ½É®7_ltJ,6zaX¾í ƒ7ª 2Ú¨ã‡ÍÞ› ¼I^»úÁ"§ar7;ÖÆÏîd°¶¯­.DÐê‡X SÎÆ…Ãs<³±‹û`ê=¹N¶ëÏ<˜K25‹\I’îrõ†ý¢³¯væÈï ƒÙSä§Ðz7€FË£h+™p·‚÷í|´ÅGÛ)!öOBÿ‰Pþû^œ(X}á½Î<KÞ°#ÿý:*¡ÿ0Zm~ȹïÛaðè;<Í‚Ç?“I¯Ó>Zü^e†qúYøcv0¼%¢ñ¤ŽûY/f³ÑàÈŸ@”Œ6¥“Sß„#¦Oß䊖fõVº,®°?šôþ ýúÝFotÝ…` ” çBeoöW{ÿ0¼¾Bvð¹6öG­•kõÝ®×=¹°j^†­Î6üÝ>¹@nsÿ2ôàûí]v~ŽoWǶwpµëÕoAfï[x¿]YßýOÁÍì%‰õç?õaE®÷þ Ö~}Ÿsös„,7ú±)'˜qñ¡ä¸ø?ß¹¨g\ð}0ÀßÝ{9ëuº¯úbÎGã×8ý[ú¶zøí“aøðý™œ·\FgV:Þ§[³Þ]ðÉ&Ãvð-é|ºÜÛúr¯šÁN浿Χ³œ¼ ŒJè`æ!¥Í°ÈiWö[}‰ †íh£ìµ´†ÉÄ¢Rjš~ŸúÛJö>”þͱl‡ ÛF¶+8¡øCIÎÌ\A\LgâÿYš ñÙ·CØísNC•zæåÔÚö‡wþ4§Ó+ç;ØÑO–<ƒåG¿ÊÙCºÎ/ßøB`ÚõÛ£{MbId!³3ñïõþÿWþ؉¾ØZ¹Àl2ê¯rí³þqñî§Ac4Í<¬è’™Ž/¯(+ËËyðm¶~îïídÞœ?óÕë’X¤[>½dϱñõðÒn\ËÏÍAËS"µƒóðøôz0¯IÍë6ª»ñ±óX$nQjåêÃåéeŸvjØÃñ»X¤YùŠÏø¼ñùûWÇáöÀÅÍúÚÄÌŽEZwÚ¬\>tGµmÿ¢_ÞcÑ…ŽàBª»ÐÒö¯ÜùõçCæÖw½«þNT¿ ;í“XäÂ>¾kV܇î8j½­wšøÛh]ꎼ½¯Þum·1€ÿF'ñíõ?:økÍÞá;ª/óýÎi§R‰NÞ-ƒ®Vî¶{Cøÿ^õeïØ®¾…ñ…>Ã…ŽÛŸÏº×ƒo¡êKeïáúªýpý¹<º g¬Y¹ð@´¢µXî¤Û ‡ÝV¥ >œ6jÛ]ïl§f—ÕÓõ¤¾¸¸“rkxØ @dP{¨wÓ׸}ÁÎ/+—´½×ïÈ3î4:^Kö-ñïäÀ¯w«'=o¯.ïìë¨S¾Ïˆt®ï=y(úìw’ϱH§UK¿¼ôóÍ}2^Ô¡³N9ù|^+g/té•»£ëè áøºwWÑCüT“Ãûãz£zPÛ»ú«¦^À5²+üìwàŸÓ­Ô½rèUî½v§¬^€·_óüZù«WÙõ>ÕÊÛ^ExM¯Ü—ÍÒÁ0Ø>œzƒûJÓƒßÝ‘ÿÙëðz÷°×PgaÕF|[Vw‡c± {W¾€Ñ¸ÚEú·Wõñ¸:ìßöê·êmÿv^ŸVgý[^Qg‡?!=môí®wÙ<>?üú¹v-»2U"ð³µÅ<'ÐM,·½òØ«©î^×¼™·{à5ïËS¯RóÎïË=yÓp—ß’›¾ð`…™»ª¤‹Ž¶Fä.4)Rø2™Ý /€¢zÑ?­qa†¿´F­‘/¿b6]ü17áoAìßþýå Ù¬~¹±¿ÜLï{Óé—›VןLƒ™]Ú»`ÖkùÿýlÉ]DÿûË$hcƾt&A0”šá<€ßÑ‚,Ž#¸˜<ŽŽZÿß/Á·±Ú};9ÏiM;°'Nz-X³¿´fÔýÒ¢Ã/ÐMdÓ/cÒþ2ûƸ ÿÂé¢_\È_Óèè6ð‹HÚÉ_”F"pÓö¢µüÅQ$‘çíM†þl>ñÃ/·-y`& 4ÁðÆoÍFû·ßäciƒA¼ô¥uƒK'—Ç¥mxb“Q“/¿ÕƒÉ]¯|ùÁ½ì-z7º~l%½ûÏãÞU=§PËøÔ/Ö1 u€d´=õ#üØ¿øíõ:Èìʉ­¨U*h®Rq:‚ƒÑ)0g–M\ÂP1ãTD/(ìc×Ì]V¬2`›Z¶ CKBa†ÔE]×â‚ÙÄ‘æ ›Ñâæˆ1˵CØg#¡=¨¨]câ @w«W=}dÕ¦SlÔy>8ñUÞ™#A ¢)2GOBæHbë§ÁqÁ±Cé÷ÀüßÇåDõ&“QyÁ^=Iäô :~ë!Ó䯧ÁVFÖ.Ýš™ì+s]í¶~/|z¯®zí=/znøi:\üyk׊U#ϬÎÅ6ßÇïë¬b”:–p(,! R ä,l]7D]Â8,g6âºUŒ»°éQæÚôC  å”q†X²¿ºUì ìaììay{ƒ|5yïl;ÍÛyïj3¬[¶M„àyî̤i…|ÏBU´;¶+lö=ËŒ±oû–±oEö­{¶ß¸ba{pQ½Ü¿œ+ûÖàrÚ¢þ®7½Œ3;»ìü¬~Ug©„wÍÁÞC™xöÆ‹,‘…JEªF~Ôºl³óv§MràÉ +À}e·\‘¦+çü‰Szºïî×¶:[íXäóŽ(Çf•ÈÔ¤Û˜’; §ëÅ…öàBçŸ/íë«Ãiã²ÜMÌq s—²sÉ0…]¸#iÔR}ÉØ¶ ›»5ûØ»¸?HŸn|'ŸA´ï…‡ý£þHD„‰Å…ÁþÙÅõUt£Û5|ííÇ–*eÒØé)ƒÕ…²)µ2"•#OÙ·*£ôóþ®f‰u´ÏB~VÝí^{×êë][}NβWƒC•è ‡þU¹oyàÎÛÚYäM«Ãe~1¨÷vË—Sû¨r¬ÞQY­ÊþÙÙÚêw¼F­<õö¯åoeó*}ïú¾Üõ¶m¯Ö)ßI3OCšˆÒÁÐèÒ¼SÜo7¡?•nç_ÿz·fyÅÅtaçÁØ~¬ÇC]ÀüKKªlcìÛ”òdp,¨åÀ#µ9ÇDPÀÉÇ6 dÀñãÁ±x8æ¯Vņ"4ÅåàÇÏÅÃ(ã-v 6xØàá5x¸{Øž…­`â?ÁZî-á`€c‘<¬p=j8X`rp°À±HVX»Pø{u¬°B²98XÝB,’‡ƒNPõ*VX9Psp°ÀKHv{YÈ¼ŠƒV"98X`Ugp°À±HV8q}®â`€•HntzŽE Þ`û;dî €ãK|™/ágû—ùW4 ´ WXÂu8¡B8Ü…Ý»ØH‘kŠÁçÈ–‘­ {PYØA‚!(‚soàÁm?%Ü6ÞÀ¿ÝÈŸíÜ3Ë÷K—oñ Ë7Ŷ©ƒ‘ –fÌž¸|3a9ÂF.À1XP³|ÿ€å;v%Ò¹é÷Vãµ®DbÖò÷¶–³§Fv W‰ì Yðs™ ‰K¢u…[Äq\fÛ.GDÂÂ¥slÁ2ƒmæ Ìá?ãõ‰ìpßÈxm²L’±J«ô¬Òíz“\ÚþÊ|"eýè—ËZZ‘žO¤¬Œ™´"=Ÿ(íËRZ‘žO¤¬Ò™´"=ŸHY¥3iEz>Qz¡¥´¢5&g•VTë¡¥éùD*Ö#“V¤ç)«t&­HÏ'R.“V¤çé™?ZZ‘žO¤RŒ2iEz>Q6Å(N+Òó‰4=­HÏ'J‚m–ÓŠô|"uG™´"=Ÿ(ɦéùDÉ2 8œ€s6šÏàY¿J@F.¦Å1|Å]…úšÁ´„i‘m@­µÔP»y 6«§ÛÙ^0tðøwÍa¸á¦éMËõ „áe™ï”U–yðü,sÏÛ†Áp} ã+ÔYãnWsÿËÛ¶õûÞ¬Õ}ShëÊ=p×q±K¦Â@ÛM‚¶È@[m ´5Ðv³êÊ×ø8l‘£ˆ9*¹k¼qVݨêÚŽìcïòIRºCMVÝ/’U×ûy²ê~nˆëµZÁtªø“^çRf ÌŠˆkS‰sí˜]•Á6!à„"V„‘%d`¬M8@lCÃúF4¬9v†‡õeˆZâ)ò=DUˆ¨3áaODÔ~¨ÑF½.ï´U¹œŸ ¾ÝµéIÆËÂg…›M2žIÆ3ðyøGœí…£ûÿ|·R†|M§!¬Ò² T.Üʺ͟ ¸á%[XÊ‘ä dðàd°kaxý.s¨´ë\dXfÜâ.v±£È…‡n÷[nSøàgÜîs ö6ØÛ`ïbìým|zuÎ+î×ë«ûª‚[•= b»-|Q½Æî¼ÑcýëѤ­°ÔÙI.4—˜œaûiêÃ!C!ù†<Ãà/Û0GEÁ¶k·D£!)gÂqm&sŠ|XR,¹Þs‡2‡ ›s¹àÇ9ÄaŒÀÎ!Ì‚o|³àoê‚¿j½Š fÅ/HixEâ…,ñ°+D™/Ò\9¢ÎÓT "°ÅqJæP×–*ÕÆ×Á˜› Âlfƒøi4ò*¼Â˜r Öä Ç•tå‘q"S1š&ð¹Äb”ÀRׯ\j¼ïoå}Æûn¼ïÆûn¼ï¿¦÷=L½ïW)7¾¨6Iã¯Få`Ö—íÁåƒd8–ÔÆ±ÈN#ŸáXº–•c±€áXò§‘À¹ Ç’Ú8)b8–ÔÆ E>ñ¤6N/”Ëpœ Îc8–^yåM-`8–ÔÆZŽcñ¤6N¹ôrŽ%µñ’S|•áX§0.`8–ÔÆY ã Ãq–+9‡áx9™2—áXþ‹1Ë(\­QÀp,£pÖŒ|†cIm¬îhäö:2~¸×ðXµÑßÖ‡‡ÕÝáX$g9¨wå «ýþíU}<®û·½úm£z›xßçõÉauÖ¿åQàô„Uw§¾-ƒb‘ãóCC0ÍrÙ%Ý]Ž!ЃÔ¨»/÷äMÃÍ~óö/<?¡ám~ž÷ "ÝMÛÍÓ`rL6+§sKSÆqÇå8e˜¸@•V?&â2G`ê %l#À¹p*‹ 9?FÚ¨²Ý(¿ô‰ùoóMΨ¿,Åíœá=»ñ¹Ý ê Înâ³°ES^åQLy™]’q—Ÿù·—rfeþI`ŸÂ¼Ýü¨Û -ú77óO¦ü)‘‚Ì?™ò§²Ü’Ì¿í›8ó¯3¼_°q(}(âÌ?I2ÿ¢Ï£¥Ô>yHeþEŸ;KüÑ!‘~­Èì¶“\¹$óO~½Lf§ÆKAæŸLùS!¬™2º4Qtò3ÿdtiõÌ¿8íî+_½êÑ2ZÇ ­/%–*M32±úTFð(­P&^ ½a@S‰ZíÒ»VüYpï?”ŽGí`ºYà5cÀwȇR&QÎ¥Eö{3õQhË8u)2öû7²ßcÛØï_Øs"$V£ –9Oö°dΙ ‹Œó `/ µí¸Ôæ†äÙ@ k5@nä§u‚a¢ð6„ÞÔBæS†¹ê¯ Ô.O/û4¢³–<Ö‰)û[ØêŒÏŸ±un\Ü졯šÉ¶5°UIÍú{ZÅBÅ—1ëëöü¤ȲY_·ç§¬"Kf}Ýž¯ú’1ë¯)Å¢Ìúº=_õ%GPöü"ÕC³çktºY_·çÇ"Y³¾nÏ×ÌêºY_·çë–wͬ¯Ûó•‰?cÖ×íùYlÖ×íùú…4³¾nÏ×΢›õu{¾zt³¾nÏOÒÑòÉ5´¼¬Y¡!ðHCPgɘõu{~b¿_6ëëö|MßÒÍúº=ÿù%jt{~rÓËf}cÏßp{þ±"òƒÉ—ßêÁä®×ÚÓþðc+éݫЄ L-SŠ@‡!L,iB2òŒþć͂H6ËqP~@[§¨Àb[†Ææ5š5rGè½jVÈhV¯À´H1…‰Sºˆ†1ñ9Üø«ü‹ê¦cFÿø.ô›cè½Òo6Ïk4”½k?[Á1Ô'.± ¬W?È éy¬N:‚ C}(!›[X"aä:ÂE´Éb:ÄŒŽÊ@oE ÌÂ4tˆ‹8ÇÌuÖ°k!Y §Dº tçeûî”ÄBÀÐ6…!àÒgp§Àje ÛA¢ Ób- CQj©RC£ò.hTVÃh ÏÖ›o6ß;èËöDâ€ÁlXè1‘ ˜4f«‰jbrŒÈšµ?'ˆ»1•®#„@‚0¾¦=cì a¶+nö³w˜½ãîx38¸(u,!‹XÉ,Ñ"Š,‡`,0'ôL&ôî‚®E™kƒ$މa,¶‘¿º'æ-|0ì|0y…|7y/m;ÍÛy/k3<*¶M„à6l© ƒZÇŸ´ 3™±M ul‡SÄ R¶‹dGFœQF(¨’Èv‰0>ãS1>•5iól¿qÅÂöà¢z¹9O¢½.§iÚ<TÚ¼–øTÒj¼Ù °$L(­Æ{~ŠÄ)=Ýw÷k[‡­v,òyG”‹‚Âô*TAa©7dX¦ú’ÉÄу’§»œ“¯…%Ì^Ë9ùzP˜2£kÕx³AaiéÝ£¢ °ÕÒ»+AaË¥wsƒÂ’³,çäëAaúM…©±›ÉÉ×] ‰Ãdµâ“ S¾¦Nðó„OÉ+.¾  ÿÆöcý .è§ÐE—qÁ¿´¤’Á6Æ¿@ã´xèî]ðå·óùp„Ó×Iü@®©Ü¶´#› ‰y×5`^Ûu9úC-gQŠ…)ûfaJÜ„)™0%© ¤6µ† ÖÔ‚ZCPkB“ A­q.¯-‹â<ß¹üšLT,œÃÒ³Lµ)(š‰Ã×vm†”¯ ,¢ ¨‚>ÊçÈF¶ã0˜„¶Ù.hò´RŒMÖ»cwELp» »ÏqG0€î.w›Ðg¸#àš²¢±±cüÃïÃ?ÌÞ®jëÚL=PV~&X€æÆ±ôcÇb –&JÊ]P  ŽÇBêJdÈÏ©Oÿ Çy"õ ¥šóls{&ñ:oä–6´$†–ÄØÃŒ=ì .æÙñùÎAç¢ò 5P’p§8GjsÃ9b8G~çˆaçØXÑÑ'\: ç½á«x–‰kÉ@râ""—À–þ=JA@†¶Ë‘+ý†JAa C)øÓR ºÆ—m°»Áî»ÿÊ”‚ù”YJÁ¹¡4ðÞP JAC¹±™”‚ÃW£Le @c\F)Ú9üC Þº…@¶p¨K„Ë™)FµA‰lÄþ¥<Q-¢4{A‘ÍŸQûU*í0Öñsj¿b,!3±16Z‡Ñ:ŒÖ±Æc6‡ÇÂÙׯç“Jã)ü³ ~’ºOaô?IÝ—Bç\¿§•ï,bð“Ô}ú…rü¤ö¡º[Àà—SE*Ëà'o!©h•Ïà'©ûb‘"¿LÔjƒß×Uо,ƒ_†¢/ÁO£è+bð“Ô}êé0øIê¾ä¦óü$ußó«%)?‰¶ÕY ×ݦïƒátæ[Áëd¡ÁÛ²° Ç u•ÖÖä±]Îa;!Eå‡2ž!ˆ¶¯m_·ÐûÌ3Ø×`_ƒ}7?{ìpaq¿’Fè³n¶°[­]µC‰„b\W88AD)Îâ`ÝøÃá,N,¨)ÎâàXD‡ÃY¼b_e²NRÃ.k9ÅM#¼&Má`EZ¬Áá,N_@‡³8x Ø.àpg°o“µ†}¿i_ÓUì«Ãá,Î\(ÉZ=: gq°ÊµÒàðós­t8lpð»0@3_¾•/¿yÐû™¡WþaII¿,)‰lqIwŒSØø 跈㸠ÁN háb”šÛž !*œ‹¸Â$%ý08G™ËƱÅÓá–<€Ä†WÈŸç(qd¤s°ÉHzIÄ~»Œ$³ö¿xí)S¾»`>’ü‡.ÅÄuäÚ-€L®Àœs”ê5lÅÊ *7ED¦±B{ɦ„“‹¿ËˆYûÍÚoÖþ÷°ö£·cº_äÁõ3`ìºÈ%.å¹õ7 ,áKlÅ æ6c uK85AÆÐm ÝÆÐ½yAíz“\ÚþJ4ïpÑ/—Y>Ú’‰:Ÿ-íË~š òtŠxÐKWw9$CçAK/ô¹ˆMY±ã€ð<4¿¸€-‰l9ìñ )£{t£ù[ä¨ÀtžŠÜ5Þ¸R\–ë#‘”¦RܯQ)ΰb¼ˆëµZÁtZÚ†‡6…¯J>¡G;!Ä-IÛçPÊ9`_·|ad á:Ü&Qo›˜BqoD®GC®÷äz¥l.`Ôcô r=ˆ sÄãÏ ×ã.u Pîc±AÔQ¯CÔÝÓVår~6øvצ~‚Ö"$½Êš'!´V-5OBè,þ̰æI‹óYó–ë”岿5®—.”Çš—g ΰæåIdYóä-¨T¿Ö< ¡\žÏš·dæÍgÍ[Á«¬yKX8Ÿ5OBè”/—5OBh—ç°æÉ¿c‘"Ö< ¡cq>kž„ÐJ¤€5OBèXÄ éÍOŽîß¼@ÙSÙ1dé+:!¨@®Mìâ˜à “6üÂÇÁì~4éÃãþòÛ‘?ô;Qœé†¸†‡ÁàÁðã4˜Ü“Š•¥tIYIÏ2²"'„ 8gŠ”%dK_Âß Û›­ìÐ…ÈkÑ"\FtJü,˜FÚÁßnbT Ê[h$ù$íÆÀõ7^å9Ñ {a¾£ ÂfÖñ:¾¾bݾœ5®˜Šµ;¨°°½ß¾k 榎×r//5z~9ÕÝòÞô[#´ûYîP-zPQˆ*îÐýˆ;ô€ª;ŠcÛö2±m2¨M]H‹m‹¸BÇ ®PIª.d¸BupÒnf~vêÈÔ¡"@q3ÅöbÇN!lÞ²øþ®}›\Þ¾”]ŽžŸ×²Â~d¢ÀžFé϶pØP‡Û‚ÁëÏqØpḶLÕÀüÊ£ä*DmdbÀ6&ì)ë‰ôßæd¿EëÉjü)ÂÔr0¥¶p0èÄA¶ž¼ˆ+í¹ø½&~7¬½17¾qü‚Éc^Dy*ß”ÞÐê•Ošø,”Õ,d ¯‘–±PLZ5‹¸ŒÅvdNî®U–b½æsj;Ï-8‘IØÊ+8!µ3å¼((8!+Mh<y'd¥‰”Ú8-8q(â‚Û7²ÒDjï ïã‚ ¢ NÈϪ¢„*8!¿V'äçÀÓËED‡nÒÖ²à„N,©‚òs\pb™Ð"·à„¦DœÚb,RTpBfB%$fË'v¢$¿í¯žŸPøŽ”íž-l÷z=“ä5.Êšd뙌î+I&a'žLL]£?¾­O«3cVß8³:¦R¶K m8&J ƒIéxÔ6Ê€M@ÿEÌ¡Ž ð<‘ˤ•Z+C¬­qYª2צJ.±%Äæv)¨ÏÜp=¼×å†ëÁÄ÷˜ø×M¸F\ŠÜAÁu|Qm’Æ_ÊÁ¬…/ÚƒËÝc‘F~!º¥ÈœüBt±¥p=·¬@§‚1 Ñý$zVˆnY/È-D§Wš+(D—Õ r ÑeKÚå¢Û½@¢[v.}ö:’Û¡×ðØº«»Ã±Ð¬ºòŒÆÕ~ÿöª>W‡ýÛ^ý¶Q½UO÷vþþ-H-& eL}[†$Å"Ç燪òÈ4’”t7­<’ IR£î¾Ü“7 7ûÍÛ¿ð`ü„¦¼Þ{ãyØ0‚‡ÑÝ´ÝÜÄbÃæ/s~eEÅðnBx^Ýà˜Âój¸±\k \ì_…­^yž}/BÅVÜ=|Œ™7‰I”Ï2VÔAbJ#¶„u¨0l+®)æ°¢U_VJÁ¥ ±8.<‰I|Î2V\€ÄíJ§æm'wtÖ<ºj´ŸPBâìAÆ?œdKHÔUB"bÓº]ª‘Tj3¼S[Ïédt×k“ à!Ô¶–l•$“™;9#y™‰B…`Âr¨—*ºÃc¬|3c¥!¦5ÆJc¬4ÆÊ_ÔX9K•õr-±Ûí͵ñ]{x4k‰ÿùì«_©G1äÊøTÛ=Í+0,cÈSÖÜòYÑò KªŒr†µõ¢Ãú… g‹µåÎr׿–1äi¾bnáË4†½¨À°f‰,*0œÉÌ+0¬%#®¤„‹ ËÌÃT$·À°ü[—‚ÃòEL[P`Xšù×üÃ2G/¥q]1ÆA Zæa,’M@Ô3cår¢žy¨fc&QGÿIàþ*Ůʺ[¹  È9Q®*rAm“9Q®*r!É‹rUöÝt³½/Š\P[vN”«Š\XW-Ž\P $'ÊUE.({wÍÛ.Š\P¯1SL\ˆEN’ò`«‘ Iä­f»”‘ Ýí¼:`&ráñ;x±=ñfJp–ά${­ tß›uK'—Ç¥héžø³ÞhXúß­`8›ø!líÒd4ŸA‹ÿ³T%Â…-߸K\î:¤Z@PK r‡ÀÅ”sC-ð'ÆZÀP jãþsƒ íhEhU~ÐùÀí7®uh½Ze©­:‹V‡6*@—Š]Ä—ŠÝKm2E¥b%ÓŒùþý -Î ·T¬¬«D JÅʱ‰}H•ŠÝ¾‰KÅJ‹LÆK‰$¥b£Ï£¥Z°ò*}îè…^‡Dúõ¢T¬üœœE•Š•_§¥buR¾‚R± *gyA©X‰S 8-òc'Á­z©Ø¢áî+_½êÑr V)DK•ˆÂŽ /U"®.Ìmª/9V7enSw$ëÐÊŠ²×ƪ´Ñ´*¿çÇ ˜zU XÉß®:"#/«W•D ÀØd”Èðids Kˆ\G¸ST# ³Ø‚SPNl—s$˜Œ©& }Øô ø•8bMóÌÕ‘Ôi„°d,€@„ÀT³M¹*S®Ê”«zåªì·+WeöšEémñü­ƒ¾lë Y´Tl;°ŽË’`ˆS‹Êx3GpYøÊ]SV— jÉ•ŸÃLw˜°•欸Ô.°8Üáfé7K¿YúßÁÒO…)‹ûÖK¿ó†j{á^a#K×a.(V÷izØâ².ì-Ì¡ -C–  Â@ƒp…ôm˜ÝÂìf·ø)ò*um1å–¬Ô$±*²Î%¥ÈP ”Á`eщ.)æ„‘ ªRD— †­_¹Ô$¿Qò83L—ÆÛl’Ç÷Ø0]¦KÃti˜. Ó¥ñ…¦Ëg©@˜[Ò°Â0ƈ;.ÃE«0ÂsA".ƒÍž:èC Ôo6Ç‚hœVú'sLX© +5a¥F1øa¥5ìÎZûp¶z‚´eµª5a¥&¬Ô„•(ý¼jUƒñ|¶ˆ*¾2|ÕM¤ÑÙ*“^{j‹q"Q‡=ú¤]-viXb¹Â³¾ ÄV㯣IÁ)ÿËJ‡|@£û©l©7)² .Z ýñô|ôÈ6úS‰|P§~'Ð6·"÷P$\ ‚ñù¨ÞòuЩûž~'ÿ!˜è§ÔìÓ+z„£fVF•Xrg-N:jõ¿#rìr™”¢þ”Pž&p –GÓl2_>ëe/¸/’X7Äણùlý ó†½? r^ÛJŠŸ”hõ&­ÑQo¸Ó›ÎV%+£0’¯0%£4Ä•&¶e¯´ £nï;½a^¿Ú£YNǤ5{$ÝŸ‚agÉ®­.„íÅv… ,9+ŽÎñÄÎÎîGãÞ£Ûd‡þɤkI¦eQ,I:\N†œ3k&ÀÙè>Ãá²vVøÊ2±è?ÑW–xy‚Ùyo¶4c˜?¼ó§™µºönç®Z®íÐåSÖ„ËÞ´× ƒËQ¯] ²w ñz%?*‹[À–=/¹´Wæ=ؼ֬w—ÜbþZ–6–óÁ¶‚'¶½˜»ÃYo²ô:Ó—»µ‡ ³é³8™Ã a>™@¯£×’Ü{Á–´°*LôU*µ™ØJ NqD)|8ãùM6‡¥%]Œ’ïÏæa0)€Ôôµ¯Ó¡«¿ìÌO§ɹ¡¸Éÿ;÷›zñx9 :ú¼KÄGJˆå8T9\»ÇJ‰kÉP  á"C‹g%/бã‚8ÇžœÀB WÚ‚»˜P†rdc4\ú€¬ÒQ®íRKֺɕ,3€.ùÃ0‘½ KAñL°ZZŒ’‹hó!™"Ñò iL§'rÁïk“+3F[£ñ,¾ý }0w$\~Ô¡ûapº³W,0à¥âºLî[Õäks?ìÍô&‹[·ÓeX—²¥Î¦Ode툺>…—W‘›^ì&°>®ÇÝÙ Œ—Œy\“Þ°ÎÛAÙoõ;iþŒMŒzk5ÝôÇ£a´q@¿Шø¹‡þt¶=œ¢n!´Ò/Xa·ŽæìÊ£×wÀHFÔ šÇß54nŸ ÇâNÃ…s¹Â (®¾ÿå.,ÉWöž$¿~ôéI O+O’?=Α·¨ Pß‘=XjXtÍ ÎöžpK°5Ϧ{Sÿ.ð¦{¡?ÛƒÁ~2–ÍóZ’•–óiàìâ÷RGÑPZ+r<šå/1jñúck†üçoÿ(®Ã+‹$networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-services.svg0000666000175100017510000006575313245511164026131 0ustar zuulzuul00000000000000 Produced by OmniGraffle 7.4.1 2017-08-04 09:17:59 +0000 Canvas 1 Layer 1 Controller Node Service layout Networking Management Compute Nodes OVS Data Plane ovs-vswitchd KVM Hypervisor Compute Networking ML2 Plug-in Database Node OVN Northbound Service ovn-northd OVS Local Database ovsdb-server OVN Controller Service ovn-controller OVN Metadata Agent OVN Northbound Database ovsdb-server OVN Southbound Database ovsdb-server OVN Mechanism Driver Layer-3 Service Plug-in Gateway Nodes OVS Data Plane ovs-vswitchd OVS Local Database ovsdb-server OVN Controller Service ovn-controller networking-ovn-4.0.0/doc/source/admin/refarch/figures/ovn-hw.png0000666000175100017510000022611313245511164024676 0ustar zuulzuul00000000000000‰PNG  IHDRÇÍ¥:sRGB®Îé pHYsˆˆÈ¥†ÕiTXtXML:com.adobe.xmp 5 2 1 °ã2Ý@IDATxì`ÅÕÇg÷îÔ%ËÝ– ®¸a0L BB  à¡$¶drAÅ”$š[Hh)ò‘˜^˜^lŒmÜ{o²eõ»ÛýþoO{>•“î¤;éîôû´mvvæ·³oæÍ¼™QŠŽH€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H€H á h Ÿ&€H )¸Ýï8w¥~œ-‰™3cFis‰ºý2+¼Þ”´ÔÔêo»­ª9¿±ºv£û±,ÍSÚÝ“í<0ÿ®»Æê9ñî ³gw•¸´ônâ%¾Œ @ÇÐ;æ±|* Ô'°ÝóÁ Þrï~_¹o[ý+¬~RüV”VÞÝøjûœñy÷¿V™U¹çíóÄŽ}JÝ»Ùѱ±àÓI€H€â€3Þ#Èø‘ @,Lu—Œ4=Æ2M3WÌ/š5.Ï`˜$@$Ðٰ碳½q¦—H€HÀ"àô:`l:L¥X2O @”P F $ƒ! ˆ-—Û'0t  ¶  n+AÞO$W¦Îºw¬2½¿DkôiÊT½`ò²\¤éÚûs“ÿ¢†v„§çýÐÐÔLeþΩR¶xMÏД}‚Ó¥M~Ü¿DüÝ쾯Ç#á 43[7µw±ÿ7´xÛÁXÛ©ſƹÐ>o~ÑÌì‹S ‡(¥=*ÇN§ãæÇÝ3ÖÚצ¹‹Ž5½ª×Í/Ê/–ón÷ó);|«¯5Mó'ÊÔ†ãT¥ÒÔ"ùéÇ‹sÝ36Š?ÛM-(zÅ4ÕNÜí´Y%·#-7 …_Î/.øžíçgî’áq'â ÎõÅïcSéo÷sžô˜Û}†×öךm¸¼Áçð9ÓÔÔÂ…7|Öô‚ÂɆÒî‹ HËöuHî-÷•ཛྷ²ÍóÁQà¹\ë.åžë.øÔö'Ûi…7›J»@Wúcs‹f¾|íF÷ý}¼ÞÚ?ãµm›iSgÝç1kO?{ pÔ4õé¼Â¼G:  Ö`ÏEkÉñ> ¸# •uez>FÅüTdQ)7ßEÕ±?*ßw>ó¿Ó Š Ž´¡iCá÷G®EÅ;Wk(Óxƒè_ý¹ûÁ\Ûo¤Ûˆx›æFI+ŸÛšz Q¨ÎSÊØk_¿Á]<Î[á[Œ÷2¼{âü›HCÒr¾Ï£>œ’_t»íW¶HÛ ç‚Ï˾î­I—k¦fžj]3Ôà:åMÓÀx,òÉ@ëÿ ´š{.ZŽ7’ Ĉ€SZ › Û0µ¡¨J6ò"½¨ f(Mÿù‚¢ü?ئÜ]|¾ò/£ãT÷¼óÝÓ*ík²E¥óרlÒÚ .Ã\ªuËÙŽs”‘¿£"Û½O)gÏà>™÷àÁšxüUÁa8•óeªý-*ϧŸGeøô@\Mí \›søºv–\s8/˹iwÏž„ç~-è«tgêisÝwì–óÒ›±Í»êM¤íTϸ§òå|ë‹™YJÓîw(ÇóÎts³\›ê¾w€éõS]kJ”‹;C–²¡éê/§˜Æ²¦œ ÚßMŸåclÐiî’ t0*üøx LÍ3¯¸ ¨ÁÙz‡S ƉFÊ…x²Zá=Õ×¢Vz!*΃ÌÍ{ú¡ÂïJv(‡ü7›¸fõpº€ì•Þ C4ÚËÊgÞ¨™¦˜FýIS5“å¿…#¶>W>CέP†q–\s˜æK² vSï.>ׯ—10wäóTç_o¸ø{æÝ3ã½…3—„ƒé©ê*O÷zjbìEàš½ƒø[ Ðô³ÏEº„÷œÂ‚w1z+8 —ñbÎTgòõmQ*uGêßíçJ ³öu}¥}.xëLÓ¾¬­À[5Õ ÃêÝ öÀ}  !Ðt X‡D…% ¶@oÂo•§z+LlîC¥ó(T¬WÂ|éq]s\† ø—!C7ýmàÁ×q/9†¬ >oï£wEÌ|ê;½Ç;PjªQY?].ø0ÞB*Íg·E)†ù–œÓ 5Y¶P \h‡ú8G,’cq2€ŠÓ Óg¼Ã+¡X m^“®0‹ú™å©é?›¬ çòy{×b¬4ÒÐÄq[‡û>Dd¬±A·‡µ)ïº8Z½0g²z+vxמ ^¹òF°)"`Å_7õýMEÆgÖ u(yƨQ@Ý‚óiÎn-xáe  (`ÏE 2 Ž'0­ äÃôýuì½è¦8oNaþ»Án´ÜÌp‰-*Þ¡` 7 ãTø·ƒàûLMë/ÍæÁ®ÎÔè](7çÞ˜_t„GYc,>³Æ8`6)LwºjÅé–™“§FÌy^¨×ânzž@Eû((D \úŒÇfÎÜg‡?íњ¶KÏql¬=àÓ¡œ®«š}…P­åípêÏú0-.ȉrq·aþÁäºölð£ 4l‚Ÿq^Ó”^•/‚¯É¾¯SÃÁϦ+®¸¢EåÂ0|ÃÅ? @l °ç"¶|: @»ðR÷¨§æÍú_=ÅÂ=/SF6©Vûófxj” (g6:‰ `¡ðjÚµ¨ùöÇ)é…¨sšÌøÔËôÕ †RÚa“¨_üþ÷éð„u(43Í™s{°b!7c€û¤º@ÂÚXÏ•¶ÍòìõÀD¬±C/ɽPºþy£{öÐÆW[:Ó:ÞsÝùË ¸}%J”Ì%c[´CÊÑ#°6ˆõdÍ´økuc/ÆF3LÿþuLü—ë¦ô…šÕë|©«ÈÞðÍÜ' ›•‹°QÑ# @<Àô´~3%S›O·ÕJÏ™5)%ø|‹ûºs>*½lž0Ý]\¯b:íîb 7et#笛Vë]ü\.jºþNÀ“é7½Ö5—3åûZEf¦­ð^Q:j=eõfÃs)˜S]gû {«iö¬Yßo¸ºµ5=¯©îDX'L5dCØaÖyl o˜„Y½†é} |e†¯Ú³qÙñМŽ? èZ×ÊÀxû¼lo-.îÙ±üƒÀ5Çoík0U³z8ÀñìçŸX~'ïƒÈoµ[§!];°SÙ2õmàóVTˆ?C/Äq øñü5¸6LÆ9dä¦hûqeêo×Vˆ¢‰ÚÚ’GÝwn·¯Y3På-Deû2Tsÿ„56`õð=è²8Õ4¼—¡’½ R_èã§Í*¾Óã>mßj›Ú+û·5»Ê~„ç¾ÝûÁ;ˆ×ßÁ¥ᜄãÓPA7±¶Çuá˜5|F[x;ÒÕߌj%ãbü½¦z²aø2£zUæÀÏMJ«ù‹>Œ±'˱PàðªJk‘Äàûü¼Â™Ù÷B›øÌdÌËÉo,]ý&Þß[è±: ã;°8Ÿ&ïÛ2¥²ý÷Rw­ß®Š÷Ï€i³ŠþŠç½Šõ/ž²¯sK$@$9ö\DÎŒw Ä!Y” MÏÓQ‰¬@…ôG+ñL' eC3].}’æÐÐ[ í‚²p«2}—†“„¾®ü›a»T‚–ð\¬=+w?‹ð;`úôz$BŽá@<,Ó(T†?~ð¶Ûªìg‰©Æ„,‘clÍ•š©ßs®÷PÙ‚±÷Bxa}çîç#¼@ù„ öŸí0›ÛÊzŽ,ÇIÖ}¦y ¸<*\p¿ ÿ Àä¹÷ä¿Ö\¡®µ…÷Ü‚‚mP’Þñ‡­­Æì`ï5õœF@¹+îç,¨·áãÅ[ `\¥ãÆÃL6”)‹*þ÷˜êrÙSùåv£?H3çÊ ø»ÿø€î DJå $›KJzÖT›“tÌŸ”îLûðA÷mÙ†d\CeiõøGÖòGÜ·”…›j1éªÒN„À¬Íp¦}f¸a„늅6ý7ÅÇ>m¨Ó¡VôV3—Y•àºn(¸oâ¡Ë¢}á†)þd€¹Ï¡&¢RŸ Õf]p‹$á4ôÛZÞè)xÀ0ÌÛ—;°:vÀ´©aør|ÃìÙ]}UÆx«uû sµËÕuiÝ ù¦¼cÑASßåÅ#ºw”À5sÝ366é±îäÏJJº{kUo‡ê²¹¹p› ƒ×H€H€ü¨\0' ´+ÿ±ÝóÁFt*uÏrfö‹¥²Ö® ãÃH€H€°:, ´Ûx 3«âöªžâ[`ÞÔ½(s©X´x>‚H€Ú‘•‹v„ÍG‘ @g&Pv æ¹2U|6‹T˜Cr)UÒ™y0í$@$Œ¨\$ã[ešH€H  h¦¬’.+e¯IÑœ÷?Vx×–xŒ&ãD$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@@@ë€gFí‘7ÌžÝÕ,÷]`èjŒ2U_üº™š™ÐiŠDIDŸµ©4µ¿º¡–iYŽ—ç̘QšDI ;)”{a£¢GHh”{ ýú:uä²"~£»x¼×c)Í<×45± •Ží¦2÷wê·ÉÄ“@Ð”Ö y¦R5Íô)S{ÍéÒ wç/Iâd’F¹@Áè4:»Üë4/:ÉšPÊÅÍ?œZ³ûÐLÓœŠÊÅjMÓr9\ÿ}Ô}çö${/L @7¹ïËóø<—@ü Ã!æ§öʾõ‘[n© qKBŸ¦ÜKè×ÇÈ“@Tt6¹h ¤Ã$ŒrqsIIÏšJßÐj9‘¾=ÏyÊ<·û o‡‘ãƒI€:”€ÛýŽs»wÑ4ȄߢuïËÔ ýÒGfÎÜÓ¡‘ŠòÃ)÷¢ ”Á‘@‚è r/Á_£ ¡\ø[VÊ#.uÉ\wÁ§|{$@$ ¦»‹&ù<ê¿èÍ\ŸÚ«ËÉÒƒA¹ÇüM$Š@²Ê½PéåùÄ" 'Btkv•=,=T,ám1Ž$AdƒÈ‘íûôØ=r/vl2 $:d•{‰þ^?¸W.d#* SÄŠ=̶$@MÙ 2Bd…ÈŒ¦ü$Ò9ʽDz[Œ+ t d“{C‘O¸W.dV(¼-c,b€a’ $‘"+¬™ä çGPi†b¡a´¶ û©¦iLÛîÙöoû~„›DÖ©æÓ8ŸŽž…Ç0ü-Øî-š¢tÝÈØ¯@!` Ä~ üæ@±®)ß_dk‡ E&¬8Øþ›ÛÞ0kö1šé{i€b¡Â`Þ­Øï3±‡§ϰïŰë||÷£Òð}øI·¯5³]ˆÞ Á!ï`YÍ]¡ü…›)¬}^ó]\ëB3M¬>o^v¸i ¾‡û$ÐÄRæéFÌSæx4†@0û Sù^½^òÓ÷4¶dZœM• eâY»¥“?@†Zòî˜1-À5KqÀ¹øÍ•ƹÏö'òÈ Çþ£™Yr¿fäR9dh­\ÆÖ‹¿eØQ’ –\4•qw¨Þ蟻ÌÅÃ?CœNÇO‡B²òQ0‡z ã(¶â£ÌÍ{¯›á/þ<ð6é—2£¾óîþ›¡ÌEö¢,Y£dšyežaT›ÞZ\Ü[Ë•1 Ÿkø¿–{!èÏŸvwñYÁáHžŠ;Ðò6Ps:ÆBØKïƒx'Í»'ÿ-Í•ð#÷ëàï¶÷÷Áñ70Óº\iÎ[‡àðî{MQЪ†C—«k^?gÁ@”„(¨ïŸÁ÷ E…¾ök‡¦ÎH×3¾¾ÖhšÊ¿ŸKHš¡~)&O ýD’¦Í8˜Biº®ÿ8Ï5<Ûkÿz=‘¦©aœxLÉJ d*úÚO²ºevÓ4ý1‹% ßf£§¢›Ci–¢€o9ÅðyN fo9 ¿éç‘]gšºY®£Aç7ÁþZÚÇs®@8Ï‹?<÷SÈÕ‰sŠî\ Ùàt¥¼†@_Düs5Ou“ñªôU܉X÷„¿ZÍ¡Ÿ‰ø Ñ]úÄf-Î;P W“Zäø]ç§¶Pw9†ëiš”)ŸÊ9ÛÝ0«äT°¹DŽ¥,@z†Ã&k0©ÄùUUêj¹†F®ïÈ"w žyüMÖuu#ü}…%ϺÆ?$@qO€ÊEÜ¿¢Ð4^Èg¿C7;Z·ZpFÍñäYâ ɜݷíw»¯¨Mså<iîïõ0Œ€²`‡–æÌþ³ìÏsÏ\Bw±ì£º‰=ñUóŠfþs~á]_«VÆAžÙ”C«ÿ$9=`­ÇSúmžâ¢ÕKLPvš]k÷ªWøÃßß`¶uf_ùßCî_͹…3>CÁÓ%´(z=VZÏDé1ü­}šùμÂüg…=¶O£ñ^p˜‘¦)ø^î“@2h™§U@F<#ÓµêÊø§ÍÓáÔ­ž]WïœÀ9T†³íë²…¬(›W8óëÛ.šù2¾í'å<äÇ‘2å´ì·ÅEK6è*õ6ée0ÐYð³Y÷6^ÆðÏZÅá£ù÷ä¿#qùŠþ þø›'ɘ1Ÿ¦‡´YåCwüf®{ÆFËÄI× ‚Ó‰ÞŽãå|ÊQxd çãǰ²š Y½².¼K­­®¾²¶¦:æPë`6:ßÐÔž³Ç8=ðõzxý÷ñ/ @< ro%Ì8ùT´"Õ9Ó`ïÚ[˜ÿhþÆ÷¸ÙýpŽ2ŒAöµ¬.iŸÙû²¶ ‘Url(m6'…eðÚ ¨œ×.†±ƒ¬j^aÁú€×VÄ!poƒ`h+KØþEøÓòC×{aÀ«Ï”ébNSú²ÀA¸;éÚ )ñŒ+1¶ãäz·E’SZôàL-ÀÞ¬ï±5i²ÂàèÚEæ)Um£44‡eæ)Ç>¯3R¥jRSQ'éÖA¡@Ǯߦèõz÷[¦?öµH·Ñ” óŠ~µ•|L;k:<¦÷÷⢙ƒêÎÕëÀä Ö1d¡¾Gí€ù“¿Ü|ôž1vÈ;§Ï È49‡òe lE^Ãìé)‘Óòƒb2QÎCû²ät†#óYxþÀ ¦e0U>óŸo.]½jzAÉE–_þ!ˆ{T.âþ…Žà|÷´JTÞ7Öù¸Rfáö=}Öìó1h{“üjŒC§jº¶Õ¾~ð@ÍÑö¾Ìœ„.ç¡rŒ–±€ë:.ØþZ¹­ .l[‡îY;£é·”]i¿Ôt׸†¿tWækÁ·#9V!ø\Kû²B<ÊÒËŸ©Ýì?Âô”ʽ(ˆG‡ýqöqkÒdßË- $;v‘ymƒX¯Gò4 g3»en×Mã „jJðc ÇÄ|´YuÙåB#Œ¶Pç ò?6øáh—²ÊlÇŸ‡BR—ÍLÏtmCk”%Ó 48ß\±v˜í×ëÐ2­îÜÿV[ÛPF˱CsY=Ò›ŒŠSœ.ýh˜¤@X¾-Š â8ħŒG٥ˎ·$@G€ÊEDZΓ5ó^ Â}fá˜/Š‚c–’!è²@ö!œ÷æéÃÞpé®ÏĆVÎaû ½¬| ZެÂÎTŽEÖõVüÑ5³^ÙTÑŒƒÛ¡æÏ!ÏÁª“ÄìJ~wž…Uʉß/šŠCkÎ¥õÊþ=@n@K[½à¥vÄþg›gÜPpŸµ&ˆØ"£Àö·Þáb{¦©5x t88’y†¦×k|0õîâ …ÑÍîûú£/ø2?/m­˜Y¡Â¼ÚlfÛƒ¢e±ã:™9êW÷Ý—^òkƒS¢9ÔòºûéŸ)rzÞ=w.3MÌX½¿44¯e6S¨·Qv­6<æ%ó‹ò‹1£ÖYˆãVX¦9p·º÷Èàp¹O$Ÿ¨\Äç{ ;V°Eý#DýÇr ˆkËVˉÛQÈ­Å1 ŒŽPêÇbü¨ûÎíØ÷OÏŠîf̾±S¨b„ù˜ÿÚÇ Šfþ7ì‡Ãcžê^n÷`ÐÆÃ(0ooîþhÇ Í y”¤ï£PZŠ´¿ˆ…§ÞÁñ58Û%œqÍÅ×¾&S êJo”¶HÒãМEò>ðnzyMÏr°úÊkøÞƒfóãî½Òtø‰Ü#Ä!ÐÑ2/˜ª§\ !Ç0}æB|Û‹k<žµøÖ­–~|Ó%rŸáÔ—ŠŸº0Af}izP‘7ëÆ¼®Œ½u‡ÇÁߦ»gжlèë˜ùGÄÇ?Î!øÙZ ÎcÆ*3ÝðË0îU•æ&o(GfÊv^á]_Aþ/”}Ã0o7·ìYs Ì³ é±,9/®¯žoͼNŸÇüPÒƒYöV£Ðºç®Öʯ¤hÚJƒBõkŒ·xòüA„û =à[z©»ÖË> @| rßï§ÅØÉô³˜ñét¿ƒ÷ˆðFÐWn„0^‡Âè¬÷ðªPžó”»à·W}–?SöûU/`ö¨z‚}Os[k] Í|á!8+¬Sšó/×¢I›Ì¼„çïBÚÇ"â—Š4þUe¹êµžµ¯–®cPú¿õ ý…›ž9…3¾Àwâ Ó,™åË'3Zç‡Ùži ~.÷I t´Ìk–‘©­Æ÷ü[ü†×É!¯˜÷Ì-,xRî›ï¾k3dÀt©¸ãûG]ÁüÇœ åE¹^Ï9‡¿Í«im—`pyn´eƒÕ¢9n­÷\ˆ)(°Ÿ -=èêcÍ +=J+ÅlO——)™®ŒŸ@¦½f…Þ¤%ÅÔtoC]ÀòLfþ=„÷O„—-éÁv¨ÈmÍ¡]<Ç¿T¼ætI¹Cd7:Ôh1šŒgþ\ä:ü-UNíR+œº0¹!ˆ_"ÜâÖM)(´ZD`ƒyqÜF2Ž"&sÀïRkF^­¿Ó©Öž>j躆k_ØÑµ4R{Faa¤Œ WÆ2™9ʾ֚­Ì²Ó¹­{š7³"ÜÞ‚hÆAºãõ{G ;=בåX>gÆ Ë¸5iií=á¦Çb¥¶Ã¢X[¥ç#Ôóâ!M¡âÏç]n$züÛ3ot¤Ì N§µ¦Ži” ±`%¦P=ÊúÆ};2ÝVÉ8‘`¿²/cvxbf»Å<©áuû•zí¦Ù³»ù #­÷ä]n÷^¹Öž²A&Ñ ß0,¦ºíìQCׄ.SàOy{õU×JO¹††Ûéîz™žš‘ºæÚuƸÁk› OühªæHÓkf˜*uËÜÂ;ÖÝk&±À!ëKÌqD€ÊE½ F…H í½Môø·ý &^ •‹ÄKcœè(7ý &Wüi•\ï“©!    #àì°'óÁ$@$@$0ÅìAQŒÅL÷$Ar˜ h*mÂÇ›I€H€:;yÅïüèH€H Ó YT§Ï@$@$@$@$@Ñ!@å": tzT.:}     ˆ*ÑáÈPH€H€H€H€H Ó rÑé³ @tP¹ˆG†B$@$@$@$@ž•‹NŸ€H€H€H€H€¢C€ÊEt82    èô¨\tú,@$@$@$@$@$T.¢Ã‘¡ @§'@å¢Óg     èpF'˜ø å†Ù³»‡<º61ì«LÕM)S‹ÏØF#Vš©4µ!íÐ s™žíziÎŒ¥Ñ¹a­h¾ÂÖ,÷]`èjŒišyÉŸoÛmÃ|œlÇÌ;1•{ÚunwWg­Žo“eJ´¿æÝ˜æÝh¿.†GÍHFåB›>ëžñ¦©—ø*|ß25]ï’•æíš•¡§¥¹ÍÒH‚‹ÕÕ_iy¥qðPµÓ¬ðÓ ŠÞÐ4cæÜ»— yf“ة٠»Xò½Ñ]<Þð)É·çàEé]2;O¾5Û6æû¸¿y'frO£´k•?ÑåJ/2½êlC÷É2%ZeŠbÞYÞ{¹Å&/¤R.~âv§¹<އ}†v}÷.¾Ó&wŒÜWåd¦¹’÷6J™(P޲ŠjµbÃÇ{_®>{ßÁÊs§äþÑãòÝò¤Û]ÝèŽ0NmRÔùÞüðé5»ÊöxÌ)8ß à¨³ ¼µ$ÝaÞ ¼ØXä}òå—g :æ!Mw\Û•eJTËæÝ˜æÝ@àÜ!Ž ×&BS  ”E³.nŽö“;îèš’³Psè.>e¬ãø1G*]ë䵤è\6 S}²l½Z¸èkŸ2Œ/«kÊ.yòv!ôp{1ȶ™WÑV¾7—”ôôT/*];†ù¶>èÖ²@nÔ`œ…æÐ/¬µy§.D)8_}u^ÞCÿ­9ãùmfÝF¶V@Ì»‡y6Ük-ßpåFÃçñ˜bA tkçüøÇ¢X¤¥¥Œ¿á²Ó'ŽBÅ¢.·ˆ‚%<„KjjÊá$¼p9Í‹l[øêÚÂWZî<•¾—RS]™oƒn ÛÆ¡%׿æßgòŽÈEçÈI“ºôí7ä…´´Ô£ùmÖgݶV@Ì»õy6Âå«;RJ˜o›Dòd˜lCÞŸ$tæÈßdyÇ’y9³wÿÁ³øm†Ï8 ¶v`Ì»6‰¶ð Tz%ØhªbÛ'F/tç)g_ØGw:OǬPN±S¤k™€p²x›ðÃMÍF¶-£lÒG8|¯ºþÖžJÓÎb¾maȓᰠys’\`ÞiÝ‹ #ïˆL1fBWjÊÉü6Ãç[+0æÝð™û —oð=Ü'Ž&¨Ê…ÄÛÕoذ MS风ìÛÑêù2=¯p~¿à|@¶m|›-ñMÉ;ˆù¶u[`Ûº@ç.y§õ/«™¼cÉ<„œ1lÂÄóLSÓX¦Dƹ¶v@Ì»6‰VlÃàÛŠPy ÄŽ@p¥2vO‰~Èb›âLqî’•êËÉH‹þ’8D¬û¡„›ðŽø½H¶²ŽÇ¶=DIªç¼>CU×z¿Z¯Þõö8h‰¯†Õ}1ßbµpµk™*=XÑcÇ`.üe:ÅX¸ØÆâ‘ñ¦#QóN<@l&ïX2qÌHMO?*Q¾Íx’Ͱµ_}RäÝŽ’aðµ9sKqA )“˜¸ˆX ‘°  ˆÍËÍÎìpiÛîjÁ ‹ÔO/9I ìÓôÀ÷¯±Ö›Hu¹ÔÑÃP§Œ¢zÇE=+–ï?p¶• Oóa[U]«Þüt¥Z¾q‡ê›¥NKù’) ÅÉX)˜ üÄ ;qŽºë Íû|°SKMqz‡dlÌš-þV¿‰#ò·Œ­8ùè!VXÛ1i1¾ƒ¾Z«Î9a”u.²Á#â&Hëm<zQÒÿqK×l³zººæd¨¿¿þY@‘•÷,c{d ص;1]\^±ƒåþ Ýóo~ŽüæP§Ã 2Ô8µàûc°oçàmÌ{Â7îØ‡qNÔ0ȹÔÔ«ajå†íªO.V™a+]Ÿ€—´¬K¯øi†’Ÿ 2Ðfj˹vÉ»ÍÉ3‘iõ§×µˆø|¦eRN¾Œ3Øo p‡â…@"*ÂÎVÊ´¶ñÂ2™½êÈ~~S©^g!NÆYH+‹´È_tʸ@….pSGìøùùY~~‡°ý¶ù#öÁÀì£-ÛÿÇÿñ?«2"f8¶ëSÇRZÎÄàlé ’ÅsþùžÚ±ï€íÍÚÊù^]³­ýýej1ZSeÛ°7¤ÞMÑ>Í7ÚOj6¼,äGQ&Ê«ªUVzš:ˆVþqLÓ¾sªÕ0ó±0c˜ÕÕe`C"‡›“gC!Å¢@Ê{’ i Óæpò%eàaÎÜ#p$¢ra·€ØÛpÒÙ¡~VÕÍ"J†üÄ»Û8Q.lŽöV¢¼/Ç1wç`@ñ‹˜aFf“ 1­èƒÁÉÁNÀ埠R"•–an#?鉙¡^ýhY°WõÆ'~³4™ÕHZ.ÂÀo¹¿œýP{ÛîQÚ¿—ó%178¯‡5kŠ˜I]Š)|Ÿ{ã3õÄ>PéPÀ.:­ñ´¡¢Ðýø‚`‚ö¥ZøÞR‹£Tr¤wÈ6ãpņùߘÖV\:̨dÒóOmÇèÍÔÞÆè1ql‡¦Qú‹ ¶q—Izw÷÷Ê5ù¼‚eš ß^(øÒÛ‡Îæ)&&âäØ>gˆÅùפðo~Žw…AÉ}0ùÁà°• 2ÐæØpv:[ã±9yvæq#Ôºm»1ëÝÇV>•ÞTQ,Âuq&Û•k¸Œè‚ $¢rÿ¸ØéPåÊÉÜö<Ü¡ütöóÒ«sË•gZ6Ù²ÎB(E +f~ºá{§[ë-Èš ¹°ûw*f^'öÉÁ6ÊÖÉNö§áàu©ôɸŸ‹¡„yÀ,x‹Ì¨%«,¯´µPÜGÃϨÁ´¬-Îi* 3C»›®˜|Èý$#pç5çFœ¢ük/htOK²²Ñ Iv" J÷Õžh“¤¥¥ºê¥PÆCÝwówëkx@è'Ò«PòL‰;~|®5 H¯dƼ`N¾¤ &Æ}hž@ý/¬y¿¼J1'\ñmîa Œ6ç—×üB-f'½;²Ð`KN””püµ¯“@g'ÐP©h ÊÀ¦©5'ÏD1k‹£ l =ÞÛ™Ø]Â)ÍL+ @ P¹ˆTI$@$@$@$@‘•‹ÎøÖ™f    ˆ޹h%TYPª´¬ÊZ¥8xêΜ·gБÁ±ÁÇdªÆÇZÄ̾GÖA¶r,Ô:³«Æ<øÁN?6ålÆÁ×m¶â?ÅéÄBIþ 5l¿ÂÖfÞT˜ÉpNlÝ]Šie3U:è¢#hhÜób- YqZd¡=a€„c ÃÃÑŽk"„çOÐjÜ:ÊŒ”ƒ%¶|kxÝ>/~(÷(÷$Б@< rá[eàÅE˰íZKàKayô°þê²3'¢Pp¨'^Xd-g+SØ8îHu˜ÁX€ªB=ðÌkÖ*Æ—œv´åe ÖxÓþ S]·0•}ogÚÊ4¦%z¥^’¥ˆY¸.<Þš>P.ÊŠÛ…O¼„ÊŠ¡®¿ôk½9¿ w=ø×7eW}çŒ oÙ¿÷ÉWU%ÖuiS¯8û9•”î­ÏVb5ìuÖ¢P¢X é×S]yî±Ö¢y›±ˆä³3Ùhå䤄ÁDE@[äÞû‹×`ªèåÖ,oƒúúý뫟ªo°ÀbK3#E=!qàGËÖ[Ó;GKfË“òaìÐ~Ó²øÞs˜‚[Ö)À,\vã å^h‚Š„°@IDAT¹'«ÂWCöOÆ´æt$@íK s7•·‚õûX OÖW„µ~xÞñjÒ¨AÖâSUZyø8ôêHo(l÷üñEuÔà<µeç~k¼±CóÔ³¯|¢ö¨°L­N>zˆúÖñGY æ=µð#+|aîÀt¶²ÙôïžsÁJõÌ+«m»X¦0² "[;çµEî%vÊÛ'öÃôŠçþÕì×b¡Õ¯Vo…ráµzá@Î8nˆúlùµ+MÄ"–ÁŽr¯¾Ü“ET×mÛc™(Ë¡×}ûdµqǾ&åÞÚ­þÅõD¶}²l£š~Ùi(wö¨—Ñ8#ï@Ê›ËÏš¨cEoiÈ %÷¤ì_ï,VUè-éŠõ—~pîñj@ÿ; ~WÜ'Î@€=¼ei5YáØ¶–ÛÏÅjŸ­lìAëÇJ¬h+Âæ£¥ë¬'äd¥Eð¤ÎëUi… f¯´Bɼ䣎̳€Hex=” ©ðŽÁ¯ Ý7wÔƒ5 O7«r[VQm)ûv«w=†`BY$ê•—©{¼¨ž\ø¡Õk &(=s³`†7ÔJöw ˆeg¤AÙøZÉ\ð²ØÞÉ0ÙûtÅFT^ü­¡ôd|ŽcQ˜‡ÑK½·XÌÿ4uÃe§« X]øÏWYJÉÇK×[ŠÅ){È=Ñ[´7Ð òò|YyµúéÅ'«‰P\^Á»%‘.1 PîÅþ½}ŠJ­È½ß_ŠF“íjÂð#&ŒK hÈ8¾cF¡†C Y±~»Ué Žå^}¹7iô Õy]³3­ÅC…U(¹g wUziÁ"aì^aánNèk%åÍ4<¦£ý™—>²Þ[ð{â> t‰ÞsÑ®F2QœôF4ç¤åâÏÿýÀòÒ-ñÒÒ+=iý3׿æ®Å,bÊ$æ;UÕ%-vc ìÅYœ²âF è­ºƒ«ß4js=Ûä.P⺢¢½¦QÒRuî £Õg¨,wkšahK¤VE33=Eýò‡ßR+6lÇo‡Z³ ±gߌÞ¯Ò½âzwÏA¥¿JmÁŒ N‹·ûZ,E¹Xºf+*.þÖÐaP*d<‹81M[±~‡e‚¶jÓN%fÒ3!á‹?[©–ÖTÛ-ƒRžæR⿦nþªÍ»¬–?ÛO¶M³BÀqD”óN8im«Ü (–ñc#2ïh¦òZ8¬Zëgõ–]Ö„UYfŸ#õ %ã-¤ñ g×l5|`oµߣ|çã¡€Ø.Žä^h~Qλ-ɽTL"=½ºe[+s‡’{c†ö·0Ê8—ãG¶öoºb²Z¶n‡Z´dÚs ÜRäB(¹·yg©ÕèÒ×êQßn ®/«<¨vì-Sy=»XaFéOh¾Qzƒ!hHDåÂþ¸LŸ×[^UU+"«ùÚ~4H! iõ“¨õ袻L{P ÈvBáø.ìeÅÉÊ©·^u–rh:àžµG éñ8TQcù“?‡ÐÂ.®k— kÛ^„›ðÃó<ƒžÝîlíg7j Uñ•ã½õ¥Õª¾kÿ!Õã%(dÅÍû¿÷­­ü‘¢†6®Ðz´…ïžÒr´$ùnh§æø¦q0šùv#z Ö¡gà„1GZÉ›Oüg‘ú&dŸ:®^Š}uoÛáðϦ%ùQþù¤ù®ÎuAEÆvϾü ”‘R5yâ0kP¼´®Š³gD³ýÙ¹Hf¿³«þPJÄ4@Ü…Ò/Š&ͱ Ä'ùv¬í¼¦¶Ê½®ÙéÖcÊ‚äžô,J…¹½]yÇ®ò _ÓëõTDóÛ 7}?:ÿxË,J¾Ÿâ?¿¬ÞÆ F“ÎC–y¡„s÷Üÿ‚…#X¹ -÷š`k •˜äÝhË=1+wòë{JIŽ:±”WÖ¨˜0D\(¹WZæo8ÌÆ€{‘{ò†±ÌŒÃå¿@þ4÷ ¡òVˆ DT.$jkkö•–WÚ,p>–;bb"3óü =Çb0÷JTb¥Õö*Ìöd;•µ(" L…Úk妖ÙO6Zäa†™&µ/f•jO'Ü„_¨gvÛ†qÉCU¡7º v®âvì+³z)ÆñÉØ‚iWe&1?Ðç°1û/b€½ô`ؽ ÃŽõqs|¡Ôí>P^5¥XÌÃ^G·½Ì u2ì² h¢ˆi“5¢.±û1[Ù´|öÅ8”Ï—o„Ù@Wkà· N<º®õN¼j¶¦€ým{JaØ]{d§!N>81 Ð ÿ[bÙ…‹ÉšTBåy¢HÈÌhÒ²c>¤çHÌ6¢åšc­gÄk8ÑÎ;ᦳ-r¯_/ÿ»—£<^¯•7·ï=hõœ…ûühùk)ïÔTW?Ñz^¤áÈ÷#e„ŒWgOZqôœLå÷C˜ʠú‡;ZîµÄ6Úy·%¹' 'Um2€[¦çnIîÙïj/z*D&ÊŒÁhœz ¦jŒÿJîõïk5VÀjátŒYsè}ËÕIÇÑr-ñÖs Dƒ@¢*"üÍòƒ6”•×èÒ Ö^•È3 ³úøëuVÅL^‚´¶sÙ«0=è“/}¬Þ†íº8™åãGß®k0/á&ü‹¥ÿŸck73-ÕÚî‚R±½EâN›0<0HNì-Y ånk=åB*²ÒzßQã-Zâ[UQ±æ`y#Zùv$L(ÄÌIZ3e𢠄Ö¿—:kÒQ3T(ƒÿŒ±w]süŽV…¢0¼ÅÉ”—2 ´)'…£(Ó¿Á@o1£'ŠƒL¯,fgŸ³Éƒ$¦uK‹¨Ë1KÚK°?~èooY-}ã1ðÀ()-°m* ÉtÎŒvÞ N[䞘†^zúÑ »Ìšv[ž) è·q®=]3yÇ–æÁýû¶¶w™ÒAVzšª…væ‡2…t.ZÁO0,à­×äÛ[ºv«õíÙ:Rî5ÃÖŽ^ÔónKrO½‹i¨ À¾ùûg„”{kêÊ;¢Gbà¶ ®—‰Xº@¡ËëÝf¶2±HmH¹gúþß«­éæe†Bã&cÛ¢áÂàÇ0 ˆ¿mDÔ‚‹n@S Jˆ Šf]²Ì+š_¼ƒºèêk_¸tò]Ö‘hO'Óî/+·„OzZä]Ÿ2žÀ‹0dÀr{;™ÿû…ÿ-6^|úO—nß¼Aì\d´í¡ºxt8ÛöæíçµÄw䨉CNûöw>E¾•BH&kjú]ié³>q¢|‰’ f{-ä_C$-ªÁ‹{‰i`9 [™ÙK&9xà™×Õôf\†YUl'æh­ù>ì0n›a˜·3„ÜhT܇ˆ¿%÷b™wÂÒ¹'C.¬<‡ñ8ÑÌáÄ[ü„È;•¸$öY=ðëߣwßáßrãœX|›?i]¶R¦Èwó¼JîIžEÍ^Ì6¹'æQÒ[„€ Gî•B®fÃÄ*ZŠ…<¼¾VüBÈ@ܹCíI :ju{ÆØÿ,1)ñ¡b| ¢¬lé»_®Æ8Si|j?'•7Y ¯µ…¤˜t„b!œ„—p~ &Âß¶;€ÎV"‘¨.¾+¿þrMeÕ§±È·Òƒ×”b!û蛽;¶¾¿pÑRC¦š£ M@ø'áµì³OV ?ü„£ð®d ­u‘ðýäí×–Ø»ûµDη2ÛÌX#¦Q±žù' ¶­}m‰r_àÛL†¼ÓžÐ[È;vÅ×*O¯êÕK¿\¿kë¦OùÛl/¾-°•<+.©ò.åžÿ¥ò/ 4G • I­\È4B¯<÷Ì+Ëm~â¿úDØÑ5& \„p^ ?ág+öMdk“ˆ`Û¾/ÿý©ßW•W¬g¾mtl›(ñ¯¾Mæð^f˜y'À¡Š\¬|íùgŸ«8xp¿ÍМÃdk`̼k#i~!ßæãUhg2Ð*nÝ1§ù‰Ü—ï½ó·&")Š‘ŒNuy='Ƭê?xؘ¯ÖïÌÎHMÑúašM™Š®³;±×üøëõê/¯}j”8°õÕçŸ}°lÿ~YÖZ¦¡•1R˜Š‚a·2a×Z7„l…D ®-|=55Ž=Û·|ÝoÐЉKÖíèÂ|[v+ÙªäFý‡ÄáQ ñ·äóNó/®y'Pž äL›êܺaíš#† Î2¥>ëV°µ`ÞµI4³m-ßäF3Oä%ˆ>DU.¤",šƒü$ ®ÊC‡ÌU_/þªO¿Ý¶”V÷_¼z³áÄ72ÛCjÝ 9ð×iœÌž!ëoüõµO}‹WmÕvmÙüÉžùã\(;A ù•á'¦Qbol;²µI4³ßC«—-ù¬OÿA}¶ì¯ÐÙó­ o[ë%z!ÛLüë}›Ì;?ÐVæz\ªK~˜ö×\¹tñÒ^ýän-­Éëìßf+ÙÚ/©cæ]Ëámù&|£ÊaÜKþy)3%R!–бLw'sÁ:k*+õÿ<5ÿ©¡cÆ}xÌ©g^„U3‡ýß;K´œ¬T£kV†–žž"-'IídOYlGæj×4Ó<¸oßÚ/Þûŵ˖®AÂKñ¥BlÇ„[Cŧ,G¶6‰ÛXð­*/w¾ð繎8z»Nž|Ù e#:[¾ÌQbÛà%Ýa½o“yÇÿ~£wêqE¨R6êµUUjáÓ|nȨ1{úÙç¡LÚÙ¾Í(°µ?ÂzŒ™w£–wm¾Ü’@ÜHdåBâ‰9˜õHï…­8¨H{ðÛ”Ó½{—QãÛ³gŸ´ôŒ§+%Ãêë€ç¤thòzj+««*ËìÙ³sÅ’Ï–—íÛwiFÒK!Š…¬ileÏèÓG¶A;1æ»ê«Å_á·¡K÷î=FO˜4.§G~’o]®ÔtS3“ÛÆ/zlƒ^XRî6ùm2ï´Yî5É9HÎ{Ö­X&¿ù9ݺešpÜÈÜž½¤LÉf™v™"c“Œ™wÛœw…- ÄDV.¤ “Öw»â%ÂËnÉFźòã·^• µÕ UçÏö‹Ã¤sÒõ,?›ƒ(_²H”ôRˆ’!=¢XؽÂ/”#ÛÆdÚ…/z›ª>|óÉ·V¶¢8K¾eÞ /ïUR»ß&óŽ%ÿ[+÷BrEnÊÆ/f¥(S^“o³3|“òESæIx!3ï¶)ï [:ˆ‰®\ˆàeBf=’}\vÁRŽ}Yþ: ?±¡µ{7’µ’\~¢xI¯…(Òs![9¶Í¡äžPŽlë“!ßú<¢ym¶ÑŒ[<†Åoóð[‰fÞ!×Ã\e/šlíÉØ&¾‡Cç t DW.]°°²• Q6¤"ŽŸ(Ò,Ê…üìÖ_{‹S ï„8Ù ›ƒ(ÂBz/ä'Ç¢|Éuûì†tdëGc³"ßY¥ÕbŶÕJùm–aÑü.ÉÕÿÄò»$ãØäÝ]Œfg Ê…¼'[XÙæ@58'-ô©øI¯…¤3¸ç‡%CöÝÂ@”éÑ‘Þ a! …ìË9¹‰ëìl…ùF’c"óK¶‘Å$ñ|wöo3Vy§³s•/!Vlí¯¬³3Ž5_›3·$Ð!’E¹°áIÅÙ®DK…ZZíí‹Î`#+K~¶‚a÷bØ=¶@ƒ—ˆ]gg+ÀÈ7âlö ±dv$Ôcgÿ6c•w:;WùbÅÖþÔ:;ãXóµ9sKíJ Ù” ü±JK½=Æ"Ø *x¿]Çða’nÛÙ ì­}¾­[;<):[áF¾mÍ=¡ïo¶¡ŸžW:ë·ë¼ÓY¹ÊWk¶ö—×Y·_›3·$Ðn’Q¹À»aöì\ãçBCׯàd_ˆÊn—ɨXÔ¥Y3¡JÉŒP;tÃ\¦g»^š3c†¬m g±›aV’É‚Â&™×Õ,÷]`èjŒišyÉ/óMûɽëÜî\g­¾,SÂΔazü¹ûÁÜjOÅ…†fC¾€÷ÚÃTÈÉA…·á3Ô}O̾û#œ–F.5Ýý@/Ÿ·úìf(³~ù®)ýó‹ žÁ5«¼Â7¡M+(ž‹©½óäÞ`ÿ¦¦-ùcѬ»m¿ryJ~ÑíÈ^§Ë~°_MÓJk¾)OºÝbvl) S J.Eò”†~q\‹Hÿê‰{Ýël¿ÓÝÅc ¯QˆêˆáDx;MŸ±X¥d¼>ß}»LO‚€õ3?]¥•ºR #Ŭu¸¼ÊÀRÉzí£î;·‡¸§Û™@\W´§. Šf]mú¬{Æ›^UbêÚ·PÛÖ³«+½]jªôÔš1‘JjW“šê;˜šnJËpjÊ44Ã|Csª™s ï^‚„[B0©0qž@+åFÜpkMüot7j}%†¦ÓÙdž¼¸Ê=)#µk•?ÑåL)25ýlá›®ô¦;Ët—Y‘ôeŠGËôUysŒ*³‹¿LQ(Sôè•)ÓÜ%£¼>ã¬ùz1–óqyš¯Ú©ëµ½žba} x[»eªCi‡ÛE]Ð6ŽÜ]®Ð fy þ³/;UíΑ9]»!ð›âKáú®anî.Lvyû+U—*±´®ï|]­éÝàpª[y­ê}P,±ë;Qw6öÌRÕ®ÃY%£Ö«îŰPDÙe*âc¤z|.g`÷Ú?Ïþµ(D–òT?´ÐG­‘¡Cëø+SÝ%#u¯¯ÏÜ¢Yÿs›¦¾ðÙù½ku=ïèµûf§×z¾ÕT WäåÜ} +m3>¤kŽ=÷•έð\¯›æz4A¬5ê—O}úxqÁ–¦îç¹è8ü…F/Ì é'nwšËãxØgh×w­>ä›´aµcè®*«¦Juw'ÌQžš®Ööîëøtðð³K3²Ï’_øGËw ZZdÆ(: $ póçÖì*{Øã1§t­ªè¬2OÞd,äž>ùòË3†ó¦;®M×÷ù†¥~àèã\©Ò´C®L©6³ÕNïHÇšš“Ï®0ºG¥L‘Jã’û(Ȩñ]¼''-e”Z§Q½ÄƒŠþª¾9aÍëze…íw{· nSøþ¬%¿p\%ÚÙ¿Éëb{µò®(I¹µú¡Œ”{GÍ$=ýó¯Ÿøbþ|1?n¬5Ùw&ÙvZAáyШ®1L4 {|Ýkt­vôSóÖüãéùÃMeºÐo¥Væe«¬jBCŠõåM~^è¢åiÎ{— b†éS ,ö×*Uf­ï„´Z¯æð™NQ¯/(ÚíÐôKçΔ0ºˆè#ŽÁó£¤ö“;îèR£BcpÖ7‹µñ›78u3"Å?Zq‰‹p P©ñ›×«q[6:— ¬Þ:jüµ)5ÚxpºäÉØ…Hv/„‘ (¸¹¤¤gíÎÒ5¥sö7K:½Ì¼Q’{Òí¸øê«óúô;òߺC›ºPäúÜ©i·LB¥¹>S_87zŽU_×\pmJmëË”ãŸ}6çù§çþ]õÉ9?ÊŸFB'J,‰{åÔçT8þÒÑyî,w»Å´9™Ëkíº;ݧjNçÃ4‡W¦:¼û3Sœeé)ª,Ý•Uatð ­Lq(ù…ãªàoMYÿÒš5T‰—ƒž¨Œo¯}9iÿõÔÜgSS\.¾êºF:äÔ‚Â!šrLHqf½þˆûY'L‰2¼òþû'fò<Ërt­í…f³¦m¥JÓö;Žç¸g¬¿Ý%ƒr¡óãg¤83¦y}ã/ÿâ]Gß2ì€Nˆ‚5qÓ:Õ÷`©ãÇœ2AxM~ý™gdÝ‹dXÌ$´¬‹¥/¥y½.ÿbe^ƒ7ݹ'Š…sä¤I9½ó½â¨{bÆÓŽ®Žm žÐyEÁœò©Êuls|Tyõ劼LŢܨX‹³±—d )×õsÓòz|t]~Ñ¢#\ý~ævÿ4ᬦÍ*9 ¾90ÐvÀnÍaºÃÄÖ¥;¾zÜ=c-XßÛ®®é7¤(mèöÜtQ(bV/n_Vªõóûàw{­Ç3}Ì“ó®å4”éôaà‡×@²LmW†ë‡£Ÿšûœr8?ÉIÉùò£+®¨³¿³F ]uÅW4¶Åk! $ò嘽Äv‚beÌþyG>ªi: Y*¡¸‹Â…JˆóÙΘ ¼ào*~ªË5ž'# Uí<ð¨CÓ&R±hþÍE(÷¬ò!¦ê·þàpêGC±Ð©X4ÍX¸€óýÊ)•)Sg•üjUéÞËå¤Q±hmà,&ŽÊùÐm5›d÷ üâ®ëÌí~>e»wÕdŒ•/ÿò„žKjËô£0€}D—ªê£Íe¿ $ƪZãþïKÑ× þ‚áõîQ^_öú´Ô+þÚy&TY½UýÉkøzaL†Q–æÒa^%JŽ:”îÔ`n5\ì¬ì–XQPÖa¼M™Ü2 Ç“”ϫʪ÷{[ðØ7ÆÆýc_ÿju)&xC‰þ:§pæûöÍɼm<`*±Rë¸òÆ[Žs8]לõͶ޵ðNÂK¸Á{ðGÑÂݼL$®¿kÖD4¦\G™ÞÛˆ@î‰Ü͹Š Æ’Œö)óÙ–|wƉæÓÉՎ̤‘ij)¿ÎSξ°ž’r:f…êÔƒ·›‚êœØ"[¼ÀMøÁ_¢›Æ…J*Ï“@Ò¸êú[{*‡ã,ʼÈ^mrOä`Úˆ1ú»RÓNƬPzðv$te †ðÒÍ—)WÞtS@þî–®é#i«T‘ ϯT|wå¦;.ÇYGŸxfwÜÕQe¶>äwÏҌԢ)G~Ý?W-ØÍ¹¥{†’ 4]ËJ3RÔ²þ]téÅY2°ÇüQOιw%åרʅÄÛÕoذ eÎñ¡» ôoù-wb2=¯p~¿DÍø-2逞’›}e^ëÞ|3rÏ*Ojư Ï3•®õq~Óº‡tÒ»dzÞÊ=5¥Ë‰0ÑÅÜ„.2{1Å-ÖiH1nÌŸ³#Êl}äÜß_˜Ú«ÏòýsG}“—£—eH4èZC@zq0ŸN6f˜úÆžüóÄçŸOoM8ñ|O¢V*¥ë-Å™âò|YÕ 7‰B‡æ ™®Q¸ ?áˆ_½®ÌN$ЦfׯPæ…ÂÓüùfäžUžàîŒÔôô£°@ž/M+o>0^­G@¦©nÍ”)’wÇàXŸŒ# ‹Œ€0Ûž›Vv`ÿ>ØÝÞe¶cÔüLOÏxÏîYÌé»%0ÅúnÆÎm‹®Ï/šÙ’ßDºžÐÊ5æå`åíD/qnÂñioA/H4b­L™×†·BîÙÊEºÓéêá(c™Ò ÆéàÖL™‚™: µ7'µ!ó!°¡gVêëÿøË*ì¶WÏ…víîSPé=èÒÀsù]È‹ˆ3”6ëfþô—îQ>)̤1³x«0€ ËM«©IÄ4Ä {F¤p~¸ËV.’"CGF¾I aÔÉ=­+e^ëßYrOÊ[¹HÓuG§ª`™Ò Ä.p Q¦HÞÕŸþ]ÉCX›Ý­`kÝ¢i¢™ÉO8ÇX–ÙúäÉ“S5§þdEº3Í£a9lº˜ØÅ}ÍÐ\ê <$)¾‘D¢vaà4f%¦k5:~¶ JļÐê´óFH0–ÜÃlëüNÛøâÈ=[þÉ«äÛ¾ س´Œ6Í[ý„©übYÙ—°ýlj®´!ëzB!ŒåÓüéêÔeÐ~­Ž¾ ŸW&Ùi¯ž©˜2þøcú (.ñv$û<ÁQfÖ(¸:~¢ %j>h”&ž $& c1'óm|ÁMÈ=«Í£UÒjO‰¸ ‹”åÛ¼ü‹O;zè‹ó†:h¯oG Vå¢Éƽñ¼¼ùÝôÕ™“(ªcxl4ývH¥$x‰¦^§GG ¦ON?‰~ݧEË[#Í„zjüú쌓è‰qÃé„.ØK¥ñT¥üje¸ñ4qcæ",3}ßwù†ûXÃAZÆÇ ΤgO>¡ÁÈ®èß›næºn ²[,ôИá4¹[b‹'g­¨()X¾òË=úäûÏ>õ—íÖ¦s"P.©<¶x9|EŒÊ…ÖèôÝWÙån„à{ª$ƒYØÏÏ%Û Z ˜;_Ÿ³`~^¤Âûâ{à—|Õ>Ÿ|ü̈ïé>ø¶(Æ+L§S|¯3©k¿ Ž;ð“Åj¯wÖÃb ¥è¤±dïJ»–N§ÒüÝÕá­vžòBú”ºEi(­!¬ ÙCâ(sû(¡Ï¹Ôû„ß{‰Ý/'¸çqn9“>Þ–Šl†®:¢>ÑtYïTбïÇ‘Óúf#0+‚ žšÔ…9JÿÙ°ºpܦ½ÒE8oT_ZwéKƒb"é•]û)ž¿‰‡†$«þÝ[ÜÇ݆Ȳ®ñ=þºùO:^Ë‚÷ÿóýæ­ßo~”Í‹!ÄR³.¦¦&Óù½°‹~MŠñÁkà+´V}ÖŽSÇm·×ÊÅÕûÐÈ„¦)ƒ:ÞÚ÷¸¼’’“öåGœ:ñÂ+®½hÊù?†i=Úæà«ÕfŒjç­¥~³DÜy)öŸÏ’{÷>²ôH!k÷r¬ÛD…|’bžø£%ìò‹È]XDåŸ~M‘÷ÞL¡gŸÆŸ³Ak6RñãÏ‘mðŠšñ{*ûøK ;ïLªø~…œr+ŠšvåßÅ£?C7þJ)%îŒL*úë°ôí©ìÝëT’;¿œ›·“ýÄquÞ‰ƒ Þ°JÑz€yÕr ™»YÂð˜„X5êÿmF6–ÜE½½±'Våäѽƒ+_œ8Šnúq- 㙌† ”–Ó£¶Ò–¼Bå6:.–6ç°ŸrºM cÁsRb]QܾÏÌ¡™c†Rˆ0*t8éý‡é5V@7ôïE×öí‰î6æÒÌÛ)«¬œÎHîê5-¨ê_zq)­ä|~q(“º††ÐõW,çíXEãfrª0„ÜfõÊs}\¡!ÿ`)t’Yî2a2EÃÞ˜wwoİ>Ò »fÅõ×çx{_åfö}èww³YÔåŽü{ùጊÐÔ”ššr=[âÕƒ<[pf÷dZ›“Kç²|v°¨„^±Ž¦¤$ÒDV¡ä½|ÊDº}ñ :§G7ztüHŠåÙ¡Ã%¥tÿò5´‘±^|ñÙ´ˆe®Q ñôõÁ Šb¼½Å¹ñX¥²¹ü_OOÃâc©Äé¢W¶î¤W™o½~ú$Uœ¹}åq]½]Õš[Æü¸ˆˆm< Í3y ±‘á§$¦v?}Ðèq7”<õí»o½œ™™‰Œ &ï*xPÉÿÌ[x6Á6v$•þ _ fȸ‘TüÂk*’òEKȱd›  ÐŸM¥Ò×ßáNï9 ;‚"®»‚û î˜Q„ýü<ªXº‚Ê¿XHÎ-ÛÉ—OÅO¾ Fí"®¾Œ?®¤‚ßÎ w¹ƒÂ¯¿RÅ~ýUê}1+&+ ΌZ"†]|.]â©èOPÅ×?PÄ Wq‡<¤F¡ÜYÙTúÏ·©ü‡å5Üå‡ õ!``´–gKÁÓŠŸÜG)슟³!ŸîP‹ïùâE ñ=óQûøÑTüÜkT8û¯J¡=oªÊ–/¾qË5Ì;wRáóÉdA"j:›®bfŃ‚ïa¶"$*…•*í^ú( éÉÔmÈU”µã*ÊÙBåE‡iߊÇ)š‡ä—Ñ5£?ܯ‚^ãîS¥5¬¡Jép–£cûÐá•ýÑÞs9|:õs7+ iãgW‘ÛYΊFe¸®ý.TñÚø*eîü€’9Ýè¤qõ¦¥á-/>Bkÿw¾R,¬!1ßk*ÛÆÅh¶b$ TX›»ÓFú1£0<.šžä¶r¸¤L û¥eôÞþCª(OmÙÅ»ƒîÒ6±pßÊJI˜;f˜Hí¬Üõ‰Š`³¤xúôàšÅ hÑ‘lú(=ƒ®êÓ™tçŠõôÕáLº¶_Jf9 ÁÍzÓî«çlÜIyB›QùJKE\õï»(ÅbúÈÁt§ñ%+U,*£R3P.,Nƒ.e3©Ó×\. …áÒ¿½ÝÙÿÏJ¬¥çyæ­Ö³™tñ™v[b×Ûx}j¢%"<®dû®²½û)¦ÔA!ÎÖY“Â|*…~+\µ’YÙ»’Í¡þ»7¶äæsÝ—Ðë·¨¬?ÀŠÈºì<ºùûŸ¨€e¬g'WuÙŠ_ôéIÇÊ+觬ò'"ùõÀ>”Äõ|'Ë{íK§ûF¡ñ]»Ð̵ؼ‰èÓ‡XA9¢ž[â¦%r¢BˆÏ}¡5}¬ëzÇSn\DrTlü'_wköa-‘‰ÇFکɱf•}ôYR’)ô³ÉÒ·9>Y 01³s•¢€ r:É’Ø•¯.<›QLöF“cåZå¯tÞTÆŠÈÍ&Qñpî;¨~çÿn›J@¡¿8Ÿ7ŽîFnî8-q1d6ˆJæ¿Iå_OËW“…™:mûä‰Ü¹VÆÅç û cÔl‡ú!ÿA@h&eoÿ*xp|/‚<,]ÈUe¢ùž/^äÚ´U¥î“ï1ï+üããjà&¤j‡55Å'ß³³ý4øªkÏ~Å+ÍŠr²²‚•G‰]»+G›YÜÖ Î'QïZö(™n¥ »–×I ¢ò’L5+aaS¦Òü=Ô{ÂüÞI¡‘©|¥³,ŸâºO©Îg~ÆrÚ²àfõÛÆ¦J ²‚½ä`P*ØÄ*eØõÛ›g) Õ{̘]Ï ¦ÌBtöîOØÿ1^çqS½i©ÀUÿ"âÒà©Ï’•M«ö¯|ÜóUsž¡\ð¦8-cõúžô-+ݹ¼…ÂhÎàY~Ðî_!ø'ó¦-» ŠèTžá(cE¯Kh$õgÓ)M7,[£”Œ€ƒrY!9ÊBè_X9™Â#ãç¤&ÑIÜ'ÃôªÇÕ—П6l£lö·‘g>lv+:¾ÒÚYP¬ÂxþC:¥<2Ž4°xÏþRQ¸ÜùІ u[,%¥ÿ3ãB.Ô3Öª™ >Œ-×Wœ\Úõ®°/|½gwwÖÇ b&ŸòŒ%$ô×–Ðг¬ÜR¼c·1¨,,&„öœè‡¾p~\,³ŠÜ>-mêê´4˜IA JêôÊ…ÉÓhЏû"˜*™¬A»f«»ëýO˜ÓW23„qñL‚7ÂŽQ±ÏÏ%׃jVJ…µwOžúç†ÏŒÉd…ED¬¢“É®•Ã8wí% ç«ÿ"'›n ‚€ ÐR¸¹³Ìw¼ l¾x‘…g9@¾ø^è¹§óš°[©ü«ETöÁ§j þ}ñ=ƒMCA˜ñßÃU±t%™lÆŒävUp?Q9âo²°Ã§V×)FXTwXvSy1[°0ÿ?¼ùï< QRí¯¼8£úÙó3!£.ú@­½ÈÜñ.ÙÃXy¨¼Øø³šLÓÅ’‰‹JKûM9Q-L/+<@Û¾¹J öëW-q7¸-t¦¨™T\%CPôF),\‚Ž9Ê$ fQ?°¹SjƒJ\.¥X¨µþÁ$ &Uÿbù‹ÃYt#›/°Nä¨J“ÅrñsCi©@üïRlw²0üÂö=´­ þ4j̦X yæÐ_JOˆ -éßì½±éÞ? ‚NçKÑ-Óg²`Bôʬ<ÚdCpìz0íO|ÿ_ø8“pÿ_BB·qS¦žÚkðàSº–«]«ø ÷Á„Hëј–ß³‚e2( +ZTUÿºGT*}9åå”Á¦g¸ò ñ²Js³ŒÒ*ù®Ê¿·8±®³S˜Ax¢RúK^úí™Vk·€>àµyý“KŽæ¨ç»õ¥Q9{æW¾÷·íùY«‹-f9“òëÖ#žSë'0C…uÇë-¾=|D™6MàY¡\ž]RäGÖ Àlåxzò&:Kx½×1VTN㙫JŦ2‚DN ³%­Fã,N -É ¤N?sá­Ö°º‚3†}:™YTúá—d°À~Ó•Á P-ÀžÿYÙ¤ >rnÜJÎ{(ê;ÉsŒ\;÷’rÌ`JþõEñè^Ìã3È䡸¯/«k0"xýEÌóV6ÇX÷ÁÊ D 6ßóÅ‹°.­>ªXø=…žz’Ú&›d¸yÐ’ÚMñÆ÷ÜlÏ\üÌ+vÙEûêÓ¼‘E¹Z¥#QîÁ(:q›½@›¾¸†·zíI½ÇÿŽ•Šhž‰ØËfHÌó ‚#«¨({3+¦Š’,*æç˜äqd ‹£,^gEäý'ÏT±Ýõ¡Z(ž—ñSƒiE& V;R!`¿I¬ÎÅê÷W°9WÍÑßê—þ?@BsgÊ`Ÿ&> çãj¸@MH *^­‰iáÖ %þGÝn¼zjäÈaçfÛí1•öMK¤©¡óli?Ï °¯Z¸T™NÝĦ•Ÿw†ªëÇVo¬žñð7«6ÒÿñlÒgŸ¢fH°Æb=/ô†â‚ô~Ém ‹Êÿ±}·¿Q6Ë_J^YRáðá7oF=åì…n8Í"P}LóA›Ã´AÒÕ÷ýá#KŠNºlõÒ€dÛ(ò(%ô«ØÀ»™4†0çæ]0ýíIØÖë8\G2§Qå&T&L¥x·ª@Ðûã'ÓÆˆ¨åÿzæ‰9þ,¾0Œ¢J ’”8VAÀßh•´["ùW|ïú¦½?4/÷Œ@ñ<ä¿6ßk/â™ üºsóê@R/ßãýä¯ bá…ïAƒ L?z^y×ïžØõè˜Iÿª“ï–q0Ôlvfa{X{x¢ZèݘøaUÎÊE]ùÒ °èžät°‰TM웚–¿ùú±äjÚ›—ê­O…D$_]†½þò^ãkŠ¿H]ÖfSŸÂŠA6+«þ(:=ì0å`3oë!p^E8›/ªenãOZ¶`F…è5%rÃ÷-×ß>…}â S)XÔQi7Ä>ø¿i2¡ÞÀw"/¹éŽËº¤¤<³µ[ta^ 7Ø6&`Î<¦D›–óïî|Phc«M©š’Åžå‚¥h~­]¼°u-fLš·¿ùa“3œQ@»·nÿíßÙÌá·ØÃßL5Á°ubÑ3îð«X jw6€ÔR,àŽ:˜ ÔH/˜0Ú(Å¢2ù/‚@]jó½&ñ"îô½)H­^¾Çç xã‡us¬.¼ ¢J±@ °vj,ax]ű˜¼ýí:ŠÞ45-„m&A–ÆèkÀ§¢< ä³Q,¦€Í§¼)x‡ž¼)x×PZáp3‹ž9%té­÷\ÍÑGàÙT…ƒúEˆß}ýÿM»,1¥û˹1á!ùÑam®X çȘ§b·C<›Ô\ák5j+ˆ»‚×N57nÄÓa¿æÞ9E®òò²U¬Xà#‡-V»žðU&Q.|!#î‚€ ‚€ Ð\ š¿¼å®3â‹«lá›c' Uæ ˆ¨¨\tmhåÂrùm÷ {63Žwdêm1dÖÖ «³N‘y;5”Qh†V¸ÌuË~ø {À¬‘(uj‡@\­SжKEðk;ì%eA ñTA4>¨„¨FÀ'ß3LtñBÍ@À'~q ]¯é–×ìõÍÈZð哵|âÛÂ¥S&QáQ1×òv³¶=]£‚rô¼…1 XtJ±È,4»UÐŽukžß°lñAN ˜-îA=sár:‹ÊBC¥»mB“nÀ¯ A%ˆ ´nÓ/<¯éà7Ä÷ŽŠEJŸÒˆ›>E In·;?ÄUµ·hâïìAlüñ»Ü.¬³ÐBg OŒ˜c-RßR^kZµEjg¯‚€•?5·ÄÕµ ÔܾnÕ?|öábNH/äZ^Œ»EiÍݬ¨(ÏÉŽÑ¿Vñ1âü0³¢°‹Â4~úÞ‹+e‚õ}²ð–UŽ'¨†Úª2¼ð=Ýy_>¿¯,·Ä'¼° TâŒæ>¹Ô[Ÿ!Øê¬(Ï q†IÛm¶âp¹¹}æó#ä6|ÿÀ5mñ"°ücÙ[Bc"°%-ε ®Ââ£;?ýü“9¹Ç¶¯ãS‰0è‹°»5ÖÖp2-OÁ¨\T£P”—·¯()ÅRNQå2ÝZ LÀ«(<ÒR”~`_^åµ ´#J‹‹wvKµ Ïk|¥øÃ÷òsr•õìc)3£)ÌhÒ9ÏX¼Ê(ÎR”—±ÏKq ¬ZJKŠ÷&DE[í¼£Óó’N‡ufaN·-+'g'³ ´Å×ÊEÈGÿ˜· ~Ò‰9)7_÷Ù¬•'ÖuX”[¹`nÓQ¸zÝw‡^{c¡»´3R`88]Û¼á;Ùañ~ HŽ6°¬£_j”iÓò¥?¼ñ®ä”À¢ÔÁb^À øqÑ–¬ˆRA #"`n^¾ì{áyM«Úzøžææ–•Ë×ß#Î!MK¤“†^ ô)Æú¥K1ÐîDÞjS¨qØ&·J*ß¶nõZ©g-‰ÿ¾¡\€Ž%÷ÇŸ2½öú_Y®<øC½’MA –.‡ÊwÝ‘qìÀÜ¿<™þü¼ÏW(ÀdžàÂoÌ^è™ ~ > FåBwîÃéûòŠóò6þÔg ËÍ'Œ 5Œp^À øqLSkLŽ@|‚@[  ¾Ñ›×å–¯ž×¸*¨‡ïiþ‡»+;+£°àXÎöe'»LSúPNÀ«8¿þ>e×–õ…¥Å…Ë#K*D»ðX?EäÊãſܾÇVž&-`ÔÛ{•ç/[±gÏ´Çž´nÙµj`f¡B²D–;©o#Eaü雯=ºáàÞ7;)TM.vÁ+?sä©­¶ô쳡\ ˜åÀ<Ân~ENnNþ}$÷„ä±û޹º+¡° ˆ‡Bœn‚ÂS5=Ń»~ö@ÂÝ/ýXјý¹”z´°¢(ýàÆ¿þ|Þ[OϵbÑ‚Øf) Tà \8×ÊFÐÏZpÔ¢܃‰tG ´êÍ«Ú9pÔ˜¥ ‡Ž>9%?×’’'3w¾*3#.'÷ÑÃéË6¯^±‹ýaT¢5˜•¯,‰» ø‡@5ß[õÃ7›û ò Ëg Ïk¼øžæª?áØ;6®Ý?xìø•–^çg=d‰·òA§B^Èuu§åç5Ô§è¶[ÁkZò‹^ùû§}û>zbHbב^#ÇTddnË|ëíïÙÂ>.ôÛè¥`hå Y±˜»zù];ö¼õôœ9#&žR—žµuõª]eºþ <@aÃ,îÀ3žuô[»^Ð=Çù¯¶Bˆ†àñ©Êúâ7ߺü7wözwÜäîW¬Yjƒ‘©Eè`Wqaá!àůuC׌ ¸ `>¯À¾÷ùÛ¯¿péMwôçoº·ð<ßMÖ¾W-ør,Õ<ñ«÷þõÁå7ß™ºÌ¼6åäÈ7­¢`ÔŊŲâk¹O)j¨OÑÂ*Ì¡J\…Ÿ|î™Þ3xÌ•Z7fqQ<:Þ%» èÛ…8@IDAT§Ÿû»ËáÀ‚_¥¸G(Ãuúìä{ZÆòÜÕ@Ë^®M+–màk{XDTøðNœÔ½{ïÃmÚ`Ø71¼O¯žöäÄî]C“Š]a6ƒð[Éa³P/ä¯à{iÖ£'ˆèq%Á³ aNŠàcÕÃËœn»ÛmÝ]|lãʯ¿úºpó–œ0Û¯ïŸ6,+!<4½K$ñÇ#i…'Gαôoß}냮†=4*6!&2:: Éb—`GYYÉÎÅßot»ZñsïË< “'üF}¡ÞPØñ J…V,à9 ~ÀêÔ+»µkå‚‹\>4f€TuÇf]\^RRðÅ»o¾ô³Ë¯¹ó­“Nï~æÖõ–1ö’Å ”bï%GíÔ ¶Æ0 ÀŒ…R,'àÜø~Z¹h§%l C€yFóŽ] Õ~|ûÃ÷J ó|ðïÇÏùå¯fž×Kx^Íúk$߫џpL%¥¥á_¼ýÆ?Ï»êº7§Ž ýÂÒÇ¾Š Cú¬±€)f,ª úNPÞ ¬F”efIŸóôœ‹n¾ãñc Q‘qáªÌI1¥Vˆ 炜íËW­ÌùbÁ>¯ÅÍZpÁ…V‹;p^ÀWµ‚Qå9 òê߀®KO ñtª®ìöX"nŒ‡¹ÊNõ’7Ý Á¡¢¢r23³ÞíÅçιüê˾6nÂÊÞÜ'îÛi™Ñ)·©Å¶‹Ø ?aŸ}$}ߪïýëýòRµ90ÃüD¹`„:&¥6­AIþò½Ìƒ¿÷ê sϽüš˜çÔÙy*»‰|¯NÂQ…;z4÷½W^˜Ïø^äîuÑ ;Ë'¹…-³v³më”ÛÔb»Yì …ÅÛXcÑÈ>Âú=r^røpfé¾ý?õqõ:½Ëއ",y!TVkT›Ãt ‚ R~%”ºÂ+Ü–ô]Û¿]øß÷þër9ôH7îz”m¶&µ<߃°‹t z Ðø Ù!’/˜…ð…©¬áÅr¥}ÍâE«ùZ_ån‰ˆŠ ‰ëÒ%’ïáû¶o>ètòXUúÒw%%}š—]̲»gZh3z¦†ÉþíÇï/ذjÙšñSÎ8%¹wß±Éù¡|0ag'Æb»\äC˜V©„`^Åx.³[ë(#XtÍÊ„;¬ÌaòÑ̇ið™éûVì~ër0ϼ =#* ¯ðEC+gP6à¦qvP*ðïW‡£v=VpÛŒÙ×p;{#Ônïñ|ÚƒXðâIhИ’êÂW7¾’«ž£úÞç„)SωML`’ň*-vÇV”aååÕŽývH ´8( çX`kÀü£Gw­Zòí‚Ý[6ïãCkÆ!GX8„…Dx†¾ ôÜöxj¹ÃqÐb1®›7sÚ[ÁX ¦ð½ÃG?ùô ;#ÏC·ßóÙŸpô¡ý†ï>áÔ©gÇvMì>%ŒòܶBÃNžOÁÉÛ8 çX¨>%›û”ÅMêS€U(_õNä ýv·Þ‡ž0õ¬_ÆwMÂÒŸ…Mg\;“#ͼ¨°êÁO,¢í—UÄ"¤–ç8dåD‡RVL• <ÜØKö¢¬S´¯Ê{q˜tüyœ°(#ÔµÉÉ&<»’£YÄ<þ¦KQ¹RŽ»T>™,¸îMŒ¤rZ5Uîĺ@Ý,ÓáøʰWzu»aE%Eã2KcM·ifgÙ¼uå²Å;Ö¯ÛÎ`—¾v‚ÕØi€ù@Áðh(¸$~ÃïµbߨgýõˆºG<@„qOdô{vVeÃ{”B¸ÝGxÄÍd?¤Ò³ÿ€ÄýöÞµyýîÜ‚‚’ˆý»Ø“»Æ$&÷H=¥Ç€sØOÊv”Y‘“¾Ö]VVæ(((vd+š:ú¤3,.ïz–4ïXNö‘ûصó0›9A @´R€|yæå€"¡Üõì….+~ãBX¼‡ÄQ£ùw‡¡ê·=–(Âþy±Yìr¸sþ^®•GT*#ðh̺ Ò{ùúGL||ÔбÇ%&%‡EDFÙí!ªy³çIÜŒÅ%%eGåÍÊܺvåvžVÔÊF<À¨ P`š¸?à($tÀ+ ÃtEX#>Ö5…ïíÚ¼q_óbb‡™0”• ð¼èÏóPÉ-Ã÷|ö'HaÏÖÍé|½ÎøŽKJJªìSXJô>ƒµÍùÌ7°uT””•á>eCsûR¬ ¤a^  ûwnÝÃ×ü¨˜˜Ø#ÇôOîѫϡ·×lØ¿c „iE IÝ¢ú]ôËó ›µŽÌRºcû¦-‹lÔ~- õ»æ¦‹Œðpe¯Ýqwådglyÿ?ßyº%]|Ùd#%¥—§›z.//Ý>ãŸ;+*ªÀq§œ>Ä6rl¿N·ëð‹Ÿ,ÈL?;zE}MîuêÔÓ dÈ“Óüÿ®X¶eÕOû´³Íf#÷‰SíÚ¼n¬…XôÑè³q/¸¿êüð³¢ó=Îqõ(<b]‡¨?hH¸ X œ¨­X„ó3”¼‡»V.|oºp¯­\hAB=dü†Ä 7é¸ÂÒwï*æë?ƒŒ‚µë¡€YXÐÙ’Û½çªððp«aµVðXùn±Yé² òX«ôPpþ÷ãŠÝˆ€ åÕåDH_›3ánÈ?ü´­@஼C9ñ[_ø­ËΓÚ=[¼uÚ¬Oy¸b@ªmʈ´´3P¡ž„‹Æ†‘¾ºòÕ¥ê74kO¥eÕåÕwvê0äÙÐuƒ^`ø ðÁA±Èæ Ê~ã#­); Á‡@ZÚ"Ûaç’M<Ô¸kþìé_ ŽçXøÞq,xjI¾'ýIM°[[3ú^›üÑg'ñ•ÈW<_pCŸ A¤ûiÜqy  xBu§ø_C~uø×q뻎CûÑ÷úüâ„EýÂ_mÿˆîZ¨…0 ¥ V_è³áÁþª©•ùžÆVÏRà® ¼Ã3êõÅІ–ÃøQʹw‡;Ø œ]0ª…y­\`æ"–/Ü!ÿiåáqÁŸÎ¾iüÆÒ{ úpÓaáé#H2”<\À_+xïY£Ë…g\x_ûb§ÎA¾]“Åbár»V³Ðpgô…Z™Ee¢x6JÝ(ÐðÔ´ßuól@ìÜáH7dýqè|ùƒÂÈ ~7à'$tÀ#x{¹Av»qU°Hø^£j°¥øžô'uao)lü}4„6 pAé~©î¯á®ûløÃ!Q÷÷ˆK uZ˜óô_[¨„íaAðã/Âë2£=è0ü¨ÒÕ~u>´_/î ÄS;n¸ëø´ïð ¡}3°r>}·„ŸÔÊ|é\È«Æ[ßu™Q‡Z©À3pÓØê²×ÆJ¿×i ~´ Üáqà!¡½ÔVH‘–Vpt¤ÓFœ:^¤§ó¯ý@v‚Rƒt08‹ ikEGçT½á7Ä…«Óm÷Ä£xóx•ë¬6ã´—Ó¦¯¨•a” ³P*bªîø w­Ù¢y6"þÙaH7jÏ!> (øqÇo¸ã£•€A ~nO›5Ñå4¿ç­ßàY D= ßó« [šïIrö–ÆöxÌ•}6FµuŸ­G¢u­…AÜq¡Ç¥ócµ Ž¾L÷g¨?—Z Õñ ,Z(†_ô“ð«ãF-ãîGáá@گ΃àWçÏH ~=óáéñéxµµ­×‚ºÏÖB-=Ní”ïi\Q~Ü<1ÐåÖ…?MÀï=/¼CxÄ yNψÔVtz¨#ø_\:~Ä ì3” ÔÒ«?í¸ë þáŽ8„@Ðî)49æÞò¬ü‘.}ÌÓŵ 4 T8´J44<ã£Ô6Z³Õ̃_U7t<;îÀ@ã%X@ûÆ…ßÀGPü($7ªƒeÞÀk*ׂWwiŽç^øÞq,|<‚ïIR v °õ¬FôAÚ„6ôMƒ@¨å-ðiRâ7H÷u q! ÂÃ/î*áþµ_- zúÕòn+àá@:ƒgÄ ¿x¯ýâÂ;ϸµ_ôÃð‹°ˆ¤Ó.ºßƳî³á§šÚ1ßÓ˜hÜt=ê¼ã½.³vƒMµßyºküP/¨}éð¸kì¿g]ñÏõüÒÓáù±†]·Ú/Þ ù€' ~xo;/÷Ì™“X^âúˆ[ÁXÎôý¼cž—5ºQ±@¡Ðš«gÓeÖ÷¶+TË¥¬?FÜõLJ fNx–„AêTÙ߯þ)Þã}Mh„å’çy6ʆ„ïÕ[•æ{µ?èÆVW¬'ÆZ!€À¨ûgÜánžý8ÿ¬îë´ˆÃç®ÜÊ;Âìàýàÿf·Ú?®µM­n\šè;™~׋×yÂG ?0.ÏK¿k‰t$A ÍÀ¶‹Ø…ùÀo±Æ‚ùÀüФèûž»÷^(ÒŽ„ï5X¥š·i¡%ùžî3t?¢ï¡?ðÄÖ³bµè WýØ£>ð¤ÀÚB:Þïg\º_Ôm¿A¾ü⽎a@H[ǫ㮧ßÚq#¼×â×ùÀ;~§ï•®ü¿³ñ½ê‚7ýA»¸{’®³ÚØ{ú‘çf"PôfF×:ÁïL›=Æé0gñ.Rç²paåBìçÏþ0•‚ÅO5ˆd©[FuFK oÁÿƒw ¯M¼ž|<µA‘ßA‹N å®8•uol7Ë»B}e³Ó_L›¶.h ÕˆŒ ßóV+ó½NÓŸêVÆÖKíV;yŽú-gµV_WŸÿöè×£(u;;ß«ˆ8u%Ò Ève&ï˜;7Þ,rï¶Ð:RøJà…ßA]¦ ‚_²*´üYó!¬¼sŠA7m2¢¬Ÿ¿ôðÃØM¥Ó‘ð½NWåRàNŠ€ð½NZñRlA@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A@A (0‚"—>2yÇܹñf‘ë|·…FI)|%˜†ÔeòQTq:5üY›dÐ1¾2,nÚdDY?éá‡s;#(Â÷:c­K™;#Â÷:c­wŒ2¥ ~gÚì1N‡9‹ ó\Ó4¬\ˆý,t6É<Ö1ªEJ!µ0ÈHà„T“¨·a˜.2¯lvcú‹iÓÖÕöÛ ß눵*eêG ³ó½úÑ‘·í R.îyöÙÐò¬ÂgLÓ¼•…‹†aüÍnµü|Úƒ‡Û+À’/A@hYîN{<Õár\Ì|à·<¸0ˆùÀüФèûž»÷Þò–M©}Ä&|¯}ÔƒäBhK:ßkK¬%íæ#4ÊÅ=sæ$–—¸>âQ˱œéûSmS楥ál>ƒ #ii‹l‡Kncžðî­ °\òÜ# ƲøÊ³ð=_Ȉ» Ð9è |¯sÖlÇ*uP(•#wù‹x”²ŸÕN¿œ6}EǪ) 4ÛÓfMt9ècžÍÜš{FG™Á¾×Ô!áŽ@Gå{¿æ:G -ÁPÌòÌ‚g1c!ŠE0Ô–äQh]0ØÞ^Ѻ©.5á{ÃVb‚ŽÊ÷‚½^$ÿ•´{å‹Yh¸¦P2c!ÍV¼!Þ^žáÍO0¹ ß ¦Ú’¼ mƒ@Gã{mƒ¢¤Ú½r]¡°xk,€Ä)ð𠵓\Iø^W d_h%:ßk%È$™V@ ]+¿K{:ÛÍbW(Y¼Ý ­A’‚ðð ð Å;‚´,Â÷‚´â$Û‚@ ÐQø^@'Iv­\”¸JÏÇ9f˜ñQ1¨A ƒ €­©Á3À;‚µHÂ÷‚µæ$ß‚@Û Ðø^Û '© v­\ð¡xÃÙŽzÿüiÓ2€Ä+œyžÞ¬¥¾¬5'ùÚŽÀ÷Ú9I5PØqKġd¥²™ƒ×H0ï˜;7>Äíñ Æö"Îy$ÇÓ­µž÷ôÓáeeöŠp×ßÒ~—×Zé6%´´wCrí;ãÖ^ð×ßÿ¾Ô3˜¬¸í¥öP›­ä‰,ô|×”ç´4Ór4d®J/±bR¾˜ÿ5ÅZa : ÞQË5h~ ßk|UÝ“öxr·s´iR4¹»»[»mLK»±¬ñ1u¬ÂÏ:V}Ö[š ç{õ–M^íZ¹0L#Þ4ÌcA‡jgØYäú“ÌskgãÖé3‹Ødd¡ÕzëËidÕ~_ßoµçþÑÂKá'4Üøº1‡•+½ß4Ý9¨xZ_:mýîˆsû>3a¡ÊGIÉ×|?Ç3OEŽ’/ÈaN,%¶<½Ïó]Sž3éÏ}œÅ®Ý{˜–žÂ·%M‰GÂG€Gþww ®'á{þ××­is†˜N×[eŽŠñÇC¹éyðð-3fÏxeæ´¿w÷ïé¶´Y'˜.cÅtyyÖŒoý Õ>} ?kŸõˆ\;ß &gÛ!о͢¸—m;h:^Ê<ªEdþÜí(_‡xSÂòcѦÛý/\åÅîÁ ¼~ͳo{tÖEÁ›ÿΛs”ZÞÌyoÍwËŒ9§‘ÓµšLbÅÂpñµŽgºà{!»¥’Ûý¨ÜÕØ<™Nãð9·i<ÒØ°íÛ¿ð³ö]?ÍÏðŽæc(1´ íZ¹h™"vÞX 2Ο5݂˰‡'Z-–+Ù-ŸG8RxtþoÈÜúç?Ç6e§wß}× Ó˜ùx‹·¶Û=sæ$ÖvóüíO|xüñè»Òæ ‚_ϰµŸa&vëìÙ)µÝýùívÑS·Î›g÷Çï­ió"îH›=ê®9sºÔçù½3í‰nõùÑïš“w‡Üކ@ZÚ"›áv¿È&ÌÓ2ÈbôÊìéc™×iìÅn‹Pf>÷äið´Úå÷‡¿Ô£#ìÝÓgö…É‘vk‰{sòäoúퟡÿ¸kÚ¬Þ õþ–Wü ‚@Û Ð®Í¢Ú’Ž•*äqÿª(›ÿ¿{Û´Y=Øá/ÜížtÇŒ¹ã_šùðj¼½}Æì\¦{¶YèH-"ñˆßv¶]þÎf‹»ßå<6Åt”½¤b©ü÷ö-Óf}ÂùÚÍR÷üoÖï8›•–ÐC4»ðÖi³VVzfÞcÓ?ñ£o›1ëvÓM–»R8l2Œ?ÍŸ9ýyíÏŸøîœ6«§“h^nAÅy÷͆íœæRµyú•Y3¾Ñqad“—EÎ!pc?‡ŽÇçÍšñœöSßB —i¥gßÍþþêË/:C‡a¾NάSœ&Y>b|vÙ,–›^šùÈbJŠ™žýÎïo q~ö¹.¯££ÍÍ»NSî‚@GD õì×ümCÙx4ã—g>¼R—ëºnK›s·épÏ‚va”9ò»Uxßáoòs2Í“«â:ùÖéü“ý¢g=¸ƒ†ãè“ÌënähÃ9·W€ïXÂéÚ—§O?tËô™ŸñtÙ6=]ļñ•Þ´YßðÖÈL2V½2kú™Êmú,6Õ2/%ÓX<öô ÊÓÓ좊*O†åÿæÏœöªŠJS¡s/Çoµ–û^ž9íŸÊíŸÝ6cö­‡œ³ærö¨Ø~½…ùõ×!I±>wï½å>Š%΂€ Ððk”¹æ[²ÔD »±@u›Nu’ñí3æLv¹Ýÿ€)dóî ð:‰ÛÎܧÉbqpgZ Ãñs13ýÊ…Î¥æîBv³¡ƒ/ãNÿLÓmþ÷Ö´?÷ÒaªîýÝnó%V”=< Ø]Ùßs܉\_íÏøXvŸÓ8îXé]Îk9~³%ÌÇ·ÏšÕqAq2L×·ìΊ›ItŸ»»Mzö–é³®N¯þåÏ$÷£·¦=ÕÕ›×ߦý5®‚h%—å4¾,ŒßNá08ÝîE¬dhA…ÌGŸ%Ó} öç`o½‹7jÇÛBy¯­ü: Ì7Æ¢0ü•O=èíÚ›—öÈð‰k^Út¥X(? ó—"þxù“V³<†aðo· ëÌú›Ì;™„3¯ÛÉÊ•ŸÏp—™Ëî›=;™ùÏRþ¶cø»f^`˜‰àp'Ã9Âd=Êñ®Ü £rà¡<½4ëAèáзÛTJ òc–8ÏâôÁK#ÜÖÐOáÖµ[~vÛô9ç»ÝîyŒi,×éŒï÷ŒÓPæ×÷•g<Ñ@¹äµ ´3D¹hgèìØCŒLË©xv›®Ÿs7]ÌLý“Tû´Ü!e殄^î'Í{lÚBæFÝàŸGç~3ÖŒß+ó“& ,›\ÆnÙN€sÇg’Ít;Õ3~ƒ¸#´†eºÑ3)Æj·õå4¨ÍÀÝŸø*•S­±’å^ž©¸2ªKDoÎÔbîÔ7P…1q9Mלn߯r»=>µ»mzo–ü•)˜aš`:üÕG6{ÈWï§œñ8ÃQö'o~K\ÅrÉÙ_…aµLå‘Èþ»…ó`ìbw+—ñq„Ãl pÃ3p¶Ø­ƒ,a+_Fåˆ$^TQKä]Ç%wA ƒ" Ö}±š~ÅWðz‹J‚zëôÙ3<¯;Òf„·þðþ~¯àÙÞw៿×ÌÓÆA¸¿cÆœS˜Ÿ\ w‹a½œÝ±­$xX »÷*-¥ëÈn|‰÷à·ýiîào6îV¥ˆ°‚b†Zu{Ú“IÌû›a£ÏüÉ“òK–áÎ4»ïáW™ü wÈ¿žŸv¶z®ç_{æg<õ •uÃ\Çup>ã{ºÅBw2¾?ñŒê§ê)š¼v†€˜Eµ³ tv\%V¯ì‹ r«ma™‘ÿÓýà#ŽÙ¿ºmúÌáÜaþyáe±‘¾òTµµmœ‘KÏšÌ3÷9ȼ;WEÓ¨–;Š‚ù³¦Í®Šo›h=Ãî_x”¯?fþöÈïrø]½ñusžœyˆ–²‚d&»M÷ûl¶ð=ïFõƒÕf»îå´‡÷UÅÍ‚9Ùà–]Gî/Ø\‹5 ƒ;`N‘Gû*² Oåר ª^²PèïMª8—‡.o»kÆŸ_¬pó`¦'¹é,üdAäÇùM[„gŒš²ùׇn7ÝÏ霖ö° gÆÓt± ññýþÿ.6i˜ 7E.3A?ÖwŸ7ë;Y‚x–Ã[¦óé:~ ³O•[^W¢~sú–£”ÁæOî^ðÇØ8ÏÙ]U²¹Lžá8N-™÷ã±Ê“ б`S!P_˜ U>â¿ñ%óµ?«‹gŽ»W>5Ä_jû׿9Þx_aó×5OaÄŠ^P+ Ð%à'›óœÄŸþxÅ óy¸ñÛ‰lÎ¥”þõ9Ü@þäé…ÙÓ÷s\Káßt™òš’a\nž 5JãcìÂÝj¯ü,Âù÷9•å#³} º¼ÿ͆ÛoŸ>çBÊ&~A ý ÊEû©‹VÉ â7¨„ *3­ßáÙåtýƒ»¬1Üáí`6Ùã*;OåÓç?,:†ÀŽ—;ó96»­d—îô*ìjäz §9’aºGh?†-ê¿ññ´ùëÜ¡¦ðLÄUܹ¾Êy>ÈyàÁ=ºÆpfý*±âá\v/GÜìçÿ ‹}Tí+Üù•N»Á{”•ã(.ÃHOÿ\V¥ ññó Ã]ùË0Ã#í‡XÝÊÅoäó›-»jŸN«1J?ãÞây÷Œ\ž‚³˜ ª(&…Ý>}öºXóg=ò!oñ°Å´`ýC„vÇÝ_þâÆã9£òÙØU›—à·Õ°_‚÷<[«L£˜WœÈo‘K›yfx7ó£}¬ˆLd>¢” ^à­”‹ÆåÉü7Ò`%ãr¸+M¢ˆ>môažíŸa>›¸N±Ù-£a:Ë…ü1Œa¹Ÿ÷wBà#$m€(m_ËA¡Ø‰×m3æLºeú¬ùÜÁ]€¹³{祇V/›á¨Ñ7n ó°ÛÒ¼?ÞZÊSù¿ò•1‹a*“^» Âq\ù©Öi3ØÔgOqN wô¦Õ[XN;¼ühÁmè(°Õ ¿Tþ ÚúbÚ]EþÄwû£³~Á3;ò +VZ¢¬ XѸ…… ž0Ö!.7g¤¥aÞ²Œãµ¹æ2¸3¿ÛÁ£ ²Ûu+©Áƒçyä(ó¼½7°ð¬ k#UÊ…ñ»ñ+3’óõ=†ÿy"¢mæÕÊìŠ[o.{~hRÔUymÔ­Ýñ3ÃØÆ8d…î<¶ˆùõ_™Oþ …bÜÓ“è!¥H6ªâYÚ Q.Ú úÖM03é%0_²ÛÎכʅa{ŠßoáŽo3ø?²¶•½»2‡fw WÕ™³í0Ê™„uS°÷;›½È~K*÷µl‡4‹ãZƒ°<p~e•ÿ¹g]Ëïx;V£§ÅÏLÃÌ >ü‰‹¶yñJNóg¤;_7Caà¸<ÒõÔ¼Çy qñY_Z,–k9™œÖHöw_¡îße¿ ~CjFÁ°ÞW;ÌüiÓ2¬6ËäJA†»FÞ#_¥CF.ï*s)ò¡ÃDÚ#nàüTšc±pÀ‚B¯;©ÓžøÞÒy÷ˆZƒ›-°Ú‰üMW*ý¼îy¯}à]è,Æ]ÌÔƒ.°?üEùµYßVq°rÀ¼ãb ¹ãðýS¸qçï3ŸŒ†;ø%ø‹a5.z)mZ¥™GÀLjVók¥ra…•‹JÒ&QøåwžªÂZMÃs¦âƒ¦žÿÐÞøYLlÈàÍl^ÊJ™y:óÐß‚o3¾¸7ºDá_…ÜA ý#À2Yû%‰R#½lªsQûÍeÇÉ™Z$h7s!0û*v>:b;Ô%ÌY¬GÿïL{!Êt N¶ž´ ®}…Õîˆ#“J¦ÔJiÑ/ªîþÄó[zÞ—ÅÙƒ sÃ)r—ÎgtðgÉÈÌÓíqÖ(ëfm æé§¥žq®‡áv 4-Æ¡³† Øék—åœI)4hCZÚ¾ÒoͼûÊC0º;ßöü·E›™¥£Ä5’§J÷>?kÆÞúòàáAãî¹søüŸ°nÎÉ™ž| ÛÉšŽò!Þyƨ¾»|}çõå¡ö;ò„0ঃg<™x`ç´y³§ÿ€ç@P[ð3`kPy?ÓiF˜šþòÌvñ Ã5„€ð†’÷­‰€(­‰¶¤%G Ø;Ù`ÏÀ+¸&€ùnØ~6KÙ×óˆþU<Ê¿“gnuB(¤È>¾áqnjÙÄ·I$QA@A@ðÀŠýûÃx—Šãë+Lc¶¯â,‚@›# ÊE›Wd@A@ð@TqoGíx•}ñ:µ¥óf=Âk?„A@hŸˆrÑ>ëEr%‚€ (ªÖfÝ"p‚€  ÈnQÁPK’GA@A@A å"*I²(‚€ ‚€ ¢\C-IA@A@ @@”‹ ¨$É¢ ‚€ ‚€  ˆr µ$yA@A@‚Q.‚ ’$‹‚€ ‚€ ‚@0 ÊE0Ô’äQA@A@D¹‚J’, ‚€ ‚€ Á€€(ÁPK’GA@A@A å"*I²(‚€ ‚€ ¢\C-IA@A@ @Àylrï˜;7Þ]è¸Àm1Fp$)dR‘i49ÂvÐ0É cœÍ ‹ÛÜd‰¶öÒÃç"Û[ Ø:ø[³Èu¾ÛB#LÓLíøí¶õ° ķО┶P¾gü&--ÞVaáoSú”–n÷ÒvÚv[ºº$>A ^:¢raÜ>ã±1¦i™ã*vmKlT˜3>*Âf·Ö‹FxYVæp啸ó Ëlf±Ë}ÛôY_†û‘—g>ºŽ‹g6³ˆ[`H|ïL›=Æí"´Ûs¸¢,±‘§ÝÛf¶ûv\ÚNÀø£Œ›þ0mœÝ>ËtÒYnƒ¿MéSZªO!i»k»ížoI;.J¹¸!--Ìî°>ër7w‰p:nuXßЉ ³wÜ*¬S2(PÖ‚â2Ú²7ÃúÚgåä—œ{Ë´™¯:ì®{ÿ™–VV'„‚m5H-Žï=Ï>ZžYð¬ÃaÞÒ‰Û-nql«k­ƒ>HÛ©®Ø@´Ëé—_1`Àˆ¿ëMñÒ§´hŸ"m7 m·:ryÚvm"tËô™Ÿ”Wf͸¨pŒx 94$æÃj{Ñ”‘ÖGô#‹¥]¯"µÌk·Û¤Ÿ6í¡O–lt‘Û½¦¬¼àâ>ùd&Çîï,†`[OU4ß{æÌIt”¸?%‹1^ÚmM ›Šm#øFÍÛÉ/ó/mÇw…5µíTňŽÃzÑu×¥¦öð_Ãj#ßæq¬›‰­ŠHÚîqNÚm] ›ƒmÝØ:–‹´úë³m|Ñ6dâÄØ”îý? -ßfM¬›­ŠHÚnMÀ x±˜ÄÕ§`¶uPôíÐ|Kä=ÏZð8i·¾qÅ›&`[„ÁÿÖ¶ã_%6²í(žÇ1‡ŸxÊÙÏXìÖÑòmúƹ‘Øêˆ¤íj$¸7ßb•ׂ@ë ìÊ…õª;ï`µÙ¯Ç´5>F!ßༀû„²/l}!ãý1øÞüÐŒq†aù´[`Örn ¶µ‚v¸ŸÒvW¥h;à‡açÿêšCÂÂ}Ñ”Q„ò@#°U‘HÛõ¥·7Å×[â&´Á¬\ ï!Q1 3±k,„F8/àüøòÖÛ†¡ôêÃ_|-Ö9Òn½BèÓÑOl}†ï /,Òv_“~´Åó8æÈä}gÈ·é?Æ~`«#“¶«‘hĽø6"Vñ*o‚e`Sl¹ØmSκ ›Åf;w…²ÁNQ¨a€“‹q~ÂÛŽa‚mÃPzõ᾿ºù¾D2Œ3¥Ýz…Ч£?Øú ÜA^HÛiZEúÑvÀÃÛÃ2Y¾Mÿqö[™´]ÿ1õôé/¾žaäYhk‚U¹@¾íݼÀ4É2¬_J[ãTéc{^àü€#_ží (°ÅV»‡Žæ¡5ÈérSY…£úªp¸j¼o á}a°´Ûr‡“Ò3s ÷Úä‰sYEÝ÷µý·Äï°m‰$Ús–`j;í ÈzÚŽâyœßˆcÇýÌ4 #ú”öÄëÁV7ƒÑvùPSÊ™zwón£ûÃÚjKØP»FêISÆô'«ÅS¦0bÑó¹ÜJŠìÃ#_|¹«¼´ ¶¥eôÍŠm´y_%ÇEѤÑhHïäª,¿å–Ð;_¯¢=‡²•cTD(è—J??mŒÚ!lÙ†ÝôÙ’ÇðS—Ø(ºüÌqÔ·{×îúѾŸîèv»ïp6ýï;œ›xœn¼èdЋޠ£Öî"§ÓEc÷¤ó'䉔ãþð„ŽqÁO[è»ÕÛ•gµZh`Dö;Š’»Dêaî?¿¬(,ÔN§Œ@gMZý%4€mK&Õã²¶FÛñVpNï/\MûåPÏnñ4‘Í+½}Ÿ:ìúé‹e›hDÿTºð”QÚ¹Mïõ´Åó8s¡ááCým6·/h<°lu·xÛ… ÿì;ßÒ ^ÉtÞÉ#T:ÙùôÁ·k);¯ˆz$ÅÑgŸ€s®ƒLÿýv íHÏ$ôçMACy­1´m_&½·p•”«`ݺÄÒ©cÐø¡½ÕïWþ·„bŽUG©yæ•çL ˆ0t³M'?ðmzäRm#Ý6¿ ª3౩qÑ‘mV0ù®QŠEiy¹yÔÜí;œ£Þ䄨+‰>_º‘6ïÎðæµÕÜâøÄràÇ ‚ëOMm‚íRxWlÙKSÇ" ø¼Ë „ÓYOÌR¼ùùOj$ý´q龫Τqƒ{ÑòM{iÅæ½:ÿêæêŸH—œ6š ŠKéóe5Žžð£>|YŽx»ÍÌ-¤|žÝ9 {õj·:ß¿_Ï W$ êÓ•Œ´nGzøðEZ´j; fïÖ_œÂXN¤Ý¬Ð}ôCM…eÔÀî ç+ÎO‰¬~ýÓV¥xÔ‰°êÁ¶Si—QY[£íx+ù—?n¡­{Ðøa½©œg©þýÅO^g³³XPð Jʨ˜ Úùh;Šçq>Ãm6{r û”æöí™úÀVW‹¶ÝË·Ðßþó->š¯È[Ü.ó¹_ÆàÝþ#Çè‹ÖªW‹Vm£u;Ò„a}x‹DƒÞúò'5Ë Ã5tÏb~úÆg?/›:Žî½r*ÙxÀå¿ß­­Áï0xƒ~çWçN  Cûжý™´zëþ†¢÷ë}øú‡xZ 6ñob!1ƪà¸ð0{›)È$‚ôIí¯oe!+·€ã£è×çžH6›…VnÙO˜ÑA0k+ rÇék市Ú[½ñÑ‘4qx_E·ÐŽ™,œ”RBLde®øÿNu:˜•Kã†ôR£íx‘š8’º°PbC¶„â¤øhu7•òᮩ§÷ §úñ5âÝns J¨Gb0¤7ñù/Rù©/Û¸G•ø2žÉAG¸‘Ûá–=4–g0<é»Õ;¸#µÑuLªža»ñÂIt;ó²rGµWŒÞév|ŒÓ„  @’lœj KäWÜU|/ðmÇ[n °ç¶ô³IÃiÏÁ£4ï‹i;äêº÷ óõòͬ¸&óû#žÎêyã®Cô_ø¸öœfæÖ‰Ü/mý‡V.ÂxÅql ¿Í†ú3ªY¯}®øÛVäjS{æ^°E›ÅwÙâmü~pï$:’“_ ÌX\8e$2v ú×lK'`Šþ¹gR¼šáèÇ3Øÿxí<¥^ACíƒ0ˆçb´ÒK‰®»ð$Z·ý ^•+^ ·pæ³ú›o\Îض”uT=ø"i!A ]!ŒÊ…î l&¹kJ”­ -„3t¶ë¶¨W¹€ÀŒ „©ÕâÒr6-ðn>ÕšE¨Âm8j% Ï­Ží9'§gÞYHyëkÊeAfÒÈ~5 àr$§75šŽ;ì^fçQdxp­³7Dóÿ»˜0-]Â#§lÒq= Æ­M¾ð5ɬ™ÙdìXA±¹›ûú—b·ÒIlÆrwºEeÊ*&2\¥¦fv<³€¹R^·2œ×2Átê–*å9!6‚®ãëX¯ÙɳFûÈÁ&Vh×PRâcpFc`É ¶Ç3ؤÛ*vÅ÷Z£íx+àuœTí¼jÛõÜ39¾ÚM?``õötºÿê³½*.n;ø&¡ô·Õj;šÿáÂïúmúÓ§Ç7æ‰S{çµ°–ø.[¼íž~Â`Å—¾gþ£ kP@š·E3ï*.­ <~7 {¢zUÉûòyF[SCí2“•ÌTôëQiZ ³ÓBæwà‡žÖÍY¹EÊ\í3汜‡ªÌ¦tZ͹ûÀ·9QJXA £r *™¯¼ *ÍŒf& –oU±ôNI +Ùîô›î|Ä&)}RºÐ‰Ãû(·¶üÇ °jD©Z±@vÚÛ›÷(¡w@Ï$Ú—‘Mw¢3¸Ñ2¦ÈéÎcçÁ,e_‹w˜š}Ç%xT4~h/V:BÕ(ûâu»”ý7ŵ&ùÄ·Úmïn](Ž;ÑI¬T,äuy›Ì'µ»Ü.¥ˆaVã€ÒpV¤BCmBãy]ÊJž]€ÒÅm£yE¥e¦Öe@ö¤86EÃlìÃ!ë`¡öÜ»~¡,Òõ$Oá'©K ½þé´‹…¢@+Uy¨­gÖ:Ú³úF[£íxkʰ`5'¿ˆíÉ'Ò˜A=”·”®qjD?"Y°Ú—‘CGx7oy½N‡ùg×*³ĔU8+w++w’ÅZ)ôyK¯ÜtÛñ¼C¹8yë \ù¦š-TçÀÁ8)¼X¡ó¤ áSÍçZ¥íFW)´Z-ds%L—Á÷òªÛª;üc¶ÕŸv‰õ’ظå(ÏLÀÄù7?ŸÌ&XÅôô¿¿ö¬æ©!ÕÊ žñÝÆæ-¥\T%Vßy‚@{@ • ঘ[sjæÕ°¬ÎC?ÞU—&Ø(C±€L ÖïLW£êCxAm›R%~•XÏH›`›Âë±IE›9mÙsX妶éØ`¶ã†+—BžêÔ3‘6ñzìСG«t1 ÔDó.!¥eÚÌñaD)žÍØZ•|ãðl¼Ë#Ì0¯ÀÂÂÕÛö«ŠÁ¬¼Er'‹…Úß­ÚA]¹“Ä.]y¶È“°sÔiã©ÅÙÿx)+q}”-³§ ‚ö×X`ÖÅ#ƒPAݽ˜Ëhÿ-v÷Žm‹EßN#·Ù&ô/èÆÖÏØH&$X¤Ú#)AýÖ‚"ŠÉ4½Í›2¤òw=j@%Á&¤ÿY°Š×òœ¤x öÛª÷ºm§’çq¿R52°ìÔ×ÜÆ'@ }ä…yAò@5U;#ížÖÅV!àm79!Vµ)ð!(´ÛöQ³bv^Ÿ1€û^X /Ð ¬1@ÓfÚ%Ú=¨À'óbñȰPZÂ3âz&I²„ûðCÌ@¥gæ©Mº·ä,®o|uä.´ ‚U¹hàù›‰í¼8„ލgrµ¹r¡rÒ>þÃfŸò3Ø&0­èÆ#ážd°Ô{ %Z–ñT3.ÌD`g¨/¬9¢Ž]‹@°“Ũù!|g!Œ”}À[/>ý¯o”¹Ù9' §pV°ôHR âa*ê›ÚUí R˜¤a×t X\ÅÝgr-fó*OÂZ \€6&"\­Aª­¬xú—çàD`;›4<LlaŒm‰5¡ xò4;{±¬àc¤W¨æöÂ}·$˜z^Â[’¿óõJzí£¥Šß]xê(`ê„Á¼Û]ïø´¼Š«^„í;Æão0¨uíù')žúÉ8ƒ èa†Š„&¬ãø/o… g3*lÅ|ÞÉØñ]Hè\£r¡%D}oóÃf'¸|öàÖûpûòÓ†îG}GV<Ÿ[%k˜Õ¹÷ª©Ê&{zûRây Õ;.;Mí5Žsâªã|ì“=m”[%óõ'¢±Ô÷ú}·Ð[¬ëù?^T‹Eï¼Ëï…³¹b9°¾ˆ2,Âö\Óâ™4Öb@!;—•,°Ç6³¨Œàizüž_êǶºkLõ½­òÑé¶i¼þÜF—qÚMç× Ó¯¬ pO˜˜€ð[»)‡@ük¨/Àw×ÐwÏÝWœ®}(®Î¼Äß9¨ù…uŽ2K)Û1¾„ÝÚY®½`´ö{ù]‰oäï¡N0)ÐkW¼Å#n‚€ Ð6ôŽ;f«qpª7òŽùõæ†Á_q{ó/n‚@gE@ÚtÖòK¹A@A@A …墅€”hA@A@ÎŽ€(½HùA@A@B@Ö\4H(•[PªNè„]º¦rv×»G`1™çÂ1l*Qîp¨mAul=ˆ…µøj™Êx|OÒ‹=Ýð¬1ö|¯±Åû›Jª\ó¦ý[9ütD³ryÆHu¾EG,£”©mh ßsò6Å8q¼P/°E]²‰O¢Ý¥Î;Bg9z`ºtê8uðØk.Q‡Àéh±…ݤQýè¤}ùªbzòÍ¯Ô žŸ:ZyYÇ{g¿ûÍjú5L5ºê`*¶3ݱ…ßœ¿Q£ÈèdÇñ.\—œ>Fír„—eåšùÚg,¬¸éæK¦TŸÑÉwýõßߨð¿8c¬Â?þüÏ/ÕyØ2ðŠ³Æ«÷ñßÂ•Ûø$ØÝêP((Vý»'ÒUçž Ê;À瀠M=aH““;"R¦–G 9|ÛùãfµËv1ýûË´•khg¤–/IûŠñÇM{[›zvËCÿ0r@÷jçµ|>È;¼wdx(Mç]¸ôà‰ð=ß|§Â—ñ9>§ó¶ÚB‚€ кtî¡ò&`½˜ÃÃù }ø|€«v"MÖGÎóÙ’ãN4Æ;³v›…>Y¼^ ÅMH®ÓÊ'K»«‚‹#L@IDATΙ@Ý“âh9w¾wªÆaÃîCJ±@çŠCj°ßË'Kƒ²r Õ,N›îÈÅì›[i+¹¿¿ú,ºˆ·Ý™žE_.Û¬Š™S¨>1Ù“ JÊÈét{:ÕxÆŒš&Ìy#_îð E#«BÁ€ð½ÀÖáÏ& W|ïÒ©cÉápñ€Ó*ªà»¦5Û¨Yœ(½ã@–v®¾ ß«Ë÷6ï>¬úæjªêã{P¢=ÆóÅß|¹# œf/$tvd梑-à{… ±NSŬöÕŽä}³+øÀ1M!V«rÇo ›ã¼½`¥êpZ­Pý$ñ[À„S·÷ÊæÑø²ê@k¹“ÅV€©]ci3ÓÇÖ8»N¸?#[ùߟqŒºwc%£ :|G|Øø˜êã¢Âù<Šhuh`.ˆ™œªü)+· —?øžî½r*å•Ò¾ZIy|~ÚðžÕÁl¶Ç^ý”†öM¥ô#ÇÔP#¤Ò[_üÄ'tóy¡!jÖí쇚×?ùQÅÌ­¼ý#N ¸ý—§²¹` ½ùÅr:”•§Lap ¢ŒwËkß î’·NîöJâÏãUb»ø Õõ;²ráT³á…<·I£úÓJ>ýy-Ÿ4=„pó$á{5ùQÝ}è¨2QÆz¿ùùdÚ—‘ã•ïí:Xy¸ÞäÑýé§MûèöKOå~ç(}΃3¨ô7—Ÿ9Žúò‰Þõñ= ‚}°h-•òlI<Ÿ¿ôësO¤^Ý*ëÔ³®äYè ÈÌE#j£F%Ì8pª±¶Fp4vQÕI øíàÑm|¢-˜ÍvÉb¢ÂÔ]þÕŒÂÅf ~ÜBØ—|X¿TÂðV6 ðŽà 'HoÝ—Q#Â^Ý”p[P\¦”ŒÞ) 5ÞwÄýùBõŲMôØ+ŸÒ??Y¦f `‚‚Ãï& Šý VÄ¢#ÂXÙØ¨N.¿þÂI4™MöVlÙÇÂKåh¨ƒg2Vño(Ìz&ÑkaþgЗžFc‡ô¢E«¶+¥dù†=J±˜Âq÷ï™È³EÙÕ³ K¹Í•ÑM¦q¬¸|Áu %Q(8¾øz[ÁB-øÞ§‹7ð Éa;¨gµ ã:V4°Žoüž4ˆ•-{+¡×3WÂ÷jò½‰ÃûPÌ‹ŽT†+_|Ïͳ«˜¥] #ùDm¬ZÀÊ ú»¯8CY|µ|‹‚»>¾÷é’„þæx çô7?ûQÕ›g=ɳ ÐY™‹FÔ4 ‚0ñÿì]`ÇÕž-Wtê½w0̓q÷–bƒíĽáà'î€-›â–?‰»Ávœ¸%n‰;n\Ó1½ $$@ýtuwÿï­4â$®Iº“tºy°Ú½ÝiûÍ›7óæ½™ G4sñòß›A:b&žfzi G³¿‚Â#@®Lä¾ãrûÍØ ‡°'d"êd‰÷êÌrkkÔžz¾É™Pâ²1ÐÞ×(š©:sÂ0¶ƒåöL©ø7}{ÃÎ…l+Ü&ÈŸ}¬´^%V ¢Î¹ô»X>Ö`œsÂ|m¶«‰%)k·îÅÀ¥f6t ” ZÏBD®ivš.h›wïgä6@– JŸÂq¥šfS9­‡Rb·0 ï©]¤¿yO‘9óÇÈsâ Ð\¹W§X ¹•¾%¿ÈÜp¢¬Òeº}îÓ¥.,­· ÉƒŽÙélPïÎì´Gj磠€prïH¹gƒ’@–‡N9éæ—¹Cɽáz˜0Ò:—c‡õ5¯ékÛë·²%«·²eU¦²@Bɽ=ûKÍI—®¦E½À\\_Q]Î V°n3Í4Å@2!èÊE‹ztÓ¬/¹D퀉šü2ù¢:Z­Báø-üe‰èË©·]:™)’ŒxÖ:~¢€,•NOݽJ̰eg:êîµàE8üÂ=‹[Ç ím|)ƒw­4gÕ‹J*Y ŒW£“%šÿ¿ïÌ3ý! QC×^˜=úïÒ*Ì$Õ, ­‹Ð²Á1 ½Ì¡I¥Û«ÁvX& ïg8ˆ7_z [²óOY/M­¶DŠR³›ñ#ýÓhú®–21áôÚ'K¡Œ”²Ic2ZO³«D|G4ŽÕF§ÝoÈíª”r :k@ºÇÞ%ðpë ‘1æhk®ÜËNO1³©{dY¤s+QHÞ‘ )ä³x–õ²³5Ý¢¨ýÌ}ùö6h=¸§¹nŒÜ ‰îþƒº"¨\Ѓ6"÷BãcÞµÜ#·R¢rȯ'þý”’ v,6b©ªö°ýØ0„(”Ü+­¨™8LÇ‚{’{t ÃÄXªãpÿo&Ðü?¡ñm~Ú"@ÌHDå‚7.Cóû«\./‰¬ð¦„˜ÁÅLÚ™ç°LƒÅÜ›0ˆ¥YÛK±Û'ƒµ (" ‰¶Bí µiw¡éö“Žù%Øá‡¶IíŠ]¥Z’7ÂyÖá«`¿yÙ Tktüû‰ U˜VŠýkÖdäcÛUÚ‰†ÜÏzu9¬DôÆõGX`O nõ0hÁ?áðÕ ½<–|KîaŸÃlO»B¿l)äÚd®…¨}ïìV63Ÿ]±åç_vÁm Û\øM‹®½£ RK0¸>• 0—dZ§AD C.4zïëÕ¦_8¹¬Ñ ”ò#E‚vF#KHÖ|åˆÜ6bEá°Um0³Æšw¢}Or­kªÜëÞ©¦îiÇ(ŸßoòfÁÁrÓrmþ± „wø—ð5ü~Ÿ3–m³±å¦öC}W(ø¦çAAÏH­üþ·CZÔítÕ_8ÜÚr/¶õú–Xón$¹G'.È6ZÀMÛsG’{¼®ÂRA2‘Ö`ôÅäÔÇpUS°þ(”ÜëÑ9Ëœ8tÂka"Ö¬‘;ô¡ò*v<äq¬( ¾±ÊB¤#ˆ‰¨\Ô½¼×ë9TZUÍXÝýx^œ:nÜv|ì§uÛÍåE³í£¢ÜFöRlúÏb_Áwˆvù¸ìœc[ü „ág"ÈŸÖÀ¶a1Rí6óV”Ší°eRØŠ¡ï^ÜsÅ™æz?¶¤Ë-M„áFø!oË€2´ ¶$ÄîyO»FøÛº¢ ̉päæßÔÝ Œxýà´ ê´ÄE$|]NçÖò*K¾·%:(MZ¸ý.Í„ÞuÅæxrë#“ýýמk* ä¶çàßÙ7ÖÇê4((d ¡UÚ}ê÷gÕ H®ÇÁ ‹vM£MõsÖ§ÖzD.l´; ¹(Ð8ÛGsñ€ms“oëñxðN4/Ý\¹G;MÑ¿†ç°'–<Mù)LÞá2Ð(/9´·%ûÚŽ@úýÙd¯±„ß}å™ÌkÚþœN².ðº%å^ly1ã»áäMŒž#Eˆ,©ÁäÞXsq#‹)#äEÖ"ü¬£prÜFé(ÅäK:\¬b©XDo]Å…@ - ;µºåÞ†wúúŸ~øQ’ ƒ·µ4Ñà…¦v’äBÒŠáDxn„~Ò :Ç”Ÿ[[*c"S$|7.ûiq¼ø–”ì@Å"GR,8Ñz!šQT,ø³†gâñ†_ &ëÝëŸ.5géèC…ôQÃQð$Z{ÔÔö˜NàulƒµÇk³mÆ“w¢­9riæ×ã”ÙhòŒU˜¼Ãåµ ËZ¯¶«÷h‹é„À–x–(î¼JîÏÑ—á95FîeBAT,(häõë±T,(ßøRA6…@"* ÙìÙY欨XûÍÊ-ØÄ†Ë±6…o›+ áDxn„ H_j"<9 l9M8Gƒï¦u+K<Õ®e‰Ì·´‹×½W…І²‹&ÅnU§1Ú7ž¶ñ̾-¤­·Þi #ðŽ)óP.í`QaEeié¦Dn›-olyqÚï ¹Ç«SœáHDå‚wdëô®ùqÉë%åÕòR|ÉYPd'ÂkÍß¾Føá ¹‚!° aØÑâ»iõŠgoi·Ú±†Ü×ZbçŸ؆­—vð°®m¶ÞiéúÃ;\þ™ý Êå[ñÝ×ï%zÛlI|Ã`K„áµ~ùÒM„‘ð4M×µ×[ÑXj ¾K¿úlmÙÁâÏßF‡rØF—P↪“{‚wW‰x‡|M™‡”Ý[Ö®ÜQ´w÷RÑ6#ã[îN x72”ACD‰oиâ¦@ 5HDå‚ðâÊíQê\øæ«/VWTîyéƒ4jŒ‚ŽD€p!|'‹pÃAøqå‚GØr$qn ¾Ÿüç_uU9w¾ t#° ŸPâ?­k›‚w¢«Ì(y§W¤Jr±ú³·^{ÓY^¾O´ÍÐ8G‰-O cÁ»’ðçFâ>1ñT ÐÂ(-œ_£²{ò©¿£+¿]üï I1¢•Z¿Ï§býÀæ}_³cºÃf•ºcÚõ!Ù‰üaZ·ƒ½þÙ2½¢¬lï§o½ö·Š’ZOÛÐÒš êLIÁà³L¸4¿"°%$"Psðõy<Ê‚üuÝû ³z{a¦àÛú`7[AnÔϤ þŠP~Sî Þ _qMàºþ)[±mªºwç¶­=û$ú”úX7[ž€à]ŽD˜sSñ 7Âä( b@¢*4&ÍzKue¥±yݪ5]º÷ÎÉ/u÷Xµe®bÿLÚJΰKÂ&ÑÖuôq¿7>[¦­Ú¼W*Êß³ôýW_|ŠÅ~@Š8È5Šü9 l9aα·²¼\ß²~õò.=útÉ/qõJv¾%È›­Yc‰ÞɆ)½¶)xçÈÚDÞ©‡+RµÐmMkW­íÔ½WÖÞRO·do›MÄ–WR=ŒïrXŸ›‰oÂOªFB\µïÑ–xoCbWâ M¨žêjùý-ø×€á#{Ò©ç•V8þoñj)#ͦg§9¤”+Íœ´k¢¯xÒÇvh¯vÚR±üСm+¾ûê£më×nÅ‹—â ¥‚|Ç·†Šn™$°åH48Ç_WU•úÞËÏ?=øèÑߌ>aÒ…ïU8'ßÌ1¶Aµ»ŸõÚ¦àšúïÔéRß({].öá+/¾Ùè🎙xÚYèS$[ÛŒ¶¼ÖÃXðnÌx—ã+Î6ƒ@"+´ÜyÈ­‡¬\qÐ1öáØ‘››9tÔ¸aY;v±§82T‹ÕaÚ:¸]æ†ü>oµÛU]QvàÀþ «—ÿRqèP9Þ•0"+)qºCñ=p»Ž¶uP\ÄßÍkV­Á±337·Ã°ÑãGftèÐøÖb±¥’Ѿ}üb‡m@…µËË mSðN³å^P\ÁAtß·}Ãz:dää¤=nHVÇNÔ§¤‹>%ê>…cPŒï6›w [A6…@"+$-£Ùw>ð"áÅgGÒ1°®þiѧ4 6g¡jÃñ°øÙîˆLÏtpHùªÆAV R2ÈbAŠ·Z~¡H`{$2-‚/¬M®¾\H|kZäp&Å™øVðnt¼ ¨Ú5…l›‚wLùßT¹WpS:ÜJѧ|Fm3Ú$5¢XʴܺÑ^iáàÃAŠY-H™ Ëé7w‡¢8¡H`[o}øN]\ ZDW.º@aÅ• R6h ‚ƒ” š&å‚>ûËϸ•ðDÑ™0à8AXõ‚úMÊ=çqp’¶5Ðp¬¾!Y¥Éâ…m“ ” EÛ<,ÃbÙ.®5 žíR`ÞMÑ%Š™ ´å‚ê‰ +îäÁ=š¡·á «½g å?ë” ºNt ìRÈ¢CÖ Â‚ º¦{ô¼1”ìØV߯pLãÂÆÛÆ•$ñB'{ÛŒï$;®Ôâ…-oeÉŽq¼ñå8‹³@ Uh/ÊÎ|Mjšµç‹dð‘%EW0¸ƒ[*¸@CFS²cK€ |Í6QGˆ'¶Q"A&{ÛŒï$;®Ôâ…-ojÉŽq¼ñå8‹³@ EhoÊW×X¯ÍËK—=Ò¹L–†ÃPÑ•FöÜiOîPõ˜E2ðî’„EÛF!ÓõºÍøø¥¼¼X~²¼[dLV¾~%ÓÀëzåKðôîœ8üÌï7÷ÌÓ£7™ðm l›[7m=¾É;xøáL½Òw®N2Ï`Ýڻ̣J‰³Ü l“Ú¥þsNªš~S¤á’$wiïøÆ[Þ¦ïÆ¯Ïæ‹³@ EhÊ…t㬇F~6ÏðI§²$§»«ý™—lÑ[ÝVÈÌc³iå¶½ÒîPŸ¡O»÷¡/$•Ý÷üìûW£8ƒ¸æ–ŽwºÍMGÄŽ€À78.ân¦çÍ¥{µyZ•ÿ C’åtWòÈ<‚#ŽrÏœ@¹æ®c,ªu°= 38rŠTîOQ+d‹ál÷}ŠOJÕ\þ Ý%gÖô)3ЧȱëSˆw}>cž¼ he¯*û½EöJ¦×Ano_·¬Ó¬>M·útUFŸ}ÝŒÙ_¨²>;Q3ÛàUÌ˰Úäž>Ý—#©J¦¬³ LÓzZdö’uÃ%©’#jM2ÌW”Vѹø©§n%O–XŽË+w»R.®Ê˳[|Ê“š.]—í®ÔÆïÜ¢ (*di­»H2ÝÀªl)l[ç®Ê²¾ƒN+u¤ŸyýŒÙ/ú,Ú­ÿÌË£EÝ‚v€À-O>ióU<‰ÁÙõÙ.g²Ê<ªÉxÈ=yÒ”)ކÿ]’•kRäCÚ@Û÷Ju³K•I×§¸t¶ß?DÙê9á4§žÛì>%w]VI+ÈNSJR­ ÊE2a[Ç»V¿Îrœ^¥[©ó´¯Ñl|T¼™ŠÄµ÷Ì:šÉê$¨£™,†ÃI_‰©¤¨¦> 8ºS`N¦–OÎïDxP£õ+ÌQa@a;+ãf<ÙˆGK5¿ôý?›µ¿…ÂAxÅÚ‹r!]uç­ö!”×Ñ“7®’Fí٠埼K’“ P±Q{v°‘ù»ÔÕ½ú²EGºÆê‘F§ þùøãE@E4ªäd ñÖí[æÍëèÝ_ú:Þ±§m\ô2ª5FrÆ,ÊùW\Ñ­K÷~ÿ•6j„íC©ågU’’·OBÅúX–³Þê u—ï¶ÎsÎ5VoÓúâ]WqÅG˜y»«cª´?3EÅuR”*¶?ÓΊ2ìj—rësÀyêW’¥Ï–/¿ùŽÞ¶ô´[uEþlHÀºË¦jUVÕâ²*Ì|¼ªÂ|ŠÌ4(~œ *¸†bÙ0˜‚CÖ fÑufñÌêפ¿ÞÉîÓ:¦ºýl>íêëµ3gï–tý5™yžY0oÞþÚdp Jœ;¥¼¼ôžëS¦ç9ÒÓôq2àG¢GA Ç«nH?[FÔ}>f¿~ÆÊë" ¿.±*l¡³ªÎ.„ß«3c½!+R+rÖ%³;V¢+fGУ[¿§Áƒèd…b†J¤pa¢¾6á”ф݀ƒ #(bâ¦@ Í" ¹ö—=­HÒ¡X„¯£FÊ=³?AŠ)ÇžtúŠ* ÅBŠEpŒ à£~W}}cú“w Y#‹à¸ò»¤t#ux~ycðåÑ[ã,M™ò–œ=`Ã@(= Ýè‰54= dcƒ ­§ÁØ_…{’†x)Î%>¯·LQÔÓ0Ÿ²''UÞ›“bZ#âYx¿"±’T›yì씦öÂæók8“¶,¹3Ê}×Î|h%´ ðâ¼¼u¸Ÿ4ã­DW.”K¦ß:NQ-WNÞ°R‹ðM’ð™¼qµòÅÐ1W·þóì“˃ A@‚ pÝ=³Æ`2åZ!󢫰FÈ=rÓ¶ŸséeÇZí)¿#W(¡X„ǘðaûDYcœUŸÂyw'\¡„Å"<¶ô”0‚ۘүؾ‘SŒiiÒ¤<¥ß6I’¥Ó0Ü>Af[ÆÂ1)…´t ­ ƒq¿W󑄸‰;-•0¸+ÁmI±ÙáÛÚÞ1퇵¦¥ JÛÝ!•e9}šCbW•¥Zå2‡E‚Õ„A©PîÆŒ‘I~(ä–åpûYºÇoɪöËtz‘ iöu3gïašñœÆ\/½üðö]+®¥ë=R~ĔִÔÌÙÙÕ•­±ˆAr"N^GbÁÄBsgʼ,!óUkQÈ=³?A¢©»÷š•ŠÅÛ´Æ¢Q™$i`àĶy׌ôˆ} \é-óÜX¼Mk,’®F¿6°b]˪µ”ôì¶Ðg›zÃåwÜ7Ôb±^/) ÖG°~Yñ•§ZÔ*›b*4ÓïU¦Ëáç´F»>™VFÃk{gÁ¢‚Ñ>½]3‰\³*¡œÐQ•"Óûeº¼¬C•·gÇ ×\…9¼ö¾‡^sû«xý±ÇÈß²]*¦ÖØL,[+ºzâiçv‘­Ö‰Ø*©o7¦ÈÙÄ ¸~ˆ+„|ca­ˆÀ¥×ÝÖsc“…Ìk\%D!÷HÚÝÃb³Ÿ€]¡’zñvcÐ¥5„—¬†ïSˆwá3¹ ;5éo7_ðf Öì³iØ­\|ÓŸ^{oÞ«v[ÊÃbùCQVJ‡µ½²ÙÒ9–M]Ó¥½9Vî°0·ÅT,"¾&YÈݨµ‰F÷±P,‚½,VŠ]жvN“–÷Ë•÷ä¦Z U¾ÂaÍØrí½Íš2%¶Óm}‚¾÷U¹ r[ºx.í9> ¸ $_TÚž—p#üG‰ÊÉWyâ“Ùš•~žyMc0rÏìOªcàè1ga.Uê¢nlZ&I‹¶çЧ˜¼‹1”|‹u5Ú¢—°k¥>›Ú‡zõ]³nÊÈÊY¯[,SwtJ“—õ˱ì€;S¥]ÌOF[›´Ó)`?÷ÉU÷eÙSdEz }²ø²?þ‘&zÛ•‚‘¨ƒJòm³ªVË0| OKs‹O7DËÜ޶k$Ü?Âá)H hÛ(ðm.d^Ó*)ŒÜ3û¤ê°¥¤…äiv©ªi™$i,Ú¦–p Ó§˜¼ëQ%X‚‡mSKØ…Á·q FZ6i’ãš»xMµXŸØŸe·,ï—£ÒnV´ ¬ ¦!@Öš]PÌÖõÈ’ EoKÍ]:eúôÎH­Ý4ŽD}³3À¢ÆnøòvÓª7¹cn„PÊEr³‚xûÄA€„ÌkF}…{\¹HQUKg‡R!ú”&`œÜÂô)&ïúðåí&$-¢Â. ¾ñÀÈlã8_±X.ÚÜ5ƒmï”ÆÈ•IPl …ãkze©XøÞ-=«Ë† Ff vp"6tž+Yv'ß!6œÙŒT7ª,$Á•‹vÁÐÍ€DD´ejåž”-d^Ó«)ˆÜ£þƒ+vYV2Uæ}J ¶·}Jïzåè¿[Є"´ë(„]|ãñÞf›¸ìö{oÇZßa½€|0Ý|’>MZŸ²µkº‚­Å'ŽœtÖDÒ.üÌQˆòÎûëÔ5Züˆ‘ ÇDä…&¾¹ˆ&H8L¹g0C´ÓfV]¹Çå­x&ðm¾ °åX Þm¦QCà$פ ª£O=µ“Íá¸v«*ΰÇ"]‘FhÁ·W‘ý»vðlë`yãñÊmöv°Â‡LÚl  `F ~B±H€ºE|VȼærB¹gö'H—d¡èSšplyj‚w9Í8‡Á·©•ÚmÈÐ1×K’dÏÏu@܈=ø Ÿä÷z}HY(±‡7ªIøÓ‘¨ŠQT/Ù‚GŽi f+²€ÙFa¹ ³ æ#Àå^à™U‚šÇ”óªàÝæc˜BC|Ÿ5÷šêŠ·)X<>¾*Å"ÑbrAñE C¥Çü¨à¡âýë‘S»P.Õ·ËV´÷]|«¼§^ƒ_ –íüUÅë ÚBÞÅ¢”{\Jµ3ñÈ%9Ó8[Žƒà]ŽDsΡñmNªqI“ q¡]VÔînìñøP\Çl§— Ø_©WVTüøÙ›¯ýŒ¸òûÌZ0ŤcÉ‘Â${|&Iv;“;ä´`5Ь@ð¥\)® j|ç’’Uî©–T¦XRB€›Û’¤0kjW|é ¾u›ÒÆ.6—Âçãb—`ˆ”²m–n‰>!²oK·I $ l^·«ÀáÕµ–*>ð sIòm>õ/®bC÷U•%‡V~úÆ+k|ÏÛübwµ;• >ÂÏâý¬Wžfi÷ÿ9|0¹cÚ•Ì2vdøpAžZDzßy‘e<:3ÈÓØÞRöeÙo½ÀÔ!›“0Ç‘Ÿ›“–ˆ+Ä&·OËÑÃXöÛ/2Ûi'‡/YrÊ=Þ¾MÂxô”¯ØàSž -žv<•uq]Äp ¨¶L6vÊ"6ö¢/˜#k@ÃÇ1ûÖq~îlÌE_²¾ãïe¶´MM›ãØðÜèôn2€}6ù8–AÁ–™Îþ8¤?#%¡±tÿÈÁìÃIØïú4ù}#f™cµ²¿ŒÎ>>e{lÌ0vL.mÔØdj6®ar¦´i„¯íÍÿ9ÕãS2\´ þ´èÜÉìÉ㉘ÑÔþ½Ùu¨ë– ‹,³{F c'té³ìÃ`Š«Œ±;KŽ%NÏö kß~ûÙ¿=Vv¨¨™ÐGÛp¡\Ä ñVNH²J™ýWg2uPFçÒêAÂ3„¤\ÎjV9ë±zo$¥ßmfû‚‘”šŠ®ŽËú!äNXêm׳ô¹÷1) á’HÛ¯„ø%4tœÖŒPr/œ,2㇒{$/äj`9CÊ=’g!(Ñ䞬Ð.߇)»×dÖ¡ß¹‡oàJV,a­²jcéF3KJ¶íû™ÌU¾½.¾b ¾ÐVV©O9²ïˆ”×(Hk+Úüo–ÓçLÖû˜?ÕåÕÖ.°'+SÉôí»˜Ü£+Sºwe¾ÕëY峌Ç0A±O9Ÿé•UÌóÑ,õÖë˜ítlA à]¹Ž9}Š©ƒ°´Ybî>eö³'3ï7?0ëI dÈ,mÆm¬ü&Ìþœs\}©©”è…E¬êoó™ý&¦ÌR︉)P´üæzímæýn)“;wdé3þÈ”}™/»þós¿õÁ•¤—W2ÿ/›™åØ1G<7@0ÔG±ôú‰YOgŽAÝo¾Ï\ï|t„Üó-_TYÆŒ +÷*n¿Ÿ¥Ýs+³Œ #¿Îü›¶³Ê™ó˜PBÉ=ë‰Ç²Ô[®eR:>ÒUt€U=ú¤¯á;´u¹×gü=,·çdVq`ËÅÝ]¹—mûî–Ýã$–Ùe<ð–ÙQ§Ïg¿˜ÆúŸÇ: ø 3+-ømýæ¦ùªØ¸‹—°’½‹YZ‡‘¬ÒLç,†>ÇÜÉ\eÛpë5æO¦Râ®Ì7•ŽÊ¢Œ,O~œeu;Žù=ìÐÎ…lÇÒ9fÜPyq|m©]w5Ûõóã¬41³9º²Nƒ.2•]k™™k^–PçQ9™æ¬ÿW…ÙÄιæÜÛ+;òÙÏ‡ÊØ­ƒk&Ÿ?’]óã*6–Œ;‡ `,îwyØýk7² e•æ½£³2Ù/å“ÁüºÁìx×1‡Muvgßb³GÅz8ì¬Òçgïì)`/A! ºª/vyßžÔ½³u¥•löºÍ¬Øía§tî4¯À÷ÈwºØr”sá¾"ÖÁfeW"­L”­Äë ñºÚaÓÞýß*™™ºâHÑ¥€AóOš‘M }e~AÄ„B0Ð^5§KÖ**d_I‰¼¤|¿zœµ‡4r·aìê”*h¡-i`r÷ÎlÕ¡Rv&Æg{«ªÙ½ËV³»vdã¡ ’’÷üIãÙß-cgôèÂî;‚eÂ:TPíbwü´’­Öß]p:[Œ1×ÈœlöÅÞB–¼ƒ¥¹®¤Œuƒ»üߎˆfg²j¿Æ^ظ•½¹õ¯IÇ™H] þ*C]ý§–BÀÕm¾kñs¿\ÙŠ|ÒÝ˪övO\||·î—sÚÙoîX·ñ¶ï¾íŠ*±6(¸ Þ† ˢɰ&¨£G0Ï'_âXĨӴŽÁœÏ¼dfãY¼„ù–,ƒÛÑf;ëTæú×›èôžbÖÑÙ㊩è'Ð-@PØu6ó~¿Œy.bþ ›™^VΜ?cÎÚ9~óý¸œUüqÓ=>–råÅfÚ)W^b>wBÙ0 (¤\û{sÑ~Á™LÊÍfU<Ƽ_|ËW]‚yH½×Ö‹2×?ÿÃ<ßþTï¾ø!Â!€©2Lt@îA¦9Ÿ~‰é…˜}꯰? >×@î…’E‘äž 9j{4s>õ«œû7S¡°}ªY¬PrÏqýe[Yå½s™DÚL¸®’e%€AšÖJ•ƶ?éY—!—°â-ﲪC˜§ª€íZö(K‡âÐyàElÏÊ¿³-ßÞa*½ÆÜf¾­¤ØL¥Ãï.a%»?gëjú£ËFü|ÖsÔÍP±u_Ât¿ŠFM¼ýÎ3ÓÙ·îEV´õ]Öù¦w6/¯Ç¹Ÿ­úß9¦b¡X3Xv¯S™³d^£m(TNšé'‹Â°¬tö8x¥ Úmö ]nööî}æ«üeÃ6 Ø}ìæ!ýØz(·-_g* jH-Pîú¤9à–”Í>Ú»ŸÍ‚@´xÿAö~~!»¤Owü2ØôekØgEìò~=XgŒH!¸n@o¶}õ¼u[Ù@X!¸U¨¼Ì„kÿ¼¼}©XÌ1˜MEŸBÉh¬bAIÁWÆŠvÑPtƶÔ]Mfv:ï5ö;t•U¹³lµvTRRrývkæ÷vËÅ%,lÔÎÖ£¤š¥xãkɰBNuÅ€_ƒò÷ÀÏkYg({Ãê¿;óÙ†ÒrÔ}5{lÍÝ;¡ˆ¬>XÆ®ûf)«ÀëÉãÆšuMÖŠßôéÉJ<^¶´ø •&%ò;X':¡ž§c¼÷þ®|vÛˆ!ll‡\6{Õ:3ö샂²ß¼ŽÕ/ð¨9qc­]ç¡tëRîYà?MrUÛ±Û”•†×ø!(˜%Ö¶×Ì7óu[6º¡³m?ÜÏ ÝǺ½ë$1Ou‘i•áÊä*ßÁz»ÏýÌ–Ú GWæw—³¬î'Ö•³¼ð'¶áóëÌß*\•ˆÜ;™áH©È‹UסW²”ÌÞ°RTšÏÉbRy` –'L ÉÁí"| Öy\6/3ríGÖ@6øÔ'™תÝË |Ôf®ÿµcû Ê@wôÓ0(L‡Kr!¬üD;пÒÀ¿36mÙVQÅN†…à E/×–ÊúÃuŠÓU?¬4•š'*…BrƒÐÿƒrr"fÆÏèÖ‰M@ŸL®W]V_($D®ÝÄ"Ü:X>TÄ E'T^[+œfœÀ?” 3Ö”-^¦ëÆJë…5°nû:L7tÔEWj-š"•ÖÝlìzIÁ›Yðæ6« 7pÙïö(«í•v—«½3:X{x³ÕÞ’&I†Ëª°‚l,±ß$GÇK’báƒ5å2xr ‚5ª eÆW`·õ;kWº@)Ø…ãTX:¨®;à÷ Ô ÑOEÙõß.5¯Ï†+U°4éá°ŽT nNìÒ‰¥ÂÂADV’6n3¯@=+U¼HG5Ò‡ ËR­ÊÈüòîöôŒ7 8ûÄmÛ’i+!×_$½raÀŒf8‘«’ Z?pДܓàJD_’{tx¿_Î ¸±$"éšýDÍŒ¿Áް6€¿=­;&•uæqƒò¿à—À QÍã~aÝuàYBFžÿ®¹ö¢hË[ÌbÏò2Ð ¢âš¬œ CèDc‘òâá3»k.LwWîa›¾¼‘¹*vóGmêì¬Ó@1uÅà’¨Äç3]¢È-ê[¸;•aV›¨ZÓLÅÂüÑà¹D‘KÕëP³«á¾DDë$ˆ|µyb8À4\GÊËŒ„?b`»ƒág6ï`›**Ùƒ#‡°ãኵ–ÃÆ£Ú³rûÃ]8Å8¨Ô™–®Ÿ9Æ^˜3‹¾òÜ¢N3›8¨qæÒyû Ø ý†ŽèsÒÙ\eµÛsR¼~Œ‹ãóí/Æd¤Xih+\4oÔþéî¨Qúy<¬®gt,‚…¡Ä]ãnVèªï],MZWAÖ)²ˆP|Æ\ìÿÊ*ÌßyµÄ5¾ÒͶtNSFä—3îŒ!§@¹ Yëº:n‰2Ä*„6»Ä „#Ò©•YRXàB mÚjº0N7ó­YÏÔ£‡a¥Xf|DÜ€2ÖqÐBn÷Ÿc=Ç/æ: z¬åïƒ;B³c=ù@§ÞyËøË¦u„ ¥Kgæ_¹†é˜±bM…M]@@ ˆ+rÊESe‘2 ºC?«ÆÄˆ ¿v>»Jîù·í2×fÐz Yq1Á¢ŽÊôŠŠ¸¾n‹'ŽÁ¨EMcV,Î& ¹Pi^'#+EF—c™Rk¡0ËU[ Ëhƒ¥‚rn|•!^jôEµTº÷k–ÖñhÓ¥ªëQ—±Q¿þ€¥çœŇ4ð¤GM¥¨hó›HgëØÿWa›ó|ÛÒ¹Üg¶Àb@Š­iøíV6ë5Ê lD¢!ilMi{k-‚‰È8°äÀ!óúæA}ÙHLþó¸ÑæŒÍåUQåu.fÕïÄ"nÚÕjÖ|‘‹U%2§Ð4=^œ4 â´ÌœÊ ®ºáœÉ¿™òGæpdîì”Æ–è ÓŒ{K>$j®Ÿ )T×YXoñUÁ~Óµi¬B¥°.™¢–—˜H§'6ÑY‚õ^%PT&ÂrU£ØÔ$Ðy‘µ¤%¨2Å ±éŠE¥µ¤è%ä8=é-Á˜…R{±˜Ñ~ú$f3×{Ÿ2 þ”k.f0 ¹{Á+LKA8ò¯ÛÈü[w°´;§3ýP Ó¶îd,ä– `ª_—¥av/ãÑYÌ@Cpþíy³C¦5¬¿ÈxúÓçØKë> ÜñD ¡Ü %‹h]Z8ò.ú†ÙNž`n“M›d蘔»u1£“{:ü™O¼ÀìÏ2_ü+6²ð˜ëAHAiOTº÷[–Þq$ÜŽžaë^†­^{²Þco‡R‘KÄN¸!AæG Šý?³ªƒ¿@x„y«‹™×Ç0՞Ŋ±Î‚‘÷?a¶™Êmï™ ÅË —FÌ+5g°¹#EìwÜu¥XñÎ2¸sÕŸý­{؆.h±ö^x!<Žm^/úv{ä—­ì÷}{°ÿœ4޹a©xîN>27D ZÀM ·iËØ%Å5V îPX>Þ[dî(u6”:vÃýùpÙ#«H4y=³e'› «Èü £ÌEàÏoÝÅ áâÓF‰€¢ÆÇ hL8ãì>CÆŒû»ªZ:ïÎM• ²Lo¥!ïw°öÐ"í§±û’Eß›®S×Àµòã³O1ëú¡ëê,Ñâ;ëçuìϰ&½{úI¦…„ÖX¬ÁBoR\(¿ß‚—hQùË›·G›d“é°&Ë“««œeH„¶œ#­8„‰¹ÉÙÄ="tò¶K!Ì|dŸ$³A§ßßv×Ë#ª«&\´âû¸¼m£ˆ/¶šƒ~3¸È`jÚͤ1D3q:v= ów Ѷ¶´ŽCÛ_t8ÚäBe«v«Š½3ö¶Î‘öÓëO7±Æ#O‘¦@ %!7Z"ë˜ä¢ü¦Ü»òÎïUVzJ¼d½@C¹×$YˇŒ™_½”úÇúVîa?ySVÆI±"÷hES¯pô¼ø¦ÛØáÀ¨ã¯×/tÌ~I¦5€vf"¢ía-)Í…Þɂܣn‡§Hy‘BQP»Ö©Â×°XÒ¤óÏOËéÖëßÕvÕ¶®G–ªaÓ™¶@„e bAeÚkRs‰¯Õh˜NK}¨œ¶§íu J/->ð! 8àmô† Dø-”‹‰Ç€@@ ’²ZXz v5ô‰Î›ºfÈmE±h¯u`Ñ 6,¿s¾òï~ð Þ“Ö¾ÕCW«ÃD ”È” ·!À~m¨2DQH8ÏÛˆoÔBÊ=ÉB>k‚&`ž¡ñ¼‹ê o,R?œù>áû‚ʱU6UwãÃo‚⇀¾XÃòËt«Ûëþáã÷,Ü»›|iU:­}i©:é &¢å‚mh~•Ûf#‘•èJRL+5šÄ7ÂaëðŒ&ž#´ f;Õ ½\ȼ¦ãDîñ!/á‹ÝÁ}NK}J &ÜBô)u¼k#iÑ_7_Â.¾MH-lªZ'c÷TW—¥¤8ðE 4Ž„tÎ ûžmâa†Ëdž”ëÌíuþôÅÇm]¿–¾ìH;uÑN ¹˜›€MèFîõz•[í¦à¢—=„á} R hm0¸(®°¥ðqk'áò$÷q8žÑMÞµù ÅêúE 0Ñ\f6͇o4É4& M<«Ë¾^´³hßž÷û;.å|gÚÆ$#Â6D@®¡„åØ%Z×ÒjßîÍ¿|øîsOÍ-Ú·ÚMø’rA– œÖ]$$%¢å‚ .}ýO?üˆoÛ:wMHð[«Ð„áFø¡ $é9¦­U$‘¯@@ ³n\öÓb!óÂêi¹Çåµ ËZEøî÷ •Œ¸Â+LŸRÇ»ènŒgí΂¤#nG ³í³©ÎÌ6ññ«ÿx£x_þ’þE•lhA…aó%ìx78¸-t—Ä%ÕlÜöC°4*ö¬ÿøõ—üâ¿ãó¹im•Gí!M[_%¬KÊž°nQ&ÓìÙYæ,+_»´Ï@MÇFEF€p"¼7Â1HRž‚¶€¾iÝÊO•s™y«¨rÏìO¢v°¨°¢²äЦ-îã5Ã}J4(N„—³› ÐWº]š¦9ßÿçü—W.YübZYUõ˜%Æ€ý•ÌáJFM …þ›âÕL+Å,Ö·ãÑë€S+Íßóóço½6ç¿/<óTáîÛ›‹ýµg²ZÐZXÂ>¡NÄݼ# UôÞ5Ë~xÝqƹ­îÕ—Ù½·…C€p*KÍ×|þákÇw# & F8àÄ3@ë"P'÷6­[ý¬õ¸“þ%d^ôFîQ?BòÏìOpö­øþ»÷&žÿÛ{wùŽa}­Ë¢Ï$ICNÕFyÍÒ}Jïn]³êÙ‘àÝ.ðá/ÌîÌѰ a•â5ä—~×R}6)4kNn9äûOeYñÍW?nY½j˸SN;¹çà£&v®p§;­ª~0Ã&J³1Wœ?®—êñ³Œj+δ³ÖÜ7·ÊÃì>yT™ù Ý%F®N P³Âª“ …ëc2ª=†Šm;½¢¬tç†[†·ÈÊòRú¢2sâ ×'—&yé ¥‚,4.KhÅåg‰¨\p­š*À½~ùŽ8ú»EG}b×òR¹kYðÖÑË&;få0à¤,Ü»dýò¥›€ ÎÈ„« €@ m"P'÷–~õÙÚ^†¶|†y‘++‚Üã_³?Ajî-kWî2zìÒu=ÏŸ¥ì“³•à:œsûQªugëRÇ3ªìª\i§M‰…B Ýíc}ðQµƒ…ûZ²ÏæõÅ• >NÔ*ËK½_½÷öǪj[<ö䉣{ô4ºgÇNƒztZÜŠ¬U¤Ù°V‘*S,¬Ú¢2½Æ?r¿J#…Âíg¹•nÝæ«I-Ëå3ðÝ ì µøÝÏ©òbg§ C7 ]–¤ ûóºÇë­t;ö—ÜS¸g×öí¿¬ß Ü¸5‚isÅ‚+t&¥‚ðNhW(”¿Ž8ÓÔÝhK´ç8þc!Òê¨30+jᛯ¾8åÚé½ÞsBÏ©+¿W„‚qd-R |´êªª| !ˆÁ ?®\IÜ$(‰ü½‚häÞ'ÿù×_/¼úÆhÓ}…Ì Í¤Qʽzý R«þì­×Þ¼èºéÝ0.ïv|꫊P0ŽÄ˜‹œ—£OqFÓ§ÔaL¼û›k§8*_î»±g†"Œ#±¥;¤X•_¡¹ÑgÇPîñú¢qÁøoããØžÔ¥_}þŽ¥)©©Ž¡Ç{T—ž}eævèÓ)5­ “jüÔ}’¤¹¡l¸­ªìÃ×øü˜é×™i4Û„h¹'¹}ÑÎI´&¾÷¥BOñh†jæà݇í¡KönؽyãzÕj³Ž>qÒ%GûJ¥Sår‡ÕL%žÈ*‘ ª»”Wë8Ë ÷­úàŸ/¾™šž®fwìˆð’>¯ß*=px‡FtØJÊŸ¸àŠ)t¶JÅ#Å®]P›V. X”‚Aš3:U i‰¥Ÿ¾óúßμðw~m¤“7®‘GíÙÉdsKí ±“èù“KY,œ••{?{÷¿^„Žvc‚K¢*¯ÈŒÈŽm‚µÙÇÑÈ=ø·—|ù¿ÿ<0ù×φÌë#d^ýêl¤Ü«×Ÿ ¥4Ë•‚I˜ξøòë¿“®ë>¶PîcùÃá=Jk,ÈŠ,Îʪ½Ÿ½UŸR‡1ñîWï¾ñÀ)¿½töð|½Ï®ŽiòþÌñ…Z¦Á6¹B‘ÅÂUU¹÷‹·^‹ªÏŽƒÜ£:#ßðÒÀ—®iÌ@[©¥áHÅ‘‚ììp™"·žŸq¨))©¶ÞCŽê™Û©s§´Œ¬\GFFnJZZnºÅêÀGù쪢Ú$Iª{’€éºnh^¿§ÊWí*§í K*+aýSþÞm[wì¥ñ oxJEÙ¡ã&Ÿyñp¿«Ë*k%©6…” XÂà¦Ô S 2¡7µù5æÀZ‰tXLpè™ÕR€äªªÊ‚•«W|ºâ›E+ÒkYrh­W¸”p#¼H© ÌH!3•2œIÁà =£0–¿.Ûµiå XÈO5UŸŸ³;)ßIDATi„TI䳿8PPPðöKÏ>vÖ”Ë~ÿÅÐ1–÷ »k«2 ¨z I.¢miwZøIk,Š÷îYúéÛ¯½Žâ‰r„á×nÌpxAõÝ$Yú6Q¡ˆVîìÞÿÎKÏÎ8sÊeÓ!óNLv™GõÝD¹wD‚¤ì%ÅÅ–·_|öé3§üþB½çùã¶zŽÓÙPº¨›˜]¢¹™ä"Ún–v…¢ÅÛ´Æ¢‘}J=Œ‰wÿûÂÓ&ïö3zœØµ¬Z/ÈNUJR­Ì _öd$š¹§]¡hñ6­±(ޛ߸>;ör+t¦3hL³î¤X8j;ÎtØpX].§eÓªŸÉ?,T™ü c…yȲEÒu”Ó9ð ¼øAyÒA¿)®eëÚÕNÛGN8aXŸ!ÃÆvêÜmhwU¡ò0XG47Ö~xUEñZdæ—a%ÅD£ÔæF‰¥Ä‚CæøŽS5C·ø5ì„e F·Œ¦ëžÊ²Ò=;÷lÙ²vͪüí›ó•“tb@e"¾&¢Ôéš:¨Ì¤8pƒÂse‚îñw¢8µ%ÃU;"¹ÍÒ´Ys/ƒFûŠÍbéñtÞÝ JìB #G'qd>ràØ&—Ù¡Ã@(œRšË©gzÝ’Ýã¡8íšè ´ô¡(úŽm X~ðàÖßýѶõk·âÅi€¶:£Ý h?eR0HXð‚KAÄEàæ¼G»y|¾½²,]1öŒ×ñMš"÷=zÄèãN¾0#7wp²É<ªãȽý ’Oí?txïcN:å¬Ì¾vV¦;ÔJÉœí¾O¡/oÓòè;fŸr}Ê’&õ)A1&Þ=úø‰fæä Æ8Tò(’î³*’WNŽ- éËÛô<úŽºb£ìС­«–,nTŸÝrÏ“£-Є4-”!E‚®Tð³©`Ô†¡p¤`S2p»Ž¸bAã®Tð: Èi N¿© äEŠYNH¹±ËÐTzôÐnY=s:wîjOM˶§8²,v{&,%6EV¬Ã'Ò‘—Á°œ»`ùt Ÿ·Êë‚ÅÄå®pV”—–—,:°oßþ};·Á¤ByÓA ¹1qw&7Ñ}^v~,?‡À3ÞîÇ\p`ÔöÈ¡¤|â4œšOó]€Ò=ß „T9¤R…sæ¥ :Ò>»ÑÑf5nXVÇŽ]Àl*ÌrµŽ~®ýØÛאַv('Þ ƒÒ§|ø€œÏòÓ3ŠCÊ W.L €m϶-•8vâY°|x^x\G”/)m~æƒÿÀüI‘ ñRàAïOaÓái¾ 'ž¨í›‚Þ¦Þø†s>‚ÃÛ€nê‰ÃóòN¡Ê $*?1/m•F~€Y8rpdÖþ&Í–˜žÂ4djÜjwDŒËœ°¢†I®OdÃ'%ƒL•¤XÐoÒÄ~ $.yy‹Õÿ’õ°jo[0wæy‰û&Œ ¹×¨Ú‹•ÜýÉ‘°Ç [ž²À˜#Qsn6¾­(÷¨.ù`žÎtÐX«á5½ŠHðÁ=?Ó…®ipNñHy!+ íHÉ0­8“ÒÁ^&>æ£sC Ì“®y”;Ñø‰”R.¸;w‰"‹âQ|N<ÝÀ3–Tgb„6M²¬ÌÒtm ÓPÐg–**œ®©²ù€šû#Óq†#mÈܸ•ðÄ™™kÊÄü\ã&e‚,t&í›înG@ ] @2ÛË ²X¤Ký…„Ü‹ºc)÷(-ÑŸ†>–ØòTƉšþ—ðhVŸÝŠr×%½Wè*oæ² ¥âLG ƵZÒhÉ¢A¿é>צù;ó3%ȧ±ã¸"ÑðLÏøÑ0O^&:Szüìšžñð Ó¿C 0ì[æÍëè©ÖÞG F¡ïÀŒùAÖ`Ðkrfâ,?sÅ"aÞ9D…»ÍW0¸Ngþ,\|ñL 0ÔúOcÿ{¼¯´9ä_?uß}´Z»!!÷¢ªJ.Ûâ!÷’¹?!ðã‰-¯ÜdÆ¸Ñø&ƒÜãŒÑ„3ç%Šx,)žˆ×¿6oŠ?ÍC€ÀOºåÉ'mžâÊ' ø;ÂlÁ~ð·(–‚lSË߉3?Þç×íåÌ ½o,üÜ^ÞQ¼‡@€Ñ¶‹´; äÀiäÀ[§ôÛžºõV²Òµ;r/l•¶¤Üãý?ó‚ÑïöH-‰-ÇcËÏ÷ùu{97 ßd“{í¥’“õ=R(NÏ›;Êï3æ`©31¸ÀåÙnè¨3h7$A@;D€¾@ µ¹zäÞ´Ý,v…úLµH3ŸÍ›±º¾î¯$äÞˆv@²Ë½v_ÁíôR¹àuñ‡‡Î6ª´st™ Ç £+Žœv¿§;yq$X¨m`¡G¡¬³õRšòÉs÷ÞK…L:r/éª\¼p’" ä^’V¼xm€@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ €@@ -‹€aäÉ¿¼5ÌÚ²¹ŠÜ€@@ H¤dxIñŽdF`ñ_Ò;¸ ç%†nœ%Il4°èjLÂ?3Ø\|§(òÛgÞáÿ6™qï.@óH å˜4IÝå.ë¡ùÕ4«a¸zvê´WZ¸ÐÓ|øD ¶‹À'Oæd0wÙý3¦C‰H‰TRƒULVî8çNÿW‘ŠçÖF`ìó;xürGCV$«•X5õš­]&‘¿@@ 0Ì]¶SÚ;~|®Wó]…Õo Ç׬sÁKëxõuc©ªõ¥>Ë–íl§0ˆ×JR>~ÔrŒÄüoƒÇû4I’ž8»Ï…–¦¾­56®/ˆãÒÈ×_8K×ËÇdürÅë‹3^_ù´ÀÇ`A€@@ ÐÒ´;åÂ8ûlÛŽ…÷b¦öNt>ŽH€¢Òнl³¦ÜÝóÇK"…Ïm…«“ Cû®O©M-+j樓›8å”S¾ö75 O +F¼þüɺ_z “E#£Iü»1å–_®¸^Xá¢L„1D ])ûÆëéÒ½ïC± ¿òÆ‘ÄöJ²ôÛþËW/o\DZ ÐvXø¨m°!y—A±Èhn© x?Î]úš›Žˆ/hÃÿ5?OgÆ,¤!7*X.ÐÁ=ºþòîVŒF!' f!Ð8aݬ¬âyÇñ£{C±ø¡IŠÍ`=˜f,Þv̨â[R‘º@ >Ð.Pºä}=Š•–¿>ª\ŸÒŠT‘öÊü§¡X<€ï«àF¾gø«ó_ˆœ“!±B ]X. ÆŽuT3mFCÚ ÌépRG÷_±bO¤´nyòI›ÍãQÇ÷îíž:uêþé7äÍwd¤VH¹óNg¤´ÄóÄBƒéοüÅÑ–êvácÊUº¡¿K$ѶŸÝç¢ÁYqÃüùVxpó›%cíSy·VIJL ÓºãñÇS¿ãŽêdœn‹|ذ~šúŠÅíx¿¿65~½x’|߆+nx¸Þ½ ?8žÒœÇxòòN‰›{`^Þbµuû*Ên˜9ç›çÌéM CøS=Ü|ÿ£C7žáãÉãån‹|X¾¦^xcþÄ}¤©ñÆ“˜þÐð7^Ùð~ÃßÓ|¤/ñSàQà_R¾þ÷fÍ;©aøXü.оŸî,q. •–ß_òœ¤x0Ôó–¼/øº%Ñy „W.vNÕ ›cYMÞ~̨s¢IßÀ·¤‹†õKÒT˜…Ü ï‹ßŒùýúEÀ쮸`áÑ{BE|…•–F\Ì—ü[)ÑÏ·‡kßðxd/ú%Ѥ[àŸó:êõG7µGú‚93ÒY‹úH×\lÅ sçv&ö&®<ÞŠðšŸ=ŒÙëºþ"ø}„jøõG#¬ KÊ…ŠEíkª4Ÿ…14CûdzÞc]¢M#ÚpEþÔ廣 ßšá_·&ú"o@â  &NQƒ—Tó±›`nˆý{ì6äøIð\ëÝý ž½§ÿ!oîÈçòf¬åOÐ1NÅzÂ…ðý–ߣóf>:XcÞS%¦PTuɳywí§û˜ë¯HJ7-U^+Uúχq&ÒøbÁœYÛé9ÍÆK{§KÒh…éktµÓ²@S9Íë.vš,ù]ÔÁK }[&¤gÙVpÓò]>š^^©©Kz'U‘–ðò’{—·¸ê\EÍü\ó•ŽA¹c€øé³sgæß8sö$ä7RfòŠçgß÷}MIjþògÀS7uð×yyS½ôdÚŒ9'[PY­æ»R‰bÉþøÙ¼›ª¦åÍjøô!È£Ó´™óΙ?ç¾zó²tU|´Ï¿ùd¸º • cíósf}]“kÍßéyÐýþStIv¦ØÙ¢'fÌ(º%ïÉ ¯¯Â´ˆ•þ nš3ç[¿[kUÓ¾$׿üCç[˜¶êé9³vRJæl¤¡?7çîͦµcÏ_C)lÈÒ[ÇôwŸºõVó{(7̘s¢jQö3ŸæÑdi"ÓïËCfü?Ìzø\”·ð¹Ù÷®|Ö×~Ã?1ŽùDLÖ¢ãÁ%²Â~=ÿ¡™ïSYòò®¦Óªòþr†äsocÕÆ]ÌûÛƒ.¿ëT‡jÿúoy‚ b M›9û,UUv<“wßºŠ·Ö&>¬M¢î²Ý?÷Uf¥ÏæÍXÍO›õÈÑ’áËnÈ_Ñ´GJ#X9ò8¤ÓI7zÎhÆfœ¼‡û~cŒÑ3÷ÃÓ¦ùòòÞ²ú·GíÚÈ-yöðjþsðÑÃE–¿nö}u¼ÖðýÛr\›sýú‹½=šö«æ¤,.øó¬‘¯¾4xíå×nö¼Þ=‰>Ÿwï®Ú{»nÌ{|‡æs_êóûÇCN,” G9ö…ÎCÕPž[0wæ’Pò<ŸŽç™O÷@æTÖæÅnÿë_S\%îÉèWr¬Šú•ÇoŠUþ˜…’ã !ÌŸ=㵺ˆ¸ˆV¾ ¾DM\ E á-ØÉü¢Æ¾t”á'ïœ4*+bX‰U¡cøTÓŒ:ëÅ<`ðŒAºl²x×Ïœû$Û+ñûdÃЯòû¼{n¼ÎoÌç’t®¦ë Œ*ß÷èT¦ê:ËÃ,ð DÓs8{¬Ç³W1<êø»á/Þz˼yé9 è5·±ƒ­écOc`þƒ._TThýè9¥SVé[e0íÿ$MÒ|Æ×7Ìš3—ži%îlÝÐÞõùKßGúX@)Ýí—ŒM0¾¬Òø*Èxþ5:®)žÜ_àÖô&ž}‚g'£œOìómùæ¦yóré9¶h™ë1¤ç4ÃGŠÃY(ëë~_éôL×°•¤dÐ ;¾­_H÷‰—¥À¿ùmà:ŠËeØ+x1{u»‘Û“ß§­G9/Ãì*Wµ±‘:C«ÎÒ%v®™0ôyÔ ¬Aø§O«<‹îI{c¾'3G$mH~]×/ûû‘_3ð]‚`÷’Ôƒú=ž¢Š•äfEqëýÈó8^oG¼Ëé'JgÚ̹ 4Cÿ?æ™Ê"ÖRg4ä£â•Þ½ã—O§›u*ÔÕ¥@xW,Ã-Ȼ㠞¿Ž{ge±îÕ:Ó^pú]Wò0äBH¼¤1=+"o…©J/lÓŒ |>ãež/ Ã÷껆gDháÊÙÇ ?„iïóò^¶SºO»øPÞ{h<ýÞïß|<0y©ê¦Á¡Çç[ ¾>¸Ælùqï G„{mškJÙ¼¿^¿ÿBT X&ö¤3¤ÝÒ½£)šªJëÉ*JõWURýäå"Cf#ÃÉH<ï“Ù¯ Ÿ¥ôIÂò­Æ´·uøÒíóm@¥gDáä8=Çô<’||M( š‹@B+ùÇÝûž÷i.ÁâcÀ¨hUÒqÁžÕ»gÀÆ Éo:›Êïcð0}ãbŒ²ò{$´!ú/ÀýÓa¸3]çá†ÿÅ¡ê”#LÀb0Ïc·d BÇU©iú©”†Ç_>½­·›:³ï sf^—‘i?¿Ó<ÕìÜB‘x¿_xaîÌcÌž9 3è‹ÐÓÔ¹øýÚSø½á´‘ƒû!ï©LV'¢Ì÷Þ˜7·Î•‹wFÞ#O9h0²ÄYã„n–z!üix—ÿ¡4}ï ýs®ÄïsSR_<»ÈÖ9cÂÛý.ýV*+âöaéê¤÷+\Ÿƒz:…ê/Ìžñ<~E^ƒ¸×šƒý1¤]ˆ;Çx¸ž½ˆüÌA¹% ½geYºqÁœ™ñ¾gH2{ Õ³ òîÙƒÎÿvJNJ³\¶`ö=ëvjé:ëIÀ%ÙSè÷ôç Ó²R¾†_ó›ÝÍÒ}Üyn´uʇúðêNÿ¶–Nƒ›ÏqÈ÷L~C1hÁÜà÷ñ‹uâó3gîãÏZò .'žùéÕÞðéKl° 9# ë×ðs²nÇÁ‡ù¾Ö…pãóy3—EÃ[xÏ#êÞ=RCÝa×Åvš€» >°©âþ‘®=†+gCÏÌ´,¢Ô÷û÷N0s‘Œ‰5|hL¢ßš$MƦ©_<ðÀ$ =Iñ2ÚÆàí«eU>å¸åÆYóN0ãÖü9âýÛ ”±9—'6'r¸¸˜´‰*mƒéÜ0kîu˜´¸Aa|òúûçòîÝÁÓ¿g+ª½ç ³g>N~Dâyž]šóvð@‹j#9?Y’Õ ·ë&¢‘ãHæþÌü!__”¸!4„V.4/Ô„wŽ>ŠÁFXU²>@GÓn1æ :‡©øfÆ[qóò$EŸ4‹cÓ´û瞎k: l˜9›Yvûü¹3¿¥ks‡CúƒòþôƒìÏ1àQè¼ÿ´ûçüª¢Ü}'n;—}Úƒö¤‘W(,.˜öSß k"š1Å êL R~^´fË dåt6”…}è4Må…ÂLy‡ÎæÎW†´—ïñ]R0àÁÀQêEÏ‘¹-sWƒ)-oQűPLÖað>™žáùÜsO9]ÛÔÌŸè̪Y?óÅÕ"½Ìƒáu~ ™q5ÍGŠŽ¿÷QÞtÀ‚±›Þ?˜O¿ ×6”÷d3-ƒ” ƒ¬5ÃnÏûküºÉõ`1íÒeçlìvù9\yÌu2ä¥åSÜÔ½ÛÐåIsÒâã*”÷÷ÜÅ—»…ÏЯâG²EŠœ¾dxB•{‚¦ Þ*é9üË_?wsÞ£ÝÌðp§ÓþÓ|o!Üõ@q#µ1ª;(ª›`2z gü^?ö=k(~ Ù£i<=rKDøo¡9œL3Óà©A3“( °¡vó1-Γuö>Ý'z>oÆz´­º®' Ö¼Ѷ£’»MÉr)ª>/gÁzr9YGQCG£’Þ•Ôsr…ç‹:|âù¼;‹éw$ùŽçyz5ép“rYbN€GéY´rAƒ¶JƒS(ù*øš#$Î@sP›¹µãb‘vê8Œ¡#Šì…¼ÉO® Ÿh†ÿb( eDLUSþ[íw×ÌTÖ–ágœ¾êKÑy®‘˜ü :,Ó¿œùUðkóŒ5„ü`|ÌØM3æôöIì5ƒyÀ I“V`Ð]f†Óô®t¶3Ç^ó7þtUúmܧo1Á)bû1ˆƒó‡Ä`ANâah …£Îï]UØ!þ ƒnS.â¿ë ¸ a¦fջϤ ü7b×½‹‡ÙütÌwáaÂaª‹´üÐV̸°Bônð@bw‘C„¢âô%ó)Ì×ܬýkQ­ áZ0Ÿ,>Ÿ÷x¦¦\ÿÿ\zõD(n§"ÞÇfPÏKõ"c­“tðX-a ¿ägäÜñ–ÃÝ&÷ê >x˜–8Šýñk @ÕŸ ^0õ®à¯c°m¨„Sƒ?$îc#1C(ʳï[Šv²Åç÷^ˆó'x:JU­ç›q£à-TøõÀóÔÆPßhCØP f'¢)8¿Êã6<‡kx¡¶ ¦‡Vü Øô|—î\¾]*«êÇšßÿ¹z«µ±‡ôiuuÍD‚œ¦¬ Œ |À‡E òþm…ë—»i¿ “²Ð6›9B,ðááö&¬,ËÓçϾïÇ0A˜*Yêø¼V~„åù€L ëºAÒ­¸E—éO´r<\û tˆBÉWÁ×5øˆ¿@óˆz°×¼lâþõõWºÅ>›Æ¤ÿ&îS!œiíÅ—‹U©XÓòæ£ãd‘Õ“`Á8®77c°±?Ú"Ã/w:§Á-ª;¹~ô š7ÝžlвÒqú«Žáé³­CÍ!n¨9©û0Æ·¨¤çàÒ·¬šCfÆÃ’ÍXÌãD{F·Oê/<:«Ìr«¬²< ²žð똞eXT`¹`iêE<ºf²ô÷nìî:åŠçùTžyoÏïù#Š´‹üÿÑyihŒ, EÁà–:[¶–—:üjîéã1`ÞÎÓ v†ûÂy’d¹iŸ “«­BÀ;Ôì{³ËƒÓ¶3ï,r†KHR¤0 zM7 G pÃoqÿ_uÏ é´Ú5ìb`ÿ)Ÿ©†·êÒhpMƒuv´Ckt}¬œb®iRäŸ-'vúÊÕ]—N—±†Èt¯1¤b(wß5´!dQMþÕ]zÒ\äu´!É;•ª­ða¸2Fý 3(Q‡ml@#>}ê(²üÁó¯^ȇ¤x×ÇÓïXËñy˜?_CEÜ‹@B+’"1˜l,áÂK²uú©¹Ž0Bí€ôîÀz€z.Q”‡ä720£äà ¬”~O›5ï8t$¿ÂeTu€™ú „=@qßzë-勵[‘4ä'¸kfû~Æ£Ûiq,v6éä÷þøTíŽGïÁq=ù™Ó®&äOL‹hUw¶{”Égž ÷®ÉTÔù±x[÷×s!¢GA N[n”;7èÃH7ÛçRMê.úHámsçvfU¾ç ]¿—Üb˜_­˜Tù¨.L¢Ycäwޝ̆±ŠÞ•ø°# çcÐiξ“_>Ê9Y–»gÕ&Wïä·ë®Z·š'Ð1?EîVõ´ÐIµ-BVx­8$})UÚ Î§øÊýÛØì·´£ }ìÆ¼9ã½~/ᾎõìôºt,´ÎÁ AÓõP ÿYw¿¼M£ú†¢ý=xÿ%Ôÿ×M^#¡œ y¼f,©:þÕ’,/®}ß/¡èÜŒkSÁ¥5C(ÛRØån'K%…ñ¨¸i¹RÇkã=µ> Z¸FÞD;2'K-ªàhãQËô¨<(²üÅó‡Ó «7qœK»§Ñ"Zó¥½;=ˆ¹§D’à놈ˆß@ˆj`Û„t[$Jß^ý7a@ãŠWfØre´i›_W•ØÀ§ÚÇ{ ãa«Ë¯0˜ù­­Øi)ß0´ÿƒ²1áNƒkÈM Ã7üm‘,Oâ^ß}¾¹_¬Ý\ W Nx÷—±JùÉi³æô“$ëïÈ®GÛ5¿{#ž}Fi¨Ša⣪6 ®áOäÓ¶±= ï!ìfuß’µa~á~Ó‚U¼ç“X¯ñÊ^døØpºúðô‘Ÿ?Óeå3¥fúwò{ÑžÉò +Ò¥P¶®ÇnYû°ST>#ÝUU½‚Òxvö][ÈM —›H£{ “É%% ƒØÅô›©–  )æoüénô$fÒagªE×Ïœ³ ~ùðŽ º(÷™ëPx¸PgUÍÉC¾^§¿ú¯¡ÂÄóþY·WcàÊ"*M)¶HþW4ñl2~ ÜŽ´7K+|åûüKœº-Aý,…ò}2m»ÊÓ¡4ðþ¿Ó»Ëƒ>ä÷›Ã[Ñ·1*£ÑxÕwƒã…ˆâ©œÁxï >”$½Gî2ÊÛC!4Re»®qR•«pÙ@mEÛÚ kÚ\ðêÔ¿çÝ^ãÉÃ…8·6†(V£n£E-w•0×´£‘¡x>ð°9Æà˯uƒ-,ðÍ=ˆÙ‚?ºõs±”ãùòkÁ× qšƒäXbÓö±Gˆãy±~ Äw÷_¹ºO¬Ó¥=ì=¿¶`ÆŒBJ›|®S,7ÿE¤üÌ=ôY.Ü{¦USX²Tt²ÌØWj™×ÑÕ¡CIªÓ©’¢sCÞ#½˜ß·nT˜AÆŒ>ˆv•ºéÁGúëLËIÉp¬3"zÐD¢²û]l¢¨;¹[K´I‘õÄQXí ©aºä.â)©fÑÏ3³ïùåˆçØ¢÷É{ï=% cÛè‰\x¼šÖ“¥Ê›ø‚ôèc·nÈOSOÂÖ¥4`a`³èœ»ŒÓ“ Õï€s(„‹ÜY鿎ÿ¤1i4‡·"µ1šS¼Šo³tÿÿöî6´Ê*ø}Ü ê´˜D-{ó &ÕT„ŠB‰Q ü‚2ªIATd¥}©|‹Â‚@?DI2¢ 7‚!4‰(E?$hSĵíÞÛÿÙº¢åÖÝÝÕ{·~ƒqǽçõ·g;Ï9çJiÛùý©¥^ãéÌä—~ŸÏÆ©!×7þ|þ ìüº'êÏ >ر<—˦3”eÿŠY£»[óiÙ þ»ÀrÝ?ž|~ãì\¦~ò[7¤Ð.¸‡•û>~1 ×õÅT¼G€@±ã~pqhÑíeóù‹íp±éâ©þ‹s;»^.6}¥ÓÅlÈÞx,w´¡®á¹l¦&×;г%HM±/á\ØÔJ·Qý—^à³M1,wá9¥Ö7‡ÞLMÝ¢Ögú”ZF5å<œìÔŸ×D¨è1ð}ì]z¡šÚ§-CÆàªûì‰tóÿ¬òš$Ç’)³»Û†û,oÙJ#@€‚À¸^•vbVG×'ñdçŸÑ5 ý+é5žÖž¨Ò°¥¤ÌÊ”ÔÖ¬vÏ;ÓæHoÿ©t]ñU µSWT¨9ª­À•¹ODd¡ŸÊQ} °Û'ÊÀ"õ8s²/=Õ=Ät83­îr)£ü{ÚÚ²“’šWÊ]r,ƒ|ÕÀ¢ÜªÊ#@€À¿ÆýÌEÚ¥ƒ‹o[4¿u?Štì_†ðñ9]»Æ^Òå/!]’rwSÓÀàY—¿z5VÀ¾×¦ÎÈœÝk)šKiN Rc¥aòôýro–’_cx)§Ü½kûW±˜óž±–5˜?I~lžÜxW:p)Ky !@€a&Äà"íÝo‹ZÚóùÜÛÃö´Èâ«­s;÷·™\2U)ðŶÆ+û{zvÄúìÑ…ÇM2±OeÒªÖõÙs›¬«²ƒ5áZ>zwf_ÿÀq ß8¶ÎÆr¨$sG÷ʵGÆVŽÜ PŒÀ¸_Uèdìx'¦½×Åhé‚Ío…Ï‹zM’sxø?#7U–D*(°|íÉžÖ ¹¶×ÜC'¤ÐžøçëT,/Ü\S;}¾ÅP>ºl]+VË$5ËâÚ$0N>ß\K.7p_<ÙaŒ¯‹x]üñGxÓä—8Ëã›+j®Ýw纣—,¬ó8eÓì*hÞ½sF¦7»=f0Msb`±·~rÝê®¶UÇG“OZ›À„\¤¿/]:íôé?žŠXöí1qÃpDÑù8Ð.y?âøošÕÙ9v¸´Þ'@€Ê ÜúáÖeÙ\æÙØ”ÞyñY÷Û[†b¯Æ¤×»W®<ë§r-V3þŸrpQøU¦ñÀ.iYœÏ%qv~NlS©Ø×&ùŽ›æÜü]²gO_!½W¨næÝï5ezû—Æìô‚ø¾:mmÜêÇò©îúºÚ¯—SUw´Ž @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€ @€¨2¿üû¾¡RO…IEND®B`‚networking-ovn-4.0.0/doc/source/admin/refarch/routers.rst0000666000175100017510000012235713245511145023547 0ustar zuulzuul00000000000000.. _refarch-routers: Routers ------- Routers pass traffic between layer-3 networks. Create a router ~~~~~~~~~~~~~~~ #. On the controller node, source the credentials for a regular (non-privileged) project. The following example uses the ``demo`` project. #. On the controller node, create router in the Networking service. .. code-block:: console $ openstack router create router +-----------------------+--------------------------------------+ | Field | Value | +-----------------------+--------------------------------------+ | admin_state_up | UP | | description | | | external_gateway_info | null | | headers | | | id | 24addfcd-5506-405d-a59f-003644c3d16a | | name | router | | project_id | b1ebf33664df402693f729090cfab861 | | routes | | | status | ACTIVE | +-----------------------+--------------------------------------+ OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations when creating a router. #. The OVN mechanism driver translates the router into a logical router object in the OVN northbound database. .. code-block:: console _uuid : 1c2e340d-dac9-496b-9e86-1065f9dab752 default_gw : [] enabled : [] external_ids : {"neutron:router_name"="router"} name : "neutron-a24fd760-1a99-4eec-9f02-24bb284ff708" ports : [] static_routes : [] #. The OVN northbound service translates this object into logical flows and datapath bindings in the OVN southbound database. * Datapath bindings .. code-block:: console _uuid : 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa external_ids : {logical-router="1c2e340d-dac9-496b-9e86-1065f9dab752"} tunnel_key : 3 * Logical flows .. code-block:: console Datapath: 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa Pipeline: ingress table= 0( lr_in_admission), priority= 100, match=(vlan.present || eth.src[40]), action=(drop;) table= 1( lr_in_ip_input), priority= 100, match=(ip4.mcast || ip4.src == 255.255.255.255 || ip4.src == 127.0.0.0/8 || ip4.dst == 127.0.0.0/8 || ip4.src == 0.0.0.0/8 || ip4.dst == 0.0.0.0/8), action=(drop;) table= 1( lr_in_ip_input), priority= 50, match=(ip4.mcast), action=(drop;) table= 1( lr_in_ip_input), priority= 50, match=(eth.bcast), action=(drop;) table= 1( lr_in_ip_input), priority= 30, match=(ip4 && ip.ttl == {0, 1}), action=(drop;) table= 1( lr_in_ip_input), priority= 0, match=(1), action=(next;) table= 2( lr_in_unsnat), priority= 0, match=(1), action=(next;) table= 3( lr_in_dnat), priority= 0, match=(1), action=(next;) table= 5( lr_in_arp_resolve), priority= 0, match=(1), action=(get_arp(outport, reg0); next;) table= 6( lr_in_arp_request), priority= 100, match=(eth.dst == 00:00:00:00:00:00), action=(arp { eth.dst = ff:ff:ff:ff:ff:ff; arp.spa = reg1; arp.op = 1; output; };) table= 6( lr_in_arp_request), priority= 0, match=(1), action=(output;) Datapath: 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa Pipeline: egress table= 0( lr_out_snat), priority= 0, match=(1), action=(next;) #. The OVN controller service on each compute node translates these objects into flows on the integration bridge ``br-int``. .. code-block:: console # ovs-ofctl dump-flows br-int cookie=0x0, duration=6.402s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x5,vlan_tci=0x1000/0x1000 actions=drop cookie=0x0, duration=6.402s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x5, dl_src=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_dst=127.0.0.0/8 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_dst=0.0.0.0/8 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_dst=224.0.0.0/4 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=50,ip,metadata=0x5,nw_dst=224.0.0.0/4 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_src=255.255.255.255 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_src=127.0.0.0/8 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x5,nw_src=0.0.0.0/8 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=90,arp,metadata=0x5,arp_op=2 actions=push:NXM_NX_REG0[],push:NXM_OF_ETH_SRC[], push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[], pop:NXM_NX_REG0[],pop:NXM_OF_ETH_SRC[], controller(userdata=00.00.00.01.00.00.00.00), pop:NXM_OF_ETH_SRC[],pop:NXM_NX_REG0[] cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=50,metadata=0x5,dl_dst=ff:ff:ff:ff:ff:ff actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=30,ip,metadata=0x5,nw_ttl=0 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=30,ip,metadata=0x5,nw_ttl=1 actions=drop cookie=0x0, duration=6.402s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x5 actions=resubmit(,18) cookie=0x0, duration=6.402s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x5 actions=resubmit(,19) cookie=0x0, duration=6.402s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x5 actions=resubmit(,20) cookie=0x0, duration=6.402s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x5 actions=resubmit(,32) cookie=0x0, duration=6.402s, table=48, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x5 actions=resubmit(,49) Attach a self-service network to the router ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Self-service networks, particularly subnets, must interface with a router to enable connectivity with other self-service and provider networks. #. On the controller node, add the self-service network subnet ``selfservice-v4`` to the router ``router``. .. code-block:: console $ openstack router add subnet router selfservice-v4 .. note:: This command provides no output. OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations when adding a subnet as an interface on a router. #. The OVN mechanism driver translates the operation into logical objects and devices in the OVN northbound database and performs a series of operations on them. * Create a logical port. .. code-block:: console _uuid : 4c9e70b1-fff0-4d0d-af8e-42d3896eb76f addresses : ["fa:16:3e:0c:55:62 192.168.1.1"] enabled : true external_ids : {"neutron:port_name"=""} name : "5b72d278-5b16-44a6-9aa0-9e513a429506" options : {router-port="lrp-5b72d278-5b16-44a6-9aa0-9e513a429506"} parent_name : [] port_security : [] tag : [] type : router up : false * Add the logical port to logical switch. .. code-block:: console _uuid : 0ab40684-7cf8-4d6c-ae8b-9d9143762d37 acls : [] external_ids : {"neutron:network_name"="selfservice"} name : "neutron-d5aadceb-d8d6-41c8-9252-c5e0fe6c26a5" ports : [1ed7c28b-dc69-42b8-bed6-46477bb8b539, 4c9e70b1-fff0-4d0d-af8e-42d3896eb76f, ae10a5e0-db25-4108-b06a-d2d5c127d9c4] * Create a logical router port object. .. code-block:: console _uuid : f60ccb93-7b3d-4713-922c-37104b7055dc enabled : [] external_ids : {} mac : "fa:16:3e:0c:55:62" name : "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506" network : "192.168.1.1/24" peer : [] * Add the logical router port to the logical router object. .. code-block:: console _uuid : 1c2e340d-dac9-496b-9e86-1065f9dab752 default_gw : [] enabled : [] external_ids : {"neutron:router_name"="router"} name : "neutron-a24fd760-1a99-4eec-9f02-24bb284ff708" ports : [f60ccb93-7b3d-4713-922c-37104b7055dc] static_routes : [] #. The OVN northbound service translates these objects into logical flows, datapath bindings, and the appropriate multicast groups in the OVN southbound database. * Logical flows in the logical router datapath .. code-block:: console Datapath: 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa Pipeline: ingress table= 0( lr_in_admission), priority= 50, match=((eth.mcast || eth.dst == fa:16:3e:0c:55:62) && inport == "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506"), action=(next;) table= 1( lr_in_ip_input), priority= 100, match=(ip4.src == {192.168.1.1, 192.168.1.255}), action=(drop;) table= 1( lr_in_ip_input), priority= 90, match=(ip4.dst == 192.168.1.1 && icmp4.type == 8 && icmp4.code == 0), action=(ip4.dst = ip4.src; ip4.src = 192.168.1.1; ip.ttl = 255; icmp4.type = 0; inport = ""; /* Allow sending out inport. */ next; ) table= 1( lr_in_ip_input), priority= 90, match=(inport == "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506" && arp.tpa == 192.168.1.1 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:0c:55:62; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:0c:55:62; arp.tpa = arp.spa; arp.spa = 192.168.1.1; outport = "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506"; inport = ""; /* Allow sending out inport. */ output;) table= 1( lr_in_ip_input), priority= 60, match=(ip4.dst == 192.168.1.1), action=(drop;) table= 4( lr_in_ip_routing), priority= 24, match=(ip4.dst == 192.168.1.0/255.255.255.0), action=(ip.ttl--; reg0 = ip4.dst; reg1 = 192.168.1.1; eth.src = fa:16:3e:0c:55:62; outport = "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506"; next;) Datapath: 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa Pipeline: egress table= 1( lr_out_delivery), priority= 100, match=(outport == "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506), action=(output;) * Logical flows in the logical switch datapath .. code-block:: console Datapath: 611d35e8-b1e1-442c-bc07-7c6192ad6216 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "5b72d278-5b16-44a6-9aa0-9e513a429506"), action=(next;) table= 3( ls_in_pre_acl), priority= 110, match=(ip && inport == "5b72d278-5b16-44a6-9aa0-9e513a429506"), action=(next;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 192.168.1.1 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:0c:55:62; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:0c:55:62; arp.tpa = arp.spa; arp.spa = 192.168.1.1; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:fa:76:8f), action=(outport = "f112b99a-8ccc-4c52-8733-7593fa0966ea"; output;) Datapath: 611d35e8-b1e1-442c-bc07-7c6192ad6216 Pipeline: egress table= 1( ls_out_pre_acl), priority= 110, match=(ip && outport == "f112b99a-8ccc-4c52-8733-7593fa0966ea"), action=(next;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "f112b99a-8ccc-4c52-8733-7593fa0966ea"), action=(output;) * Port bindings .. code-block:: console _uuid : 0f86395b-a0d8-40fd-b22c-4c9e238a7880 chassis : [] datapath : 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa logical_port : "lrp-5b72d278-5b16-44a6-9aa0-9e513a429506" mac : [] options : {peer="5b72d278-5b16-44a6-9aa0-9e513a429506"} parent_port : [] tag : [] tunnel_key : 1 type : patch _uuid : 8d95ab8c-c2ea-4231-9729-7ecbfc2cd676 chassis : [] datapath : 4aef86e4-e54a-4c83-bb27-d65c670d4b51 logical_port : "5b72d278-5b16-44a6-9aa0-9e513a429506" mac : ["fa:16:3e:0c:55:62 192.168.1.1"] options : {peer="lrp-5b72d278-5b16-44a6-9aa0-9e513a429506"} parent_port : [] tag : [] tunnel_key : 3 type : patch * Multicast groups .. code-block:: console _uuid : 4a6191aa-d8ac-4e93-8306-b0d8fbbe4e35 datapath : 4aef86e4-e54a-4c83-bb27-d65c670d4b51 name : _MC_flood ports : [8d95ab8c-c2ea-4231-9729-7ecbfc2cd676, be71fac3-9f04-41c9-9951-f3f7f1fa1ec5, da5c1269-90b7-4df2-8d76-d4575754b02d] tunnel_key : 65535 In addition, if the self-service network contains ports with IP addresses (typically instances or DHCP servers), OVN creates a logical flow for each port, similar to the following example. .. code-block:: console Datapath: 4a7485c6-a1ef-46a5-b57c-5ddb6ac15aaa Pipeline: ingress table= 5( lr_in_arp_resolve), priority= 100, match=(outport == "lrp-f112b99a-8ccc-4c52-8733-7593fa0966ea" && reg0 == 192.168.1.11), action=(eth.dst = fa:16:3e:b6:91:70; next;) #. On each compute node, the OVN controller service creates patch ports, similar to the following example. .. code-block:: console 7(patch-f112b99a-): addr:4e:01:91:2a:73:66 config: 0 state: 0 speed: 0 Mbps now, 0 Mbps max 8(patch-lrp-f112b): addr:be:9d:7b:31:bb:87 config: 0 state: 0 speed: 0 Mbps now, 0 Mbps max #. On all compute nodes, the OVN controller service creates the following additional flows: .. code-block:: console cookie=0x0, duration=6.667s, table=0, n_packets=0, n_bytes=0, idle_age=6, priority=100,in_port=8 actions=load:0x9->OXM_OF_METADATA[],load:0x1->NXM_NX_REG6[], resubmit(,16) cookie=0x0, duration=6.667s, table=0, n_packets=0, n_bytes=0, idle_age=6, priority=100,in_port=7 actions=load:0x7->OXM_OF_METADATA[],load:0x4->NXM_NX_REG6[], resubmit(,16) cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x4,metadata=0x7 actions=resubmit(,17) cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x1,metadata=0x9, dl_dst=fa:16:3e:fa:76:8f actions=resubmit(,17) cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x1,metadata=0x9, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,17) cookie=0x0, duration=6.674s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x9,nw_src=192.168.1.1 actions=drop cookie=0x0, duration=6.673s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x9,nw_src=192.168.1.255 actions=drop cookie=0x0, duration=6.673s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=90,arp,reg6=0x1,metadata=0x9, arp_tpa=192.168.1.1,arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:fa:76:8f,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163efa768f->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80101->NXM_OF_ARP_SPA[],load:0x1->NXM_NX_REG7[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=6.673s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=90,icmp,metadata=0x9,nw_dst=192.168.1.1, icmp_type=8,icmp_code=0 actions=move:NXM_OF_IP_SRC[]->NXM_OF_IP_DST[],mod_nw_src:192.168.1.1, load:0xff->NXM_NX_IP_TTL[],load:0->NXM_OF_ICMP_TYPE[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,18) cookie=0x0, duration=6.674s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=60,ip,metadata=0x9,nw_dst=192.168.1.1 actions=drop cookie=0x0, duration=6.674s, table=20, n_packets=0, n_bytes=0, idle_age=6, priority=24,ip,metadata=0x9,nw_dst=192.168.1.0/24 actions=dec_ttl(),move:NXM_OF_IP_DST[]->NXM_NX_REG0[], load:0xc0a80101->NXM_NX_REG1[],mod_dl_src:fa:16:3e:fa:76:8f, load:0x1->NXM_NX_REG7[],resubmit(,21) cookie=0x0, duration=6.674s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg0=0xc0a80103,reg7=0x1,metadata=0x9 actions=mod_dl_dst:fa:16:3e:d5:00:02,resubmit(,22) cookie=0x0, duration=6.674s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg0=0xc0a80102,reg7=0x1,metadata=0x9 actions=mod_dl_dst:fa:16:3e:82:8b:0e,resubmit(,22) cookie=0x0, duration=6.673s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg0=0xc0a8010b,reg7=0x1,metadata=0x9 actions=mod_dl_dst:fa:16:3e:b6:91:70,resubmit(,22) cookie=0x0, duration=6.673s, table=25, n_packets=0, n_bytes=0, idle_age=6, priority=50,arp,metadata=0x7,arp_tpa=192.168.1.1, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:fa:76:8f,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163efa768f->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80101->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=6.674s, table=26, n_packets=0, n_bytes=0, idle_age=6, priority=50,metadata=0x7,dl_dst=fa:16:3e:fa:76:8f actions=load:0x4->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=6.667s, table=33, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x4,metadata=0x7 actions=resubmit(,34) cookie=0x0, duration=6.667s, table=33, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x1,metadata=0x9 actions=resubmit(,34) cookie=0x0, duration=6.667s, table=34, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg6=0x4,reg7=0x4,metadata=0x7 actions=drop cookie=0x0, duration=6.667s, table=34, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg6=0x1,reg7=0x1,metadata=0x9 actions=drop cookie=0x0, duration=6.674s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=110,ipv6,reg7=0x4,metadata=0x7 actions=resubmit(,50) cookie=0x0, duration=6.673s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=110,ip,reg7=0x4,metadata=0x7 actions=resubmit(,50) cookie=0x0, duration=6.673s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x1,metadata=0x9 actions=resubmit(,64) cookie=0x0, duration=6.673s, table=55, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg7=0x4,metadata=0x7 actions=resubmit(,64) cookie=0x0, duration=6.667s, table=64, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x4,metadata=0x7 actions=output:7 cookie=0x0, duration=6.667s, table=64, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x1,metadata=0x9 actions=output:8 #. On compute nodes not containing a port on the network, the OVN controller also creates additional flows. .. code-block:: console cookie=0x0, duration=6.673s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x7, dl_src=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x7,vlan_tci=0x1000/0x1000 actions=drop cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70 actions=resubmit(,17) cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x2,metadata=0x7 actions=resubmit(,17) cookie=0x0, duration=6.674s, table=16, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg6=0x1,metadata=0x7 actions=resubmit(,17) cookie=0x0, duration=6.674s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=90,ip,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70,nw_src=192.168.1.11 actions=resubmit(,18) cookie=0x0, duration=6.674s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=90,udp,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70,nw_src=0.0.0.0, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=resubmit(,18) cookie=0x0, duration=6.674s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=80,ip,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70 actions=drop cookie=0x0, duration=6.673s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=80,ipv6,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70 actions=drop cookie=0x0, duration=6.670s, table=17, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,18) cookie=0x0, duration=6.674s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=90,arp,reg6=0x3,metadata=0x7, dl_src=fa:16:3e:b6:91:70,arp_spa=192.168.1.11, arp_sha=fa:16:3e:b6:91:70 actions=resubmit(,19) cookie=0x0, duration=6.673s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=80,icmp6,reg6=0x3,metadata=0x7,icmp_type=135, icmp_code=0 actions=drop cookie=0x0, duration=6.673s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=80,icmp6,reg6=0x3,metadata=0x7,icmp_type=136, icmp_code=0 actions=drop cookie=0x0, duration=6.673s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=80,arp,reg6=0x3,metadata=0x7 actions=drop cookie=0x0, duration=6.673s, table=18, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,19) cookie=0x0, duration=6.673s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=110,icmp6,metadata=0x7,icmp_type=136,icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=6.673s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=110,icmp6,metadata=0x7,icmp_type=135,icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=6.674s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x7 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=6.670s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,metadata=0x7 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=6.674s, table=19, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,20) cookie=0x0, duration=6.673s, table=20, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,21) cookie=0x0, duration=6.674s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x1/0x1,metadata=0x7 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=6.670s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x1/0x1,metadata=0x7 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=6.674s, table=21, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,22) cookie=0x0, duration=6.674s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=-new+est-rel-inv+trk,metadata=0x7 actions=resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=-new-est+rel-inv+trk,metadata=0x7 actions=resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=+inv+trk,metadata=0x7 actions=drop cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=65535,icmp6,metadata=0x7,icmp_type=135, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=65535,icmp6,metadata=0x7,icmp_type=136, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=6.674s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2002,udp,reg6=0x3,metadata=0x7, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.674s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2002,udp,reg6=0x3,metadata=0x7, nw_dst=192.168.1.0/24,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2002,ct_state=+new+trk,ipv6,reg6=0x3,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2002,ct_state=+new+trk,ip,reg6=0x3,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.674s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2001,ip,reg6=0x3,metadata=0x7 actions=drop cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=2001,ipv6,reg6=0x3,metadata=0x7 actions=drop cookie=0x0, duration=6.674s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=1,ipv6,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=1,ip,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=6.673s, table=22, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,23) cookie=0x0, duration=6.673s, table=23, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,24) cookie=0x0, duration=6.674s, table=24, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x2/0x2,metadata=0x7 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=6.674s, table=24, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x2/0x2,metadata=0x7 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=6.673s, table=24, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x4/0x4,metadata=0x7 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=6.670s, table=24, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x4/0x4,metadata=0x7 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=6.674s, table=24, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,25) cookie=0x0, duration=6.673s, table=25, n_packets=0, n_bytes=0, idle_age=6, priority=50,arp,metadata=0x7,arp_tpa=192.168.1.11, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:b6:91:70,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163eb69170->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a8010b->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=6.670s, table=25, n_packets=0, n_bytes=0, idle_age=6, priority=50,arp,metadata=0x7,arp_tpa=192.168.1.3,arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:d5:00:02,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ed50002->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80103->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=6.670s, table=25, n_packets=0, n_bytes=0, idle_age=6, priority=50,arp,metadata=0x7,arp_tpa=192.168.1.2, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:82:8b:0e,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163e828b0e->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80102->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[],load:0->NXM_NX_REG6[], load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=6.674s, table=25, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,26) cookie=0x0, duration=6.674s, table=26, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x7, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=load:0xffff->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=6.674s, table=26, n_packets=0, n_bytes=0, idle_age=6, priority=50,metadata=0x7,dl_dst=fa:16:3e:d5:00:02 actions=load:0x2->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=6.673s, table=26, n_packets=0, n_bytes=0, idle_age=6, priority=50,metadata=0x7,dl_dst=fa:16:3e:b6:91:70 actions=load:0x3->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=6.670s, table=26, n_packets=0, n_bytes=0, idle_age=6, priority=50,metadata=0x7,dl_dst=fa:16:3e:82:8b:0e actions=load:0x1->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=6.674s, table=32, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x3,metadata=0x7 actions=load:0x7->NXM_NX_TUN_ID[0..23], set_field:0x3/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30],output:3 cookie=0x0, duration=6.673s, table=32, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x2,metadata=0x7 actions=load:0x7->NXM_NX_TUN_ID[0..23], set_field:0x2/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30],output:3 cookie=0x0, duration=6.670s, table=32, n_packets=0, n_bytes=0, idle_age=6, priority=100,reg7=0x1,metadata=0x7 actions=load:0x7->NXM_NX_TUN_ID[0..23], set_field:0x1/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30],output:5 cookie=0x0, duration=6.674s, table=48, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,49) cookie=0x0, duration=6.674s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=110,icmp6,metadata=0x7,icmp_type=135,icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=6.673s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=110,icmp6,metadata=0x7,icmp_type=136,icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=6.674s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,metadata=0x7 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=6.673s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,metadata=0x7 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=6.674s, table=49, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,50) cookie=0x0, duration=6.674s, table=50, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x1/0x1,metadata=0x7 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=6.673s, table=50, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x1/0x1,metadata=0x7 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=6.673s, table=50, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,51) cookie=0x0, duration=6.670s, table=51, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,52) cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=+inv+trk,metadata=0x7 actions=drop cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=-new+est-rel-inv+trk,metadata=0x7 actions=resubmit(,53) cookie=0x0, duration=6.673s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=65535,ct_state=-new-est+rel-inv+trk,metadata=0x7 actions=resubmit(,53) cookie=0x0, duration=6.673s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=65535,icmp6,metadata=0x7,icmp_type=136, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=6.673s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=65535,icmp6,metadata=0x7,icmp_type=135, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2002,ct_state=+new+trk,ip,reg7=0x3,metadata=0x7, nw_src=192.168.1.11 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.670s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2002,ct_state=+new+trk,ip,reg7=0x3,metadata=0x7, nw_src=192.168.1.11 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.670s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2002,udp,reg7=0x3,metadata=0x7, nw_src=192.168.1.0/24,tp_src=67,tp_dst=68 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.670s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2002,ct_state=+new+trk,ipv6,reg7=0x3, metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.673s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2001,ip,reg7=0x3,metadata=0x7 actions=drop cookie=0x0, duration=6.673s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=2001,ipv6,reg7=0x3,metadata=0x7 actions=drop cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=1,ip,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=1,ipv6,metadata=0x7 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=6.674s, table=52, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,53) cookie=0x0, duration=6.674s, table=53, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x4/0x4,metadata=0x7 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=6.674s, table=53, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x4/0x4,metadata=0x7 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=6.673s, table=53, n_packets=0, n_bytes=0, idle_age=6, priority=100,ipv6,reg0=0x2/0x2,metadata=0x7 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=6.673s, table=53, n_packets=0, n_bytes=0, idle_age=6, priority=100,ip,reg0=0x2/0x2,metadata=0x7 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=6.674s, table=53, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,54) cookie=0x0, duration=6.674s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=90,ip,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70,nw_dst=255.255.255.255 actions=resubmit(,55) cookie=0x0, duration=6.673s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=90,ip,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70,nw_dst=192.168.1.11 actions=resubmit(,55) cookie=0x0, duration=6.673s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=90,ip,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70,nw_dst=224.0.0.0/4 actions=resubmit(,55) cookie=0x0, duration=6.670s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=80,ip,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70 actions=drop cookie=0x0, duration=6.670s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=80,ipv6,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70 actions=drop cookie=0x0, duration=6.674s, table=54, n_packets=0, n_bytes=0, idle_age=6, priority=0,metadata=0x7 actions=resubmit(,55) cookie=0x0, duration=6.673s, table=55, n_packets=0, n_bytes=0, idle_age=6, priority=100,metadata=0x7, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,64) cookie=0x0, duration=6.674s, table=55, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:b6:91:70 actions=resubmit(,64) cookie=0x0, duration=6.673s, table=55, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg7=0x1,metadata=0x7 actions=resubmit(,64) cookie=0x0, duration=6.670s, table=55, n_packets=0, n_bytes=0, idle_age=6, priority=50,reg7=0x2,metadata=0x7 actions=resubmit(,64) #. On compute nodes containing a port on the network, the OVN controller also creates an additional flow. .. code-block:: console cookie=0x0, duration=13.358s, table=52, n_packets=0, n_bytes=0, idle_age=13, priority=2002,ct_state=+new+trk,ipv6,reg7=0x3, metadata=0x7,ipv6_src=:: actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) .. todo: Future commit Attach the router to a second self-service network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. todo: Add after NAT patches merge. Attach the router to an external network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ networking-ovn-4.0.0/doc/source/admin/refarch/refarch.rst0000666000175100017510000002054413245511164023452 0ustar zuulzuul00000000000000====================== Reference architecture ====================== The reference architecture defines the minimum environment necessary to deploy OpenStack with Open Virtual Network (OVN) integration for the Networking service in production with sufficient expectations of scale and performance. For evaluation purposes, you can deploy this environment using the :ref:`Installation Guide ` or `Vagrant `_. Any scaling or performance evaluations should use bare metal instead of virtual machines. Layout ------ The reference architecture includes a minimum of four nodes. The controller node contains the following components that provide enough functionality to launch basic instances: * One network interface for management * Identity service * Image service * Networking management with ML2 mechanism driver for OVN (control plane) * Compute management (control plane) The database node contains the following components: * One network interface for management * OVN northbound service (``ovn-northd``) * Open vSwitch (OVS) database service (``ovsdb-server``) for the OVN northbound database (``ovnnb.db``) * Open vSwitch (OVS) database service (``ovsdb-server``) for the OVN southbound database (``ovnsb.db``) .. note:: For functional evaluation only, you can combine the controller and database nodes. The two compute nodes contain the following components: * Two or three network interfaces for management, overlay networks, and optionally provider networks * Compute management (hypervisor) * Hypervisor (KVM) * OVN controller service (``ovn-controller``) * OVS data plane service (``ovs-vswitchd``) * OVS database service (``ovsdb-server``) with OVS local configuration (``conf.db``) database * OVN metadata agent (``ovn-metadata-agent``) The gateway nodes contain the following components: * Three network interfaces for management, overlay networks and provider networks. * OVN controller service (``ovn-controller``) * OVS data plane service (``ovs-vswitchd``) * OVS database service (``ovsdb-server``) with OVS local configuration (``conf.db``) database .. note:: Each OVN metadata agent provides metadata service locally on the compute nodes in a lightweight way. Each network being accessed by the instances of the compute node will have a corresponding metadata ovn-metadata-$net_uuid namespace, and inside an haproxy will funnel the requests to the ovn-metadata-agent over a unix socket. Such namespace can be very helpful for debug purposes to access the local instances on the compute node. If you login as root on such compute node you can execute: ip netns ovn-metadata-$net_uuid exec ssh user@my.instance.ip.address .. image:: figures/ovn-hw.png :alt: Hardware layout :align: center .. image:: figures/ovn-services.png :alt: Service layout :align: center Networking service with OVN integration --------------------------------------- The reference architecture deploys the Networking service with OVN integration as follows, in the case of centralized routing: .. image:: figures/ovn-architecture-centralized-routing1.png :alt: Architecture for Networking service with OVN integration # TODO(majopela): This depends on patch https://review.openstack.org/#/c/463928/ # in the case of DVR: # # .. image:: figures/ovn-architecture1.png # :alt: Architecture for Networking service with OVN integration (DVR) # :align: center Each compute node contains the following network components: .. image:: figures/ovn-compute1.png :alt: Compute node network components :align: center .. note:: The Networking service creates a unique network namespace for each virtual network that enables the metadata service. .. _refarch_database-access: Accessing OVN database content ------------------------------ OVN stores configuration data in a collection of OVS database tables. The following commands show the contents of the most common database tables in the northbound and southbound databases. The example database output in this section uses these commands with various output filters. .. code-block:: console $ ovn-nbctl list Logical_Switch $ ovn-nbctl list Logical_Switch_Port $ ovn-nbctl list ACL $ ovn-nbctl list Address_Set $ ovn-nbctl list Logical_Router $ ovn-nbctl list Logical_Router_Port $ ovn-nbctl list Gateway_Chassis $ ovn-sbctl list Chassis $ ovn-sbctl list Encap $ ovn-nbctl list Address_Set $ ovn-sbctl lflow-list $ ovn-sbctl list Multicast_Group $ ovn-sbctl list Datapath_Binding $ ovn-sbctl list Port_Binding $ ovn-sbctl list MAC_Binding $ ovn-sbctl list Gateway_Chassis .. note:: By default, you must run these commands from the node containing the OVN databases. .. _refarch-adding-compute-node: Adding a compute node --------------------- When you add a compute node to the environment, the OVN controller service on it connects to the OVN southbound database and registers the node as a chassis. .. code-block:: console _uuid : 9be8639d-1d0b-4e3d-9070-03a655073871 encaps : [2fcefdf4-a5e7-43ed-b7b2-62039cc7e32e] external_ids : {ovn-bridge-mappings=""} hostname : "compute1" name : "410ee302-850b-4277-8610-fa675d620cb7" vtep_logical_switches: [] The ``encaps`` field value refers to tunnel endpoint information for the compute node. .. code-block:: console _uuid : 2fcefdf4-a5e7-43ed-b7b2-62039cc7e32e ip : "10.0.0.32" options : {} type : geneve Security Groups/Rules --------------------- Each security group will map to 2 Address_Sets in the OVN NB and SB tables, one for ipv4 and another for ipv6, which will be used to hold ip addresses for the ports that belong to the security group, so that rules with remote_group_id can be efficiently applied. .. todo: add block with openstack security group rule example OVN operations ^^^^^^^^^^^^^^ #. Creating a security group will cause the OVN mechanism driver to create 2 new entries in the Address Set table of the northbound DB: .. code-block:: console _uuid : 9a9d01bd-4afc-4d12-853a-cd21b547911d addresses : [] external_ids : {"neutron:security_group_name"=default} name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" _uuid : 27a91327-636e-4125-99f0-6f2937a3b6d8 addresses : [] external_ids : {"neutron:security_group_name"=default} name : "as_ip6_90a78a43_b549_4bee_8822_21fcccab58dc" In the above entries, the address set name include the protocol (IPv4 or IPv6, written as ip4 or ip6) and the UUID of the Openstack security group, dashes translated to underscores. #. In turn, these new entries will be translated by the OVN northd daemon into entries in the southbound DB: .. code-block:: console _uuid : 886d7b3a-e460-470f-8af2-7c7d88ce45d2 addresses : [] name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" _uuid : 355ddcba-941d-4f1c-b823-dc811cec59ca addresses : [] name : "as_ip6_90a78a43_b549_4bee_8822_21fcccab58dc" Networks -------- .. toctree:: :maxdepth: 1 provider-networks selfservice-networks Routers ------- .. toctree:: :maxdepth: 1 routers .. todo: Explain L3HA modes available starting at OVS 2.8 Instances --------- Launching an instance causes the same series of operations regardless of the network. The following example uses the ``provider`` provider network, ``cirros`` image, ``m1.tiny`` flavor, ``default`` security group, and ``mykey`` key. .. toctree:: :maxdepth: 1 launch-instance-provider-network launch-instance-selfservice-network .. todo: Add north-south when OVN gains support for it. Traffic flows ------------- East-west for instances on the same provider network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ East-west for instances on different provider networks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ East-west for instances on the same self-service network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ East-west for instances on different self-service networks ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ networking-ovn-4.0.0/doc/source/admin/refarch/launch-instance-selfservice-network.rst0000666000175100017510000011517213245511145031114 0ustar zuulzuul00000000000000.. _refarch-launch-instance-selfservice-network: Launch an instance on a self-service network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To launch an instance on a self-service network, follow the same steps as :ref:`launching an instance on the provider network `, but using the UUID of the self-service network. OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations when launching an instance. #. The OVN mechanism driver creates a logical port for the instance. .. code-block:: console _uuid : c754d1d2-a7fb-4dd0-b14c-c076962b06b9 addresses : ["fa:16:3e:15:7d:13 192.168.1.5"] enabled : true external_ids : {"neutron:port_name"=""} name : "eaf36f62-5629-4ec4-b8b9-5e562c40e7ae" options : {} parent_name : [] port_security : ["fa:16:3e:15:7d:13 192.168.1.5"] tag : [] type : "" up : true #. The OVN mechanism driver updates the appropriate Address Set object(s) with the address of the new instance: .. code-block:: console _uuid : d0becdea-e1ed-48c4-9afc-e278cdef4629 addresses : ["192.168.1.5", "203.0.113.103"] external_ids : {"neutron:security_group_name"=default} name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" #. The OVN mechanism driver creates ACL entries for this port and any other ports in the project. .. code-block:: console _uuid : 00ecbe8f-c82a-4e18-b688-af2a1941cff7 action : allow direction : from-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "inport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip4 && (ip4.dst == 255.255.255.255 || ip4.dst == 192.168.1.0/24) && udp && udp.src == 68 && udp.dst == 67" priority : 1002 _uuid : 2bf5b7ed-008e-4676-bba5-71fe58897886 action : allow-related direction : from-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "inport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip4" priority : 1002 _uuid : 330b4e27-074f-446a-849b-9ab0018b65c5 action : allow direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip4 && ip4.src == 192.168.1.0/24 && udp && udp.src == 67 && udp.dst == 68" priority : 1002 _uuid : 683f52f2-4be6-4bd7-a195-6c782daa7840 action : allow-related direction : from-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "inport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip6" priority : 1002 _uuid : 8160f0b4-b344-43d5-bbd4-ca63a71aa4fc action : drop direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip" priority : 1001 _uuid : 97c6b8ca-14ea-4812-8571-95d640a88f4f action : allow-related direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip6" priority : 1002 _uuid : 9cfd8eb5-5daa-422e-8fe8-bd22fd7fa826 action : allow-related direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip4 && ip4.src == 0.0.0.0/0 && icmp4" priority : 1002 _uuid : f72c2431-7a64-4cea-b84a-118bdc761be2 action : drop direction : from-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "inport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip" priority : 1001 _uuid : f94133fa-ed27-4d5e-a806-0d528e539cb3 action : allow-related direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip4 && ip4.src == $as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" priority : 1002 _uuid : 7f7a92ff-b7e9-49b0-8be0-0dc388035df3 action : allow-related direction : to-lport external_ids : {"neutron:lport"="eaf36f62-5629-4ec4-b8b9-5e562c40e7ae"} log : false match : "outport == \"eaf36f62-5629-4ec4-b8b9-5e562c40e7ae\" && ip6 && ip6.src == $as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" priority : 1002 #. The OVN mechanism driver updates the logical switch information with the UUIDs of these objects. .. code-block:: console _uuid : 15e2c80b-1461-4003-9869-80416cd97de5 acls : [00ecbe8f-c82a-4e18-b688-af2a1941cff7, 2bf5b7ed-008e-4676-bba5-71fe58897886, 330b4e27-074f-446a-849b-9ab0018b65c5, 683f52f2-4be6-4bd7-a195-6c782daa7840, 7f7a92ff-b7e9-49b0-8be0-0dc388035df3, 8160f0b4-b344-43d5-bbd4-ca63a71aa4fc, 97c6b8ca-14ea-4812-8571-95d640a88f4f, 9cfd8eb5-5daa-422e-8fe8-bd22fd7fa826, f72c2431-7a64-4cea-b84a-118bdc761be2, f94133fa-ed27-4d5e-a806-0d528e539cb3] external_ids : {"neutron:network_name"="selfservice"} name : "neutron-6cc81cae-8c5f-4c09-aaf2-35d0aa95c084" ports : [2df457a5-f71c-4a2f-b9ab-d9e488653872, 67c2737c-b380-492b-883b-438048b48e56, c754d1d2-a7fb-4dd0-b14c-c076962b06b9] #. With address sets, it is no longer necessary for the OVN mechanism driver to create separate ACLs for other instances in the project. That is handled automagically via address sets. #. The OVN northbound service translates the updated Address Set object(s) into updated Address Set objects in the OVN southbound database: .. code-block:: console _uuid : 2addbee3-7084-4fff-8f7b-15b1efebdaff addresses : ["192.168.1.5", "203.0.113.103"] name : "as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc" #. The OVN northbound service adds a Port Binding for the new Logical Switch Port object: .. code-block:: console _uuid : 7a558e7b-ed7a-424f-a0cf-ab67d2d832d7 chassis : b67d6da9-0222-4ab1-a852-ab2607610bf8 datapath : 3f6e16b5-a03a-48e5-9b60-7b7a0396c425 logical_port : "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" mac : ["fa:16:3e:b6:91:70 192.168.1.5"] options : {} parent_port : [] tag : [] tunnel_key : 3 type : "" #. The OVN northbound service updates the flooding multicast group for the logical datapath with the new port binding: .. code-block:: console _uuid : c08d0102-c414-4a47-98d9-dd3fa9f9901c datapath : 0b214af6-8910-489c-926a-fd0ed16a8251 name : _MC_flood ports : [3e463ca0-951c-46fd-b6cf-05392fa3aa1f, 794a6f03-7941-41ed-b1c6-0e00c1e18da0, fa7b294d-2a62-45ae-8de3-a41c002de6de] tunnel_key : 65535 #. The OVN northbound service adds Logical Flows based on the updated Address Set, ACL and Logical_Switch_Port objects: .. code-block:: console Datapath: 3f6e16b5-a03a-48e5-9b60-7b7a0396c425 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.src == {fa:16:3e:b6:a3:54}), action=(next;) table= 1( ls_in_port_sec_ip), priority= 90, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.src == fa:16:3e:b6:a3:54 && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(next;) table= 1( ls_in_port_sec_ip), priority= 90, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.src == fa:16:3e:b6:a3:54 && ip4.src == {192.168.1.5}), action=(next;) table= 1( ls_in_port_sec_ip), priority= 80, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.src == fa:16:3e:b6:a3:54 && ip), action=(drop;) table= 2( ls_in_port_sec_nd), priority= 90, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.src == fa:16:3e:b6:a3:54 && arp.sha == fa:16:3e:b6:a3:54 && (arp.spa == 192.168.1.5 )), action=(next;) table= 2( ls_in_port_sec_nd), priority= 80, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && (arp || nd)), action=(drop;) table= 3( ls_in_pre_acl), priority= 110, match=(nd), action=(next;) table= 3( ls_in_pre_acl), priority= 100, match=(ip), action=(reg0[0] = 1; next;) table= 6( ls_in_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv), action=(next;) table= 6( ls_in_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv), action=(next;) table= 6( ls_in_acl), priority=65535, match=(ct.inv), action=(drop;) table= 6( ls_in_acl), priority=65535, match=(nd), action=(next;) table= 6( ls_in_acl), priority= 2002, match=(ct.new && (inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip6)), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2002, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip4 && (ip4.dst == 255.255.255.255 || ip4.dst == 192.168.1.0/24) && udp && udp.src == 68 && udp.dst == 67), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2002, match=(ct.new && (inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip4)), action=(reg0[1] = 1; next;) table= 6( ls_in_acl), priority= 2001, match=(inport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip), action=(drop;) table= 6( ls_in_acl), priority= 1, match=(ip), action=(reg0[1] = 1; next;) table= 9( ls_in_arp_nd_rsp), priority= 50, match=(arp.tpa == 192.168.1.5 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:b6:a3:54; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:b6:a3:54; arp.tpa = arp.spa; arp.spa = 192.168.1.5; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:b6:a3:54), action=(outport = "e9cb7857-4cb1-4e91-aae5-165a7ab5b387"; output;) Datapath: 3f6e16b5-a03a-48e5-9b60-7b7a0396c425 Pipeline: egress table= 1( ls_out_pre_acl), priority= 110, match=(nd), action=(next;) table= 1( ls_out_pre_acl), priority= 100, match=(ip), action=(reg0[0] = 1; next;) table= 4( ls_out_acl), priority=65535, match=(nd), action=(next;) table= 4( ls_out_acl), priority=65535, match=(!ct.est && ct.rel && !ct.new && !ct.inv), action=(next;) table= 4( ls_out_acl), priority=65535, match=(ct.est && !ct.rel && !ct.new && !ct.inv), action=(next;) table= 4( ls_out_acl), priority=65535, match=(ct.inv), action=(drop;) table= 4( ls_out_acl), priority= 2002, match=(ct.new && (outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip6 && ip6.src == $as_ip6_90a78a43_b549_4bee_8822_21fcccab58dc)), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2002, match=(ct.new && (outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip4 && ip4.src == $as_ip4_90a78a43_b549_4bee_8822_21fcccab58dc)), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2002, match=(outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip4 && ip4.src == 192.168.1.0/24 && udp && udp.src == 67 && udp.dst == 68), action=(reg0[1] = 1; next;) table= 4( ls_out_acl), priority= 2001, match=(outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && ip), action=(drop;) table= 4( ls_out_acl), priority= 1, match=(ip), action=(reg0[1] = 1; next;) table= 6( ls_out_port_sec_ip), priority= 90, match=(outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.dst == fa:16:3e:b6:a3:54 && ip4.dst == {255.255.255.255, 224.0.0.0/4, 192.168.1.5}), action=(next;) table= 6( ls_out_port_sec_ip), priority= 80, match=(outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.dst == fa:16:3e:b6:a3:54 && ip), action=(drop;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "e9cb7857-4cb1-4e91-aae5-165a7ab5b387" && eth.dst == {fa:16:3e:b6:a3:54}), action=(output;) #. The OVN controller service on each compute node translates these objects into flows on the integration bridge ``br-int``. Exact flows depend on whether the compute node containing the instance also contains a DHCP agent on the subnet. * On the compute node containing the instance, the Compute service creates a port that connects the instance to the integration bridge and OVN creates the following flows: .. code-block:: console # ovs-ofctl show br-int OFPT_FEATURES_REPLY (xid=0x2): dpid:000022024a1dc045 n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst 12(tapeaf36f62-56): addr:fe:16:3e:15:7d:13 config: 0 state: 0 current: 10MB-FD COPPER .. code-block:: console cookie=0x0, duration=179.460s, table=0, n_packets=122, n_bytes=10556, idle_age=1, priority=100,in_port=12 actions=load:0x4->NXM_NX_REG5[],load:0x5->OXM_OF_METADATA[], load:0x3->NXM_NX_REG6[],resubmit(,16) cookie=0x0, duration=187.408s, table=16, n_packets=122, n_bytes=10556, idle_age=1, priority=50,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=resubmit(,17) cookie=0x0, duration=187.408s, table=17, n_packets=2, n_bytes=684, idle_age=84, priority=90,udp,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,nw_src=0.0.0.0,nw_dst=255.255.255.255, tp_src=68,tp_dst=67 actions=resubmit(,18) cookie=0x0, duration=187.408s, table=17, n_packets=98, n_bytes=8276, idle_age=1, priority=90,ip,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,nw_src=192.168.1.5 actions=resubmit(,18) cookie=0x0, duration=187.408s, table=17, n_packets=17, n_bytes=1386, idle_age=55, priority=80,ipv6,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=187.408s, table=17, n_packets=0, n_bytes=0, idle_age=187, priority=80,ip,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=187.408s, table=18, n_packets=5, n_bytes=210, idle_age=10, priority=90,arp,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,arp_spa=192.168.1.5, arp_sha=fa:16:3e:15:7d:13 actions=resubmit(,19) cookie=0x0, duration=187.408s, table=18, n_packets=0, n_bytes=0, idle_age=187, priority=80,icmp6,reg6=0x3,metadata=0x5, icmp_type=135,icmp_code=0 actions=drop cookie=0x0, duration=187.408s, table=18, n_packets=0, n_bytes=0, idle_age=187, priority=80,icmp6,reg6=0x3,metadata=0x5, icmp_type=136,icmp_code=0 actions=drop cookie=0x0, duration=187.408s, table=18, n_packets=0, n_bytes=0, idle_age=187, priority=80,arp,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=33, n_bytes=4081, idle_age=0, priority=100,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=100,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=47.068s, table=22, n_packets=15, n_bytes=1392, idle_age=0, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x5 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x5 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=+inv+trk,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,ct_state=+new+trk,ipv6,reg6=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=16, n_bytes=1922, idle_age=2, priority=2002,ct_state=+new+trk,ip,reg6=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,udp,reg6=0x3,metadata=0x5, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,udp,reg6=0x3,metadata=0x5, nw_dst=192.168.1.0/24,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.069s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ipv6,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ip,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=2, n_bytes=767, idle_age=27, priority=1,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=1,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=179.457s, table=25, n_packets=2, n_bytes=84, idle_age=33, priority=50,arp,metadata=0x5,arp_tpa=192.168.1.5, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:15:7d:13,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163e157d13->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80105->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=187.408s, table=26, n_packets=50, n_bytes=4806, idle_age=1, priority=50,metadata=0x5,dl_dst=fa:16:3e:15:7d:13 actions=load:0x3->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=469.575s, table=33, n_packets=74, n_bytes=7040, idle_age=305, priority=100,reg7=0x4,metadata=0x4 actions=load:0x1->NXM_NX_REG7[],resubmit(,33) cookie=0x0, duration=179.460s, table=34, n_packets=2, n_bytes=684, idle_age=84, priority=100,reg6=0x3,reg7=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.069s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=34, n_bytes=4455, idle_age=0, priority=100,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=100,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=47.069s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=+inv+trk,metadata=0x5 actions=drop cookie=0x0, duration=47.069s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=22, n_bytes=2000, idle_age=0, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x5 actions=resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x5 actions=resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=2002,ct_state=+new+trk,ip,reg7=0x3, metadata=0x5,nw_src=192.168.1.5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=2002,ct_state=+new+trk,ip,reg7=0x3, metadata=0x5,nw_src=203.0.113.103 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=3, n_bytes=1141, idle_age=27, priority=2002,udp,reg7=0x3,metadata=0x5, nw_src=192.168.1.0/24,tp_src=67,tp_dst=68 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=39.497s, table=52, n_packets=0, n_bytes=0, idle_age=39, priority=2002,ct_state=+new+trk,ipv6,reg7=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ip,reg7=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ipv6,reg7=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=52, n_packets=9, n_bytes=1314, idle_age=2, priority=1,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=47.068s, table=52, n_packets=0, n_bytes=0, idle_age=47, priority=1,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=47.068s, table=54, n_packets=23, n_bytes=2945, idle_age=0, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=192.168.1.11 actions=resubmit(,55) cookie=0x0, duration=47.068s, table=54, n_packets=0, n_bytes=0, idle_age=47, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=255.255.255.255 actions=resubmit(,55) cookie=0x0, duration=47.068s, table=54, n_packets=0, n_bytes=0, idle_age=47, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=224.0.0.0/4 actions=resubmit(,55) cookie=0x0, duration=47.068s, table=54, n_packets=0, n_bytes=0, idle_age=47, priority=80,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=47.068s, table=54, n_packets=0, n_bytes=0, idle_age=47, priority=80,ipv6,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=47.068s, table=55, n_packets=25, n_bytes=3029, idle_age=0, priority=50,reg7=0x3,metadata=0x7, dl_dst=fa:16:3e:15:7d:13 actions=resubmit(,64) cookie=0x0, duration=179.460s, table=64, n_packets=116, n_bytes=10623, idle_age=1, priority=100,reg7=0x3,metadata=0x5 actions=output:12 * For each compute node that only contains a DHCP agent on the subnet, OVN creates the following flows: .. code-block:: console cookie=0x0, duration=192.587s, table=16, n_packets=0, n_bytes=0, idle_age=192, priority=50,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=resubmit(,17) cookie=0x0, duration=192.587s, table=17, n_packets=0, n_bytes=0, idle_age=192, priority=90,ip,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,nw_src=192.168.1.5 actions=resubmit(,18) cookie=0x0, duration=192.587s, table=17, n_packets=0, n_bytes=0, idle_age=192, priority=90,udp,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,nw_src=0.0.0.0, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=resubmit(,18) cookie=0x0, duration=192.587s, table=17, n_packets=0, n_bytes=0, idle_age=192, priority=80,ipv6,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=192.587s, table=17, n_packets=0, n_bytes=0, idle_age=192, priority=80,ip,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=192.587s, table=18, n_packets=0, n_bytes=0, idle_age=192, priority=90,arp,reg6=0x3,metadata=0x5, dl_src=fa:16:3e:15:7d:13,arp_spa=192.168.1.5, arp_sha=fa:16:3e:15:7d:13 actions=resubmit(,19) cookie=0x0, duration=192.587s, table=18, n_packets=0, n_bytes=0, idle_age=192, priority=80,arp,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=192.587s, table=18, n_packets=0, n_bytes=0, idle_age=192, priority=80,icmp6,reg6=0x3,metadata=0x5, icmp_type=135,icmp_code=0 actions=drop cookie=0x0, duration=192.587s, table=18, n_packets=0, n_bytes=0, idle_age=192, priority=80,icmp6,reg6=0x3,metadata=0x5, icmp_type=136,icmp_code=0 actions=drop cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=33, n_bytes=4081, idle_age=0, priority=100,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=47.068s, table=19, n_packets=0, n_bytes=0, idle_age=47, priority=100,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,20) cookie=0x0, duration=47.068s, table=22, n_packets=15, n_bytes=1392, idle_age=0, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x5 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x5 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=65535,ct_state=+inv+trk,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,ct_state=+new+trk,ipv6,reg6=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=16, n_bytes=1922, idle_age=2, priority=2002,ct_state=+new+trk,ip,reg6=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,udp,reg6=0x3,metadata=0x5, nw_dst=255.255.255.255,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2002,udp,reg6=0x3,metadata=0x5, nw_dst=192.168.1.0/24,tp_src=68,tp_dst=67 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.069s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ipv6,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=2001,ip,reg6=0x3,metadata=0x5 actions=drop cookie=0x0, duration=47.068s, table=22, n_packets=2, n_bytes=767, idle_age=27, priority=1,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=47.068s, table=22, n_packets=0, n_bytes=0, idle_age=47, priority=1,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,23) cookie=0x0, duration=179.457s, table=25, n_packets=2, n_bytes=84, idle_age=33, priority=50,arp,metadata=0x5,arp_tpa=192.168.1.5, arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:15:7d:13,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163e157d13->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a80105->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=192.587s, table=26, n_packets=61, n_bytes=5607, idle_age=6, priority=50,metadata=0x5,dl_dst=fa:16:3e:15:7d:13 actions=load:0x3->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=184.640s, table=32, n_packets=61, n_bytes=5607, idle_age=6, priority=100,reg7=0x3,metadata=0x5 actions=load:0x5->NXM_NX_TUN_ID[0..23], set_field:0x3/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30],output:4 cookie=0x0, duration=47.069s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=135, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=110,icmp6,metadata=0x5,icmp_type=136, icmp_code=0 actions=resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=34, n_bytes=4455, idle_age=0, priority=100,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=47.068s, table=49, n_packets=0, n_bytes=0, idle_age=47, priority=100,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[0],resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=65535,ct_state=+inv+trk, metadata=0x5 actions=drop cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=65535,ct_state=-new-est+rel-inv+trk, metadata=0x5 actions=resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=27, n_bytes=2316, idle_age=6, priority=65535,ct_state=-new+est-rel-inv+trk, metadata=0x5 actions=resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2002,ct_state=+new+trk,icmp,reg7=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2002,ct_state=+new+trk,ipv6,reg7=0x3, metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2002,udp,reg7=0x3,metadata=0x5, nw_src=192.168.1.0/24,tp_src=67,tp_dst=68 actions=load:0x1->NXM_NX_REG0[1],resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2002,ct_state=+new+trk,ip,reg7=0x3, metadata=0x5,nw_src=203.0.113.103 actions=load:0x1->NXM_NX_REG0[1],resubmit(,50) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2001,ip,reg7=0x3,metadata=0x5 actions=drop cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=2001,ipv6,reg7=0x3,metadata=0x5 actions=drop cookie=0x0, duration=192.587s, table=52, n_packets=25, n_bytes=2604, idle_age=6, priority=1,ip,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=192.587s, table=52, n_packets=0, n_bytes=0, idle_age=192, priority=1,ipv6,metadata=0x5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=192.587s, table=54, n_packets=0, n_bytes=0, idle_age=192, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=224.0.0.0/4 actions=resubmit(,55) cookie=0x0, duration=192.587s, table=54, n_packets=0, n_bytes=0, idle_age=192, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=255.255.255.255 actions=resubmit(,55) cookie=0x0, duration=192.587s, table=54, n_packets=0, n_bytes=0, idle_age=192, priority=90,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13,nw_dst=192.168.1.5 actions=resubmit(,55) cookie=0x0, duration=192.587s, table=54, n_packets=0, n_bytes=0, idle_age=192, priority=80,ipv6,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=192.587s, table=54, n_packets=0, n_bytes=0, idle_age=192, priority=80,ip,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13 actions=drop cookie=0x0, duration=192.587s, table=55, n_packets=0, n_bytes=0, idle_age=192, priority=50,reg7=0x3,metadata=0x5, dl_dst=fa:16:3e:15:7d:13 actions=resubmit(,64) * For each compute node that contains neither the instance nor a DHCP agent on the subnet, OVN creates the following flows: .. code-block:: console cookie=0x0, duration=189.763s, table=52, n_packets=0, n_bytes=0, idle_age=189, priority=2002,ct_state=+new+trk,ipv6,reg7=0x4, metadata=0x4 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) cookie=0x0, duration=189.763s, table=52, n_packets=0, n_bytes=0, idle_age=189, priority=2002,ct_state=+new+trk,ip,reg7=0x4, metadata=0x4,nw_src=192.168.1.5 actions=load:0x1->NXM_NX_REG0[1],resubmit(,53) networking-ovn-4.0.0/doc/source/admin/refarch/provider-networks.rst0000666000175100017510000007741013245511145025547 0ustar zuulzuul00000000000000.. _refarch-provider-networks: Provider networks ----------------- A provider (external) network bridges instances to physical network infrastructure that provides layer-3 services. In most cases, provider networks implement layer-2 segmentation using VLAN IDs. A provider network maps to a provider bridge on each compute node that supports launching instances on the provider network. You can create more than one provider bridge, each one requiring a unique name and underlying physical network interface to prevent switching loops. Provider networks and bridges can use arbitrary names, but each mapping must reference valid provider network and bridge names. Each provider bridge can contain one ``flat`` (untagged) network and up to the maximum number of ``vlan`` (tagged) networks that the physical network infrastructure supports, typically around 4000. Creating a provider network involves several commands at the host, OVS, and Networking service levels that yield a series of operations at the OVN level to create the virtual network components. The following example creates a ``flat`` provider network ``provider`` using the provider bridge ``br-provider`` and binds a subnet to it. Create a provider network ~~~~~~~~~~~~~~~~~~~~~~~~~ #. On each compute node, create the provider bridge, map the provider network to it, and add the underlying physical or logical (typically a bond) network interface to it. .. code-block:: console # ovs-vsctl --may-exist add-br br-provider -- set bridge br-provider \ protocols=OpenFlow13 # ovs-vsctl set open . external-ids:ovn-bridge-mappings=provider:br-provider # ovs-vsctl --may-exist add-port br-provider INTERFACE_NAME Replace ``INTERFACE_NAME`` with the name of the underlying network interface. .. note:: These commands provide no output if successful. #. On the controller node, source the administrative project credentials. #. On the controller node, create the provider network in the Networking service. In this case, instances and routers in other projects can use the network. .. code-block:: console $ openstack network create --external --share \ --provider-physical-network provider --provider-network-type flat \ provider +---------------------------+--------------------------------------+ | Field | Value | +---------------------------+--------------------------------------+ | admin_state_up | UP | | availability_zone_hints | | | availability_zones | nova | | created_at | 2016-06-15 15:50:37+00:00 | | description | | | id | 0243277b-4aa8-46d8-9e10-5c9ad5e01521 | | ipv4_address_scope | None | | ipv6_address_scope | None | | is_default | False | | mtu | 1500 | | name | provider | | project_id | b1ebf33664df402693f729090cfab861 | | provider:network_type | flat | | provider:physical_network | provider | | provider:segmentation_id | None | | qos_policy_id | None | | router:external | External | | shared | True | | status | ACTIVE | | subnets | 32a61337-c5a3-448a-a1e7-c11d6f062c21 | | tags | [] | | updated_at | 2016-06-15 15:50:37+00:00 | +---------------------------+--------------------------------------+ .. note:: The value of ``--provider-physical-network`` must refer to the provider network name in the mapping. OVN operations ^^^^^^^^^^^^^^ .. todo: I don't like going this deep with headers, so a future patch will probably break this content into multiple files. The OVN mechanism driver and OVN perform the following operations during creation of a provider network. #. The mechanism driver translates the network into a logical switch in the OVN northbound database. .. code-block:: console _uuid : 98edf19f-2dbc-4182-af9b-79cafa4794b6 acls : [] external_ids : {"neutron:network_name"=provider} load_balancer : [] name : "neutron-e4abf6df-f8cf-49fd-85d4-3ea399f4d645" ports : [92ee7c2f-cd22-4cac-a9d9-68a374dc7b17] .. note:: The ``neutron:network_name`` field in ``external_ids`` contains the network name and ``name`` contains the network UUID. #. In addition, because the provider network is handled by a separate bridge, the following logical port is created in the OVN northbound database. .. code-block:: console _uuid : 92ee7c2f-cd22-4cac-a9d9-68a374dc7b17 addresses : [unknown] enabled : [] external_ids : {} name : "provnet-e4abf6df-f8cf-49fd-85d4-3ea399f4d645" options : {network_name=provider} parent_name : [] port_security : [] tag : [] type : localnet up : false #. The OVN northbound service translates these objects into datapath bindings, port bindings, and the appropriate multicast groups in the OVN southbound database. * Datapath bindings .. code-block:: console _uuid : f1f0981f-a206-4fac-b3a1-dc2030c9909f external_ids : {logical-switch="98edf19f-2dbc-4182-af9b-79cafa4794b6"} tunnel_key : 109 * Port bindings .. code-block:: console _uuid : 8427506e-46b5-41e5-a71b-a94a6859e773 chassis : [] datapath : f1f0981f-a206-4fac-b3a1-dc2030c9909f logical_port : "provnet-e4abf6df-f8cf-49fd-85d4-3ea399f4d645" mac : [unknown] options : {network_name=provider} parent_port : [] tag : [] tunnel_key : 1 type : localnet * Logical flows .. code-block:: console Datapath: f1f0981f-a206-4fac-b3a1-dc2030c9909f Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 100, match=(eth.src[40]), action=(drop;) table= 0( ls_in_port_sec_l2), priority= 100, match=(vlan.present), action=(drop;) table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "provnet-e4abf6df-f8cf-49fd-85d4-3ea399f4d645"), action=(next;) table= 1( ls_in_port_sec_ip), priority= 0, match=(1), action=(next;) table= 2( ls_in_port_sec_nd), priority= 0, match=(1), action=(next;) table= 3( ls_in_pre_acl), priority= 0, match=(1), action=(next;) table= 4( ls_in_pre_lb), priority= 0, match=(1), action=(next;) table= 5( ls_in_pre_stateful), priority= 100, match=(reg0[0] == 1), action=(ct_next;) table= 5( ls_in_pre_stateful), priority= 0, match=(1), action=(next;) table= 6( ls_in_acl), priority= 0, match=(1), action=(next;) table= 7( ls_in_lb), priority= 0, match=(1), action=(next;) table= 8( ls_in_stateful), priority= 100, match=(reg0[1] == 1), action=(ct_commit; next;) table= 8( ls_in_stateful), priority= 100, match=(reg0[2] == 1), action=(ct_lb;) table= 8( ls_in_stateful), priority= 0, match=(1), action=(next;) table= 9( ls_in_arp_rsp), priority= 100, match=(inport == "provnet-e4abf6df-f8cf-49fd-85d4-3ea399f4d645"), action=(next;) table= 9( ls_in_arp_rsp), priority= 0, match=(1), action=(next;) table=10( ls_in_l2_lkup), priority= 100, match=(eth.mcast), action=(outport = "_MC_flood"; output;) table=10( ls_in_l2_lkup), priority= 0, match=(1), action=(outport = "_MC_unknown"; output;) Datapath: f1f0981f-a206-4fac-b3a1-dc2030c9909f Pipeline: egress table= 0( ls_out_pre_lb), priority= 0, match=(1), action=(next;) table= 1( ls_out_pre_acl), priority= 0, match=(1), action=(next;) table= 2(ls_out_pre_stateful), priority= 100, match=(reg0[0] == 1), action=(ct_next;) table= 2(ls_out_pre_stateful), priority= 0, match=(1), action=(next;) table= 3( ls_out_lb), priority= 0, match=(1), action=(next;) table= 4( ls_out_acl), priority= 0, match=(1), action=(next;) table= 5( ls_out_stateful), priority= 100, match=(reg0[1] == 1), action=(ct_commit; next;) table= 5( ls_out_stateful), priority= 100, match=(reg0[2] == 1), action=(ct_lb;) table= 5( ls_out_stateful), priority= 0, match=(1), action=(next;) table= 6( ls_out_port_sec_ip), priority= 0, match=(1), action=(next;) table= 7( ls_out_port_sec_l2), priority= 100, match=(eth.mcast), action=(output;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "provnet-e4abf6df-f8cf-49fd-85d4-3ea399f4d645"), action=(output;) * Multicast groups .. code-block:: console _uuid : 0102f08d-c658-4d0a-a18a-ec8adcaddf4f datapath : f1f0981f-a206-4fac-b3a1-dc2030c9909f name : _MC_unknown ports : [8427506e-46b5-41e5-a71b-a94a6859e773] tunnel_key : 65534 _uuid : fbc38e51-ac71-4c57-a405-e6066e4c101e datapath : f1f0981f-a206-4fac-b3a1-dc2030c9909f name : _MC_flood ports : [8427506e-46b5-41e5-a71b-a94a6859e773] tunnel_key : 65535 Create a subnet on the provider network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The provider network requires at least one subnet that contains the IP address allocation available for instances, default gateway IP address, and metadata such as name resolution. #. On the controller node, create a subnet bound to the provider network ``provider``. .. code-block:: console $ openstack subnet create --network provider --subnet-range \ 203.0.113.0/24 --allocation-pool start=203.0.113.101,end=203.0.113.250 \ --dns-nameserver 8.8.8.8,8.8.4.4 --gateway 203.0.113.1 provider-v4 +-------------------+--------------------------------------+ | Field | Value | +-------------------+--------------------------------------+ | allocation_pools | 203.0.113.101-203.0.113.250 | | cidr | 203.0.113.0/24 | | created_at | 2016-06-15 15:50:45+00:00 | | description | | | dns_nameservers | 8.8.8.8, 8.8.4.4 | | enable_dhcp | True | | gateway_ip | 203.0.113.1 | | host_routes | | | id | 32a61337-c5a3-448a-a1e7-c11d6f062c21 | | ip_version | 4 | | ipv6_address_mode | None | | ipv6_ra_mode | None | | name | provider-v4 | | network_id | 0243277b-4aa8-46d8-9e10-5c9ad5e01521 | | project_id | b1ebf33664df402693f729090cfab861 | | subnetpool_id | None | | updated_at | 2016-06-15 15:50:45+00:00 | +-------------------+--------------------------------------+ If using DHCP to manage instance IP addresses, adding a subnet causes a series of operations in the Networking service and OVN. * The Networking service schedules the network on appropriate number of DHCP agents. The example environment contains three DHCP agents. * Each DHCP agent spawns a network namespace with a ``dnsmasq`` process using an IP address from the subnet allocation. * The OVN mechanism driver creates a logical switch port object in the OVN northbound database for each ``dnsmasq`` process. OVN operations ^^^^^^^^^^^^^^ The OVN mechanism driver and OVN perform the following operations during creation of a subnet on the provider network. #. If the subnet uses DHCP for IP address management, create logical ports ports for each DHCP agent serving the subnet and bind them to the logical switch. In this example, the subnet contains two DHCP agents. .. code-block:: console _uuid : 5e144ab9-3e08-4910-b936-869bbbf254c8 addresses : ["fa:16:3e:57:f9:ca 203.0.113.101"] enabled : true external_ids : {"neutron:port_name"=""} name : "6ab052c2-7b75-4463-b34f-fd3426f61787" options : {} parent_name : [] port_security : [] tag : [] type : "" up : true _uuid : 38cf8b52-47c4-4e93-be8d-06bf71f6a7c9 addresses : ["fa:16:3e:e0:eb:6d 203.0.113.102"] enabled : true external_ids : {"neutron:port_name"=""} name : "94aee636-2394-48bc-b407-8224ab6bb1ab" options : {} parent_name : [] port_security : [] tag : [] type : "" up : true _uuid : 924500c4-8580-4d5f-a7ad-8769f6e58ff5 acls : [] external_ids : {"neutron:network_name"=provider} load_balancer : [] name : "neutron-670efade-7cd0-4d87-8a04-27f366eb8941" ports : [38cf8b52-47c4-4e93-be8d-06bf71f6a7c9, 5e144ab9-3e08-4910-b936-869bbbf254c8, a576b812-9c3e-4cfb-9752-5d8500b3adf9] #. The OVN northbound service creates port bindings for these logical ports and adds them to the appropriate multicast group. * Port bindings .. code-block:: console _uuid : 030024f4-61c3-4807-859b-07727447c427 chassis : fc5ab9e7-bc28-40e8-ad52-2949358cc088 datapath : bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 logical_port : "6ab052c2-7b75-4463-b34f-fd3426f61787" mac : ["fa:16:3e:57:f9:ca 203.0.113.101"] options : {} parent_port : [] tag : [] tunnel_key : 2 type : "" _uuid : cc5bcd19-bcae-4e29-8cee-3ec8a8a75d46 chassis : 6a9d0619-8818-41e6-abef-2f3d9a597c03 datapath : bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 logical_port : "94aee636-2394-48bc-b407-8224ab6bb1ab" mac : ["fa:16:3e:e0:eb:6d 203.0.113.102"] options : {} parent_port : [] tag : [] tunnel_key : 3 type : "" * Multicast groups .. code-block:: console _uuid : 39b32ccd-fa49-4046-9527-13318842461e datapath : bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 name : _MC_flood ports : [030024f4-61c3-4807-859b-07727447c427, 904c3108-234d-41c0-b93c-116b7e352a75, cc5bcd19-bcae-4e29-8cee-3ec8a8a75d46] tunnel_key : 65535 #. The OVN northbound service translates the logical ports into additional logical flows in the OVN southbound database. .. code-block:: console Datapath: bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 Pipeline: ingress table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "94aee636-2394-48bc-b407-8224ab6bb1ab"), action=(next;) table= 0( ls_in_port_sec_l2), priority= 50, match=(inport == "6ab052c2-7b75-4463-b34f-fd3426f61787"), action=(next;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 203.0.113.101 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:57:f9:ca; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:57:f9:ca; arp.tpa = arp.spa; arp.spa = 203.0.113.101; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table= 9( ls_in_arp_rsp), priority= 50, match=(arp.tpa == 203.0.113.102 && arp.op == 1), action=(eth.dst = eth.src; eth.src = fa:16:3e:e0:eb:6d; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = fa:16:3e:e0:eb:6d; arp.tpa = arp.spa; arp.spa = 203.0.113.102; outport = inport; inport = ""; /* Allow sending out inport. */ output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:57:f9:ca), action=(outport = "6ab052c2-7b75-4463-b34f-fd3426f61787"; output;) table=10( ls_in_l2_lkup), priority= 50, match=(eth.dst == fa:16:3e:e0:eb:6d), action=(outport = "94aee636-2394-48bc-b407-8224ab6bb1ab"; output;) Datapath: bd0ab2b3-4cf4-4289-9529-ef430f6a89e6 Pipeline: egress table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "6ab052c2-7b75-4463-b34f-fd3426f61787"), action=(output;) table= 7( ls_out_port_sec_l2), priority= 50, match=(outport == "94aee636-2394-48bc-b407-8224ab6bb1ab"), action=(output;) #. For each compute node without a DHCP agent on the subnet: * The OVN controller service translates the logical flows into flows on the integration bridge ``br-int``. .. code-block:: console cookie=0x0, duration=22.303s, table=32, n_packets=0, n_bytes=0, idle_age=22, priority=100,reg7=0xffff,metadata=0x4 actions=load:0x4->NXM_NX_TUN_ID[0..23], set_field:0xffff/0xffffffff->tun_metadata0, move:NXM_NX_REG6[0..14]->NXM_NX_TUN_METADATA0[16..30], output:5,output:4,resubmit(,33) #. For each compute node with a DHCP agent on a subnet: * Creation of a DHCP network namespace adds two virtual switch ports. The first port connects the DHCP agent with ``dnsmasq`` process to the integration bridge and the second port patches the integration bridge to the provider bridge ``br-provider``. .. code-block:: console # ovs-ofctl show br-int OFPT_FEATURES_REPLY (xid=0x2): dpid:000022024a1dc045 n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst 7(tap6ab052c2-7b): addr:00:00:00:00:10:7f config: PORT_DOWN state: LINK_DOWN speed: 0 Mbps now, 0 Mbps max 8(patch-br-int-to): addr:6a:8c:30:3f:d7:dd config: 0 state: 0 speed: 0 Mbps now, 0 Mbps max # ovs-ofctl -O OpenFlow13 show br-provider OFPT_FEATURES_REPLY (OF1.3) (xid=0x2): dpid:0000080027137c4a n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS GROUP_STATS QUEUE_STATS OFPST_PORT_DESC reply (OF1.3) (xid=0x3): 1(patch-provnet-0): addr:fa:42:c5:3f:d7:6f config: 0 state: 0 speed: 0 Mbps now, 0 Mbps max * The OVN controller service translates these logical flows into flows on the integration bridge. .. code-block:: console cookie=0x0, duration=17.731s, table=0, n_packets=3, n_bytes=258, idle_age=16, priority=100,in_port=7 actions=load:0x2->NXM_NX_REG5[],load:0x4->OXM_OF_METADATA[], load:0x2->NXM_NX_REG6[],resubmit(,16) cookie=0x0, duration=17.730s, table=0, n_packets=15, n_bytes=954, idle_age=2, priority=100,in_port=8,vlan_tci=0x0000/0x1000 actions=load:0x1->NXM_NX_REG5[],load:0x4->OXM_OF_METADATA[], load:0x1->NXM_NX_REG6[],resubmit(,16) cookie=0x0, duration=17.730s, table=0, n_packets=0, n_bytes=0, idle_age=17, priority=100,in_port=8,dl_vlan=0 actions=strip_vlan,load:0x1->NXM_NX_REG5[], load:0x4->OXM_OF_METADATA[],load:0x1->NXM_NX_REG6[], resubmit(,16) cookie=0x0, duration=17.732s, table=16, n_packets=0, n_bytes=0, idle_age=17, priority=100,metadata=0x4, dl_src=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop cookie=0x0, duration=17.732s, table=16, n_packets=0, n_bytes=0, idle_age=17, priority=100,metadata=0x4,vlan_tci=0x1000/0x1000 actions=drop cookie=0x0, duration=17.732s, table=16, n_packets=3, n_bytes=258, idle_age=16, priority=50,reg6=0x2,metadata=0x4 actions=resubmit(,17) cookie=0x0, duration=17.732s, table=16, n_packets=0, n_bytes=0, idle_age=17, priority=50,reg6=0x3,metadata=0x4 actions=resubmit(,17) cookie=0x0, duration=17.732s, table=16, n_packets=15, n_bytes=954, idle_age=2, priority=50,reg6=0x1,metadata=0x4 actions=resubmit(,17) cookie=0x0, duration=21.714s, table=17, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,18) cookie=0x0, duration=21.714s, table=18, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,19) cookie=0x0, duration=21.714s, table=19, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,20) cookie=0x0, duration=21.714s, table=20, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,21) cookie=0x0, duration=21.714s, table=21, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x1/0x1,metadata=0x4 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=21.714s, table=21, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x1/0x1,metadata=0x4 actions=ct(table=22,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=21.714s, table=21, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,22) cookie=0x0, duration=21.714s, table=22, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,23) cookie=0x0, duration=21.714s, table=23, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,24) cookie=0x0, duration=21.714s, table=24, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x4/0x4,metadata=0x4 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=21.714s, table=24, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x4/0x4,metadata=0x4 actions=ct(table=25,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=21.714s, table=24, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x2/0x2,metadata=0x4 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=21.714s, table=24, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x2/0x2,metadata=0x4 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,25) cookie=0x0, duration=21.714s, table=24, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,25) cookie=0x0, duration=21.714s, table=25, n_packets=15, n_bytes=954, idle_age=6, priority=100,reg6=0x1,metadata=0x4 actions=resubmit(,26) cookie=0x0, duration=21.714s, table=25, n_packets=0, n_bytes=0, idle_age=21, priority=50,arp,metadata=0x4, arp_tpa=203.0.113.101,arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:f9:5d:f3,load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ef95df3->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a81264->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=21.714s, table=25, n_packets=0, n_bytes=0, idle_age=21, priority=50,arp,metadata=0x4, arp_tpa=203.0.113.102,arp_op=1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[], mod_dl_src:fa:16:3e:f0:a5:9f, load:0x2->NXM_OF_ARP_OP[], move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], load:0xfa163ef0a59f->NXM_NX_ARP_SHA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[], load:0xc0a81265->NXM_OF_ARP_SPA[], move:NXM_NX_REG6[]->NXM_NX_REG7[], load:0->NXM_NX_REG6[],load:0->NXM_OF_IN_PORT[],resubmit(,32) cookie=0x0, duration=21.714s, table=25, n_packets=3, n_bytes=258, idle_age=20, priority=0,metadata=0x4 actions=resubmit(,26) cookie=0x0, duration=21.714s, table=26, n_packets=18, n_bytes=1212, idle_age=6, priority=100,metadata=0x4, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=load:0xffff->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=21.714s, table=26, n_packets=0, n_bytes=0, idle_age=21, priority=50,metadata=0x4,dl_dst=fa:16:3e:f0:a5:9f actions=load:0x3->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=21.714s, table=26, n_packets=0, n_bytes=0, idle_age=21, priority=50,metadata=0x4,dl_dst=fa:16:3e:f9:5d:f3 actions=load:0x2->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=21.714s, table=26, n_packets=0, n_bytes=0, idle_age=21, priority=0,metadata=0x4 actions=load:0xfffe->NXM_NX_REG7[],resubmit(,32) cookie=0x0, duration=17.731s, table=33, n_packets=0, n_bytes=0, idle_age=17, priority=100,reg7=0x2,metadata=0x4 actions=load:0x2->NXM_NX_REG5[],resubmit(,34) cookie=0x0, duration=118.126s, table=33, n_packets=0, n_bytes=0, idle_age=118, hard_age=17, priority=100,reg7=0xfffe,metadata=0x4 actions=load:0x1->NXM_NX_REG5[],load:0x1->NXM_NX_REG7[], resubmit(,34),load:0xfffe->NXM_NX_REG7[] cookie=0x0, duration=118.126s, table=33, n_packets=18, n_bytes=1212, idle_age=2, hard_age=17, priority=100,reg7=0xffff,metadata=0x4 actions=load:0x2->NXM_NX_REG5[],load:0x2->NXM_NX_REG7[], resubmit(,34),load:0x1->NXM_NX_REG5[],load:0x1->NXM_NX_REG7[], resubmit(,34),load:0xffff->NXM_NX_REG7[] cookie=0x0, duration=17.730s, table=33, n_packets=0, n_bytes=0, idle_age=17, priority=100,reg7=0x1,metadata=0x4 actions=load:0x1->NXM_NX_REG5[],resubmit(,34) cookie=0x0, duration=17.697s, table=33, n_packets=0, n_bytes=0, idle_age=17, priority=100,reg7=0x3,metadata=0x4 actions=load:0x1->NXM_NX_REG7[],resubmit(,33) cookie=0x0, duration=17.731s, table=34, n_packets=3, n_bytes=258, idle_age=16, priority=100,reg6=0x2,reg7=0x2,metadata=0x4 actions=drop cookie=0x0, duration=17.730s, table=34, n_packets=15, n_bytes=954, idle_age=2, priority=100,reg6=0x1,reg7=0x1,metadata=0x4 actions=drop cookie=0x0, duration=21.714s, table=48, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,49) cookie=0x0, duration=21.714s, table=49, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,50) cookie=0x0, duration=21.714s, table=50, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x1/0x1,metadata=0x4 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=21.714s, table=50, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x1/0x1,metadata=0x4 actions=ct(table=51,zone=NXM_NX_REG5[0..15]) cookie=0x0, duration=21.714s, table=50, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,51) cookie=0x0, duration=21.714s, table=51, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,52) cookie=0x0, duration=21.714s, table=52, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,53) cookie=0x0, duration=21.714s, table=53, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x4/0x4,metadata=0x4 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=21.714s, table=53, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x4/0x4,metadata=0x4 actions=ct(table=54,zone=NXM_NX_REG5[0..15],nat) cookie=0x0, duration=21.714s, table=53, n_packets=0, n_bytes=0, idle_age=21, priority=100,ipv6,reg0=0x2/0x2,metadata=0x4 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=21.714s, table=53, n_packets=0, n_bytes=0, idle_age=21, priority=100,ip,reg0=0x2/0x2,metadata=0x4 actions=ct(commit,zone=NXM_NX_REG5[0..15]),resubmit(,54) cookie=0x0, duration=21.714s, table=53, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,54) cookie=0x0, duration=21.714s, table=54, n_packets=18, n_bytes=1212, idle_age=6, priority=0,metadata=0x4 actions=resubmit(,55) cookie=0x0, duration=21.714s, table=55, n_packets=18, n_bytes=1212, idle_age=6, priority=100,metadata=0x4, dl_dst=01:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,64) cookie=0x0, duration=21.714s, table=55, n_packets=0, n_bytes=0, idle_age=21, priority=50,reg7=0x3,metadata=0x4 actions=resubmit(,64) cookie=0x0, duration=21.714s, table=55, n_packets=0, n_bytes=0, idle_age=21, priority=50,reg7=0x2,metadata=0x4 actions=resubmit(,64) cookie=0x0, duration=21.714s, table=55, n_packets=0, n_bytes=0, idle_age=21, priority=50,reg7=0x1,metadata=0x4 actions=resubmit(,64) cookie=0x0, duration=21.712s, table=64, n_packets=15, n_bytes=954, idle_age=6, priority=100,reg7=0x3,metadata=0x4 actions=output:7 cookie=0x0, duration=21.711s, table=64, n_packets=3, n_bytes=258, idle_age=20, priority=100,reg7=0x1,metadata=0x4 actions=output:8 networking-ovn-4.0.0/doc/source/admin/index.rst0000666000175100017510000000030013245511164021521 0ustar zuulzuul00000000000000==================== Administration Guide ==================== .. toctree:: :maxdepth: 1 ovn features tutorial faq refarch/refarch dpdk containers troubleshooting networking-ovn-4.0.0/doc/source/admin/faq.rst0000666000175100017510000000447713245511145021203 0ustar zuulzuul00000000000000.. _faq: === FAQ === **Q: Does OVN support DVR or distributed L3 routing?** DVR (Distributed Virtual Router) is typically used to refer to a specific implementation of distributed routers provided by the Neutron L3 agent. The Neutron L3 agent in DVR mode has never been tested with OVN. Support for the Neutron L3 agent is only temporary and will be removed once OVN's native L3 support includes enough functionality. When using OVN's native L3 support, L3 routing is always distributed. **Q: Does OVN support integration with physical switches?** OVN currently integrates with physical switches by optionally using them as VTEP gateways from logical to physical networks and via integrations provided by the Neutron ML2 framework, hierarchical port binding. **Q: What's the status of HA for networking-ovn and OVN?** Typically, multiple copies of neutron-server are run across multiple servers and uses a load balancer. The neutron ML2 mechanism driver provided by networking-ovn supports this deployment model. In addition, multiple copies of neutron-dhcp-agent and neutron-metadata-agent can be run with the option of configuring neutron-dhcp-agent availability zones. The network controller portion of OVN is distributed - an instance of the ovn-controller service runs on every hypervisor. OVN also includes some central components for control purposes. ovn-northd is a centralized service that does some translation between the northbound and southbound databases in OVN. Currently, you only run this service once. You can manage it in an active/passive HA mode using something like Pacemaker. The OVN project plans to allow this service to be horizontally scaled both for scaling and HA reasons. This will allow it to be run in an active/active HA mode. OVN also makes use of ovsdb-server for the OVN northbound and southbound databases. ovsdb-server supports active/passive HA using replication. For more information, see: http://docs.openvswitch.org/en/latest/topics/ovsdb-replication/ A typical deployment would use something like Pacemaker to manage the active/passive HA process. Clients would be pointed at a virtual IP address. When the HA manager detects a failure of the master, the virtual IP would be moved and the passive replica would become the new master. See :doc:`ovn` for links to more details on OVN's architecture. networking-ovn-4.0.0/doc/source/admin/ovn.rst0000666000175100017510000000604513245511145021227 0ustar zuulzuul00000000000000=============== OVN information =============== The original OVN project announcement can be found here: * http://networkheresy.com/2015/01/13/ovn-bringing-native-virtual-networking-to-ovs/ The OVN architecture is described here: * http://openvswitch.org/support/dist-docs/ovn-architecture.7.html Here are two tutorials that help with learning different aspects of OVN: * http://blog.spinhirne.com/p/blog-series.html#introToOVN * http://docs.openvswitch.org/en/latest/tutorials/ovn-sandbox/ There is also an in depth tutorial on using OVN with OpenStack: * http://docs.openvswitch.org/en/latest/tutorials/ovn-openstack/ OVN DB schemas and other man pages: * http://openvswitch.org/support/dist-docs/ovn-nb.5.html * http://openvswitch.org/support/dist-docs/ovn-sb.5.html * http://openvswitch.org/support/dist-docs/ovn-nbctl.8.html * http://openvswitch.org/support/dist-docs/ovn-sbctl.8.html * http://openvswitch.org/support/dist-docs/ovn-northd.8.html * http://openvswitch.org/support/dist-docs/ovn-controller.8.html * http://openvswitch.org/support/dist-docs/ovn-controller-vtep.8.html or find a full list of OVS and OVN man pages here: * http://docs.openvswitch.org/en/latest/ref/ The openvswitch web page includes a list of presentations, some of which are about OVN: * http://openvswitch.org/support/ Here are some direct links to past OVN presentations: * `OVN talk at OpenStack Summit in Boston, Spring 2017 `_ * `OVN talk at OpenStack Summit in Barcelona, Fall 2016 `_ * `OVN talk at OpenStack Summit in Austin, Spring 2016 `_ * OVN Project Update at the OpenStack Summit in Tokyo, Fall 2015 - `Slides `__ - `Video `__ * OVN at OpenStack Summit in Vancouver, Sping 2015 - `Slides `__ - `Video `__ * `OVS Conference 2015 `_ These blog resources may also help with testing and understanding OVN: * http://networkop.co.uk/blog/2016/11/27/ovn-part1/ * http://networkop.co.uk/blog/2016/12/10/ovn-part2/ * https://blog.russellbryant.net/2016/12/19/comparing-openstack-neutron-ml2ovs-and-ovn-control-plane/ * https://blog.russellbryant.net/2016/11/11/ovn-logical-flows-and-ovn-trace/ * https://blog.russellbryant.net/2016/09/29/ovs-2-6-and-the-first-release-of-ovn/ * http://galsagie.github.io/2015/11/23/ovn-l3-deepdive/ * http://blog.russellbryant.net/2015/10/22/openstack-security-groups-using-ovn-acls/ * http://galsagie.github.io/sdn/openstack/ovs/2015/05/30/ovn-deep-dive/ * http://blog.russellbryant.net/2015/05/14/an-ez-bake-ovn-for-openstack/ * http://galsagie.github.io/sdn/openstack/ovs/2015/04/26/ovn-containers/ * http://blog.russellbryant.net/2015/04/21/ovn-and-openstack-status-2015-04-21/ * http://blog.russellbryant.net/2015/04/08/ovn-and-openstack-integration-development-update/ networking-ovn-4.0.0/doc/source/admin/containers.rst0000666000175100017510000001547213245511145022576 0ustar zuulzuul00000000000000.. warning:: The present document has been deprecated and this `guide `_ should be followed instead. However, parent port and tag information from a logical switch port can still be retrieved following the example_ shown in this guide below. Container Integration with OVN ================================= OVN supports virtual networking for both VMs and containers. There are two modes OVN can operate in with respect to containers. The first mode looks just like it does with VMs. If you're running a bunch of containers in a cluster of VMs, OVN can be used to provide a virtual networking overlay for those containers to use. The second mode is very interesting in the context of OpenStack. OVN makes special accommodation for running containers inside of VMs when the networking for those VMs is already being managed by OVN. You can create a special type of port in OVN for these containers and have them directly connected to virtual networks managed by OVN. There are two major benefits of this: * It allows containers to use virtual networks without creating another layer of overlay networks. This reduces networking complexity and increases performance. * It allows arbitrary connections between any VMs and any containers running inside VMs. Creating a Container Port ------------------------------ A container port has two additional attributes that do not exist with a normal Neutron port. First, you must specify the parent port that the VM is using. Second, you must specify a tag. This tag is a VLAN ID today, though that may change in the future. Traffic from the container must be tagged with this VLAN ID by open vSwitch running inside the VM. Traffic destined for the container will arrive on the parent VM port with this VLAN ID. Open vSwitch inside the VM will forward this traffic to the container. These two attributes are not currently supported in the Neutron API. As a result, we are initially allowing these attributes to be set in the 'binding:profile' extension for ports. If this approach gains traction and more general support, we will revisit making this a real extension to the Neutron API. Note that the default /etc/neutron/policy.json does not allow a regular user to set a 'binding:profile'. If you want to allow this, you must update policy.json. To do so, change:: "create_port:binding:profile": "rule:admin_only", to:: "create_port:binding:profile": "", Here is an example of creating a port for a VM, and then creating a port for a container that runs inside of that VM:: $ neutron port-create private Created a new port: +-----------------------+---------------------------------------------------------------------------------+ | Field | Value | +-----------------------+---------------------------------------------------------------------------------+ | admin_state_up | True | | allowed_address_pairs | | | binding:vnic_type | normal | | device_id | | | device_owner | | | fixed_ips | {"subnet_id": "ce5e0d61-10a1-44be-b917-f628616d686a", "ip_address": "10.0.0.3"} | | id | 74e43404-f3c2-4f13-aeec-934db4e2de35 | | mac_address | fa:16:3e:c5:a9:74 | | name | | | network_id | f654265f-baa6-4351-9d76-b5693521c521 | | security_groups | fe25592f-3610-48b9-a114-4ec834c52349 | | status | DOWN | | tenant_id | db75dd6671ef4858a7fed450f1f8e995 | +-----------------------+---------------------------------------------------------------------------------+ $ neutron port-create --binding-profile '{"parent_name":"74e43404-f3c2-4f13-aeec-934db4e2de35","tag":42}' private Created a new port: +-----------------------+---------------------------------------------------------------------------------+ | Field | Value | +-----------------------+---------------------------------------------------------------------------------+ | admin_state_up | True | | allowed_address_pairs | | | binding:vnic_type | normal | | device_id | | | device_owner | | | fixed_ips | {"subnet_id": "ce5e0d61-10a1-44be-b917-f628616d686a", "ip_address": "10.0.0.4"} | | id | be155d07-ecd9-4ad7-91e5-5be60684572a | | mac_address | fa:16:3e:74:ef:82 | | name | | | network_id | f654265f-baa6-4351-9d76-b5693521c521 | | security_groups | fe25592f-3610-48b9-a114-4ec834c52349 | | status | DOWN | | tenant_id | db75dd6671ef4858a7fed450f1f8e995 | +-----------------------+---------------------------------------------------------------------------------+ .. _example: Now we can look at the corresponding logical switch ports in OVN to see that the parent and tag were set as expected:: $ ovn-nbctl lsp-get-parent be155d07-ecd9-4ad7-91e5-5be60684572a 74e43404-f3c2-4f13-aeec-934db4e2de35 $ ovn-nbctl lsp-get-tag be155d07-ecd9-4ad7-91e5-5be60684572a 42 networking-ovn-4.0.0/doc/source/admin/tutorial.rst0000666000175100017510000000041313245511145022261 0ustar zuulzuul00000000000000========================== OpenStack and OVN Tutorial ========================== The OVN project documentation includes an in depth tutorial of using OVN with OpenStack. `OpenStack and OVN Tutorial `_ networking-ovn-4.0.0/playbooks/0000775000175100017510000000000013245511554016536 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/0000775000175100017510000000000013245511554020002 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/rally-dsvm-networking-ovn/0000775000175100017510000000000013245511554025061 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/rally-dsvm-networking-ovn/post.yaml0000666000175100017510000000123113245511145026725 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy rally files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=**/*nose_results.html - --include=**/*testr_results.html.gz - --include=/.testrepository/tmp* - --include=**/*testrepository.subunit.gz - --include=/.tox/*/log/* - --include=/rally-plot/** - --exclude=* - --prune-empty-dirs - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/rally-dsvm-networking-ovn/run.yaml0000666000175100017510000000416713245511145026557 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-rally-dsvm-networking-ovn from old job gate-rally-dsvm-networking-ovn tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn enable_plugin rally git://git.openstack.org/openstack/rally EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export RALLY_SCENARIO=ovn export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi export PROJECTS="openstack/networking-ovn $PROJECTS" export PROJECTS="openstack/rally $PROJECTS" export DEVSTACK_GATE_SETTINGS=/opt/stack/new/networking-ovn/devstack/devstackgaterc function post_test_hook { $BASE/new/rally/tests/ci/rally-gate.sh } export -f post_test_hook cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/install-dsvm-networking-ovn-kuryr/0000775000175100017510000000000013245511554026556 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/install-dsvm-networking-ovn-kuryr/post.yaml0000666000175100017510000000063313245511145030427 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs networking-ovn-4.0.0/playbooks/legacy/install-dsvm-networking-ovn-kuryr/run.yaml0000666000175100017510000000407713245511145030254 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-install-dsvm-networking-ovn-kuryr from old job gate-install-dsvm-networking-ovn-kuryr tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=0 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS=/opt/stack/new/networking-ovn/devstack/devstackgatekuryrrc cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional-py35/0000775000175100017510000000000013245511554026676 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional-py35/post.yaml0000666000175100017510000000063313245511145030547 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional-py35/run.yaml0000666000175100017510000000416513245511145030372 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-networking-ovn-dsvm-functional-py35 from old job gate-networking-ovn-dsvm-functional-py35 tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_UNSTACK=1 export DEVSTACK_GATE_TEMPEST=0 export DEVSTACK_GATE_EXERCISES=0 export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_INSTALL_TESTONLY=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" function gate_hook { bash -xe $BASE/new/networking-ovn/networking_ovn/tests/contrib/gate_hook.sh dsvm-functional-py35 } export -f gate_hook function post_test_hook { bash -xe $BASE/new/networking-ovn/networking_ovn/tests/contrib/post_test_hook.sh dsvm-functional-py35 } export -f post_test_hook cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional/0000775000175100017510000000000013245511554026100 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional/post.yaml0000666000175100017510000000063313245511145027751 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs networking-ovn-4.0.0/playbooks/legacy/networking-ovn-dsvm-functional/run.yaml0000666000175100017510000000413513245511145027571 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-networking-ovn-dsvm-functional from old job gate-networking-ovn-dsvm-functional tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_UNSTACK=1 export DEVSTACK_GATE_TEMPEST=0 export DEVSTACK_GATE_EXERCISES=0 export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_INSTALL_TESTONLY=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" function gate_hook { bash -xe $BASE/new/networking-ovn/networking_ovn/tests/contrib/gate_hook.sh dsvm-functional } export -f gate_hook function post_test_hook { bash -xe $BASE/new/networking-ovn/networking_ovn/tests/contrib/post_test_hook.sh dsvm-functional } export -f post_test_hook cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-post-common.yml0000666000175100017510000000202513245511164024455 0ustar zuulzuul00000000000000# # Copy OVN SB & NB database files: those files can be handy for debugging issues as the # ovsdb files are stored as logs. # - name: Create destination directory to collect OVN database logs file: path={{ ansible_user_dir }}/workspace/logs/ovs_dbs state=directory - name: Collect OVN databases copy: remote_src: true src: '/opt/stack/data/ovs/{{ item }}.db' dest: '{{ ansible_user_dir }}/workspace/logs/ovs_dbs/{{ item }}.txt' with_items: - conf - ovnnb_db - ovnsb_db - name: Compress OVN databases in individual files shell: gzip -9 {{ ansible_user_dir }}/workspace/logs/ovs_dbs/* # # Synchronize files from workspace in node to the zuul log_root which will be stored # - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/0000775000175100017510000000000013245511554027642 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/post.yaml0000666000175100017510000000010513245511145031505 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/run.yaml0000666000175100017510000000527413245511145031340 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-networking-ovn-ovs-release from old job gate-tempest-dsvm-networking-ovn-ovs-release tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS="/opt/stack/new/networking-ovn/devstack/devstackgaterc latest-release" cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-multinode/0000775000175100017510000000000013245511554027415 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-multinode/post.yaml0000666000175100017510000000010513245511145031260 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-multinode/run.yaml0000666000175100017510000000534313245511145031110 0ustar zuulzuul00000000000000- hosts: primary name: Autoconverted job legacy-tempest-dsvm-networking-ovn-multinode from old job gate-tempest-dsvm-networking-ovn-multinode-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi export DEVSTACK_GATE_TOPOLOGY="multinode" # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS=/opt/stack/new/networking-ovn/devstack/devstackgaterc cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/0000775000175100017510000000000013245511554027515 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/post.yaml0000666000175100017510000000010513245511145031360 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/run.yaml0000666000175100017510000000526513245511145031213 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-networking-ovn-ovs-master from old job gate-tempest-dsvm-networking-ovn-ovs-master-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS="/opt/stack/new/networking-ovn/devstack/devstackgaterc master" cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/grenade-dsvm-networking-ovn/0000775000175100017510000000000013245511554025343 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/grenade-dsvm-networking-ovn/post.yaml0000666000175100017510000000063313245511145027214 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs networking-ovn-4.0.0/playbooks/legacy/grenade-dsvm-networking-ovn/run.yaml0000666000175100017510000000447213245511145027040 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-grenade-dsvm-networking-ovn from old job gate-grenade-dsvm-networking-ovn-ubuntu-xenial-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PROJECTS="openstack-dev/grenade openstack/networking-ovn $PROJECTS" export PYTHONUNBUFFERED=true export DEVSTACK_GATE_TEMPEST=1 export DEVSTACK_GATE_GRENADE=pullup export GRENADE_PLUGINRC="enable_grenade_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn" export DEVSTACK_GATE_NEUTRON=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS=/opt/stack/new/networking-ovn/devstack/devstackgaterc cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src/0000775000175100017510000000000013245511554032243 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src/post.yaml0000666000175100017510000000010513245511145034106 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src/run.yaml0000666000175100017510000000546513245511145033743 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src from old job gate-tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn LIBS_FROM_GIT="ovsdbapp" EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" export PROJECTS="openstack/ovsdbapp $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS="/opt/stack/new/networking-ovn/devstack/devstackgaterc latest-release" cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/0000775000175100017510000000000013245511554033702 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000015500000000000011216 Lustar 00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/post.yamlnetworking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/p0000666000175100017510000000010513245511145034056 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml ././@LongLink0000000000000000000000000000015400000000000011215 Lustar 00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/run.yamlnetworking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/r0000666000175100017510000000535213245511145034071 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-networking-ovn-neutron-api-ovs-release from old job gate-tempest-dsvm-networking-ovn-neutron-api-ovs-release-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_NEUTRON=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn openstack/neutron-tempest-plugin $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS="/opt/stack/new/networking-ovn/devstack/devstackgaterc latest-release neutron-api-scenario-tests" cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/0000775000175100017510000000000013245511554031117 5ustar zuulzuul00000000000000networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/post.yaml0000666000175100017510000000010513245511145032762 0ustar zuulzuul00000000000000- hosts: primary tasks: - include: ../tempest-post-common.yml networking-ovn-4.0.0/playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/run.yaml0000666000175100017510000000542113245511145032607 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-networking-ovn-ovs-master-python3 from old job gate-tempest-dsvm-networking-ovn-ovs-master-python3-nv tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x cat << 'EOF' >>"/tmp/dg-local.conf" [[local|localrc]] enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn EOF executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true # Enable PYTHON 3 export DEVSTACK_GATE_USE_PYTHON3=True export DEVSTACK_GATE_NEUTRON=1 export DEVSTACK_GATE_TEMPEST=1 export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi # Because we are testing a non standard project, add # our project repository. This makes zuul do the right # reference magic for testing changes. export PROJECTS="openstack/networking-ovn $PROJECTS" # Keep localrc to be able to set some vars in pre_test_hook export KEEP_LOCALRC=1 function pre_test_hook { if [ -f $BASE/new/networking-ovn/devstack/pre_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/pre_test_hook.sh fi } export -f pre_test_hook function post_test_hook { if [ -f $BASE/new/networking-ovn/devstack/post_test_hook.sh ] ; then . $BASE/new/networking-ovn/devstack/post_test_hook.sh fi } export -f post_test_hook export DEVSTACK_GATE_SETTINGS="/opt/stack/new/networking-ovn/devstack/devstackgaterc master" cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' networking-ovn-4.0.0/devstack/0000775000175100017510000000000013245511554016337 5ustar zuulzuul00000000000000networking-ovn-4.0.0/devstack/devstackgatekuryrrc0000666000175100017510000000243513245511145022353 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # This script is executed in the install-dsvm-networking-ovn-kuryr # OpenStack CI job that runs DevStack + kuryr. You can find the # CI job configuration here: # # http://git.openstack.org/cgit/openstack-infra/project-config/tree/jenkins/jobs/networking-ovn.yaml # export OVERRIDE_ENABLED_SERVICES=kuryr,etcd-server,docker-engine,key,n-api,n-cpu,n-cond,n-sch,n-crt,n-cauth,n-obj,placement-api,g-api,g-reg,c-sch,c-api,c-vol,rabbit,tempest,mysql,dstat,ovn-northd,ovn-controller,q-svc export PROJECTS="openstack/networking-ovn openstack/kuryr $PROJECTS" export DEVSTACK_LOCAL_CONFIG="enable_plugin networking-ovn git://git.openstack.org/openstack/networking-ovn" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin kuryr http://git.openstack.org/openstack/kuryr" networking-ovn-4.0.0/devstack/network_utils.sh0000666000175100017510000000121313245511145021577 0ustar zuulzuul00000000000000# Network utility functions that were copied mostly from # devstack's neutron-legacy script so they could be used # by the networking-ovn devstack plugin function get_ext_gw_interface { # Get ext_gw_interface depending on value of Q_USE_PUBLIC_VETH # This function is copied directly from the devstack neutron-legacy script if [[ "$Q_USE_PUBLIC_VETH" == "True" ]]; then echo $Q_PUBLIC_VETH_EX else # Disable in-band as we are going to use local port # to communicate with VMs sudo ovs-vsctl set Bridge $PUBLIC_BRIDGE \ other_config:disable-in-band=true echo $PUBLIC_BRIDGE fi } networking-ovn-4.0.0/devstack/README.rst0000666000175100017510000000151113245511145020022 0ustar zuulzuul00000000000000====================== Enabling in Devstack ====================== 1. Download devstack and networking-ovn:: git clone https://git.openstack.org/openstack-dev/devstack.git git clone https://git.openstack.org/openstack/networking-ovn.git 2. Add networking-ovn to devstack. The minimal set of critical local.conf additions are the following:: cd devstack cat << EOF >> local.conf > enable_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn > enable_service ovn-northd > enable_service ovn-controller > enable_service networking-ovn-metadata-agent > EOF You can also use the provided example local.conf, or look at its contents to add to your own:: cd devstack cp ../networking-ovn/devstack/local.conf.sample local.conf 3. run devstack:: ./stack.sh networking-ovn-4.0.0/devstack/computenode-local.conf.sample0000666000175100017510000000472213245511145024103 0ustar zuulzuul00000000000000# # Sample DevStack local.conf. # # This sample file is intended to be used when adding an additional compute node # to your test environment. It runs a very minimal set of services. # # For this configuration to work, you *must* set the SERVICE_HOST option to the # IP address of the main DevStack host. You must also set HOST_IP to the IP # address of this host. # [[local|localrc]] DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password ADMIN_PASSWORD=password # The DevStack plugin defaults to using the ovn branch from the official ovs # repo. You can optionally use a different one. For example, you may want to # use the latest patches in blp's ovn branch: #OVN_REPO=https://github.com/blp/ovs-reviews.git #OVN_BRANCH=ovn enable_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn disable_all_services enable_service n-cpu enable_service placement-client enable_service ovn-controller enable_service networking-ovn-metadata-agent # Set this to the address of the main DevStack host running the rest of the # OpenStack services. SERVICE_HOST= RABBIT_HOST=$SERVICE_HOST Q_HOST=$SERVICE_HOST # How to connect to ovsdb-server hosting the OVN SB database OVN_SB_REMOTE=tcp:$SERVICE_HOST:6642 # A UUID to uniquely identify this system. If one is not specified, a random # one will be generated and saved in the file 'ovn-uuid' for re-use in future # DevStack runs. #OVN_UUID= # Whether or not to build custom openvswitch kernel modules from the ovs git # tree. This is enabled by default. This is required unless your distro kernel # includes ovs+conntrack support. This support was first released in Linux 4.3, # and will likely be backported by some distros. #OVN_BUILD_MODULES=False HOST_IP= NOVA_VNC_ENABLED=True NOVNCPROXY_URL=http://$SERVICE_HOST:6080/vnc_auto.html VNCSERVER_LISTEN=$HOST_IP VNCSERVER_PROXYCLIENT_ADDRESS=$VNCSERVER_LISTEN # Skydive #enable_plugin skydive https://github.com/redhat-cip/skydive.git #enable_service skydive-agent # Provider Network # If you want to enable a provider network instead of the default private # network after your DevStack environment installation, you *must* set the # Q_USE_PROVIDER_NETWORKING to True, and give value to both PHYSICAL_NETWORK # and OVS_PHYSICAL_BRIDGE. #Q_USE_PROVIDER_NETWORKING=True #PHYSICAL_NETWORK=providernet #OVS_PHYSICAL_BRIDGE=br-provider #PUBLIC_INTERFACE= networking-ovn-4.0.0/devstack/local.conf.sample0000666000175100017510000001132413245511164021560 0ustar zuulzuul00000000000000# # Sample DevStack local.conf. # # This sample file is intended to be used for your typical DevStack environment # that's running all of OpenStack on a single host. This can also be used as # the first host of a multi-host test environment. # # No changes to this sample configuration are required for this to work. # [[local|localrc]] DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password ADMIN_PASSWORD=password # The DevStack plugin defaults to using the ovn branch from the official ovs # repo. You can optionally use a different one. For example, you may want to # use the latest patches in blp's ovn branch: #OVN_REPO=https://github.com/blp/ovs-reviews.git #OVN_BRANCH=ovn enable_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn enable_service ovn-northd enable_service ovn-controller enable_service networking-ovn-metadata-agent # Use Neutron instead of nova-network disable_service n-net enable_service q-svc # Disable Neutron agents not used with OVN. disable_service q-agt disable_service q-l3 disable_service q-dhcp disable_service q-meta # Enable services, these services depend on neutron plugin. enable_plugin neutron https://git.openstack.org/openstack/neutron enable_service q-trunk #enable_service q-qos # Horizon (the web UI) is enabled by default. You may want to disable # it here to speed up DevStack a bit. enable_service horizon #disable_service horizon # Cinder (OpenStack Block Storage) is disabled by default to speed up # DevStack a bit. You may enable it here if you would like to use it. disable_service cinder c-sch c-api c-vol #enable_service cinder c-sch c-api c-vol # To enable Rally, uncomment the line below #enable_plugin rally https://github.com/openstack/rally master # How to connect to ovsdb-server hosting the OVN NB database. #OVN_NB_REMOTE=tcp:$SERVICE_HOST:6641 # How to connect to ovsdb-server hosting the OVN SB database. #OVN_SB_REMOTE=tcp:$SERVICE_HOST:6642 # A UUID to uniquely identify this system. If one is not specified, a random # one will be generated and saved in the file 'ovn-uuid' for re-use in future # DevStack runs. #OVN_UUID= # If using the OVN native layer-3 service, choose a router scheduler to # manage the distribution of router gateways on hypervisors/chassis. # Default value is leastloaded. #OVN_L3_SCHEDULER=leastloaded # Whether or not to build custom openvswitch kernel modules from the ovs git # tree. This is enabled by default. This is required unless your distro kernel # includes ovs+conntrack support. This support was first released in Linux 4.3, # and will likely be backported by some distros. #OVN_BUILD_MODULES=False # Skydive #enable_plugin skydive https://github.com/redhat-cip/skydive.git #enable_service skydive-analyzer #enable_service skydive-agent # If you want to enable a provider network instead of the default private # network after your DevStack environment installation, you *must* set # the Q_USE_PROVIDER_NETWORKING to True, and also give FIXED_RANGE, # NETWORK_GATEWAY and ALLOCATION_POOL option to the correct value that can # be used in your environment. Specifying Q_AGENT is needed to allow devstack # to run various "ip link set" and "ovs-vsctl" commands for the provider # network setup. #Q_AGENT=openvswitch #Q_USE_PROVIDER_NETWORKING=True #PHYSICAL_NETWORK=providernet #PROVIDER_NETWORK_TYPE=flat #PUBLIC_INTERFACE= #OVS_PHYSICAL_BRIDGE=br-provider #PROVIDER_SUBNET_NAME=provider-subnet # use the following for IPv4 #IP_VERSION=4 #FIXED_RANGE= #NETWORK_GATEWAY= #ALLOCATION_POOL= # use the following for IPv4+IPv6 #IP_VERSION=4+6 #FIXED_RANGE= #NETWORK_GATEWAY= #ALLOCATION_POOL= # IPV6_PROVIDER_FIXED_RANGE= # IPV6_PROVIDER_NETWORK_GATEWAY= # If you wish to use the provider network for public access to the cloud, # set the following #Q_USE_PROVIDERNET_FOR_PUBLIC=True #PUBLIC_NETWORK_NAME= #PUBLIC_NETWORK_GATEWAY= #PUBLIC_PHYSICAL_NETWORK= #IP_VERSION=4 #PUBLIC_SUBNET_NAME= #Q_FLOATING_ALLOCATION_POOL= #FLOATING_RANGE= # NOTE: DO NOT MOVE THESE SECTIONS FROM THE END OF THIS FILE # IF YOU DO, THEY WON'T WORK!!!!! # # Enable Nova automatic host discovery for cell every 2 seconds # Only needed in case of multinode devstack, as otherwise there will be issues # when the 2nd compute node goes online. [[post-config|$NOVA_CONF]] [scheduler] discover_hosts_in_cells_interval = 2 networking-ovn-4.0.0/devstack/db-local.conf.sample0000666000175100017510000000242513245511145022144 0ustar zuulzuul00000000000000# # Sample DevStack local.conf. # # This sample file is intented to be used for running ovn-northd and the # OVN DBs on a separate node. # # For this configuration to work, you *must* set the SERVICE_HOST option to the # IP address of the main DevStack host. # [[local|localrc]] DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password ADMIN_PASSWORD=password # The DevStack plugin defaults to using the ovn branch from the official ovs # repo. You can optionally use a different one. For example, you may want to # use the latest patches in blp's ovn branch: #OVN_REPO=https://github.com/blp/ovs-reviews.git #OVN_BRANCH=ovn enable_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn disable_all_services enable_service ovn-northd # A UUID to uniquely identify this system. If one is not specified, a random # one will be generated and saved in the file 'ovn-uuid' for re-use in future # DevStack runs. #OVN_UUID= # Whether or not to build custom openvswitch kernel modules from the ovs git # tree. This is enabled by default. This is required unless your distro kernel # includes ovs+conntrack support. This support was first released in Linux 4.3, # and will likely be backported by some distros. #OVN_BUILD_MODULES=False networking-ovn-4.0.0/devstack/vtep-local.conf.sample0000666000175100017510000000241513245511145022534 0ustar zuulzuul00000000000000# # Sample DevStack local.conf. # # This sample file is intended for running the HW VTEP emulator on a # separate node. # # For this configuration to work, you *must* set the SERVICE_HOST option to the # IP address of the main DevStack host. # [[local|localrc]] DATABASE_PASSWORD=password RABBIT_PASSWORD=password SERVICE_PASSWORD=password SERVICE_TOKEN=password ADMIN_PASSWORD=password # The DevStack plugin defaults to using the ovn branch from the official ovs # repo. You can optionally use a different one. For example, you may want to # use the latest patches in blp's ovn branch: #OVN_REPO=https://github.com/blp/ovs-reviews.git #OVN_BRANCH=ovn enable_plugin networking-ovn https://git.openstack.org/openstack/networking-ovn disable_all_services enable_service ovn-controller-vtep # A UUID to uniquely identify this system. If one is not specified, a random # one will be generated and saved in the file 'ovn-uuid' for re-use in future # DevStack runs. #OVN_UUID= # Whether or not to build custom openvswitch kernel modules from the ovs git # tree. This is enabled by default. This is required unless your distro kernel # includes ovs+conntrack support. This support was first released in Linux 4.3, # and will likely be backported by some distros. #OVN_BUILD_MODULES=False networking-ovn-4.0.0/devstack/devstackgaterc0000666000175100017510000001263413245511164021261 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # This script is executed in the tempest-dsvm-networking-ovn # OpenStack CI job that runs DevStack + tempest. It is also used by the # rally job. You can find the CI job configuration here: # # http://git.openstack.org/cgit/openstack-infra/project-config/tree/jenkins/jobs/networking-ovn.yaml # OVN_OPTS=$@ OVERRIDE_ENABLED_SERVICES=key,n-api,n-cpu,n-cond,n-sch,n-crt,n-cauth,n-obj,n-api-meta,placement-api,g-api,g-reg,c-sch,c-api,c-vol,rabbit,mysql,dstat,ovn-northd,ovn-controller,q-svc,networking-ovn-metadata-agent,br-ex-tcpdump,br-int-flows,q-trunk export OVERRIDE_ENABLED_SERVICES if [ -z "${RALLY_SCENARIO}" ] ; then # Only include tempest if this is not a rally job. export OVERRIDE_ENABLED_SERVICES=${OVERRIDE_ENABLED_SERVICES},tempest fi export DEVSTACK_LOCAL_CONFIG+=$'\n'"Q_USE_PROVIDERNET_FOR_PUBLIC=True" export DEVSTACK_LOCAL_CONFIG+=$'\n'"PHYSICAL_NETWORK=public" if [[ "${OVN_OPTS}" == *"latest-release"* ]] ; then export DEVSTACK_LOCAL_CONFIG+=$'\n'"OVN_BRANCH=branch-2.8" elif [[ "${OVN_OPTS}" == *"master"* ]] ; then export DEVSTACK_LOCAL_CONFIG+=$'\n'"OVN_BRANCH=master" else echo "No ovs branch specified, using the default from the devstack plugin" fi if [[ "$DEVSTACK_GATE_TOPOLOGY" == "multinode" ]] ; then # NOTE(rtheis): Multinode does not require creating an OVN L3 public network. export DEVSTACK_LOCAL_CONFIG+=$'\n'"OVN_L3_CREATE_PUBLIC_NETWORK=False" # NOTE(rtheis): Configure the enabled services on the compute node. export DEVSTACK_SUBNODE_CONFIG+=$'\n'"ENABLED_SERVICES=n-cpu,dstat,c-vol,c-bak,ovn-controller" # NOTE(rtheis): Configure OVN on the compute node. export DEVSTACK_SUBNODE_CONFIG+=$'\n'"OVN_SB_REMOTE=tcp:\$SERVICE_HOST:6642" export DEVSTACK_SUBNODE_CONFIG+=$'\n'"OVN_NB_REMOTE=tcp:\$SERVICE_HOST:6641" # NOTE(rtheis): Since we are overriding the enabled services, we must # also configure the database and rabbit services on the compute node. export DEVSTACK_SUBNODE_CONFIG+=$'\n'"DATABASE_HOST=\$SERVICE_HOST" export DEVSTACK_SUBNODE_CONFIG+=$'\n'"DATABASE_TYPE=mysql" export DEVSTACK_SUBNODE_CONFIG+=$'\n'"RABBIT_HOST=\$SERVICE_HOST" else export DEVSTACK_LOCAL_CONFIG+=$'\n'"OVN_L3_CREATE_PUBLIC_NETWORK=True" fi # Begin list of exclusions. r="^(?!.*" # exclude the slow tag (part of the default for 'full') r="$r(?:.*\[.*\bslow\b.*\])" # TODO(numans): Remove this if once OVS 2.9 branch is created. if [[ "${OVN_OPTS}" == *"latest-release"* ]] ; then r="$r|(?:tempest\.scenario\.test_network_v6.*)" fi # exclude things that just aren't enabled with OVN r="$r|(?:tempest\.api\.network\.admin\.test_quotas\.QuotasTest\.test_lbaas_quotas.*)" r="$r|(?:tempest\.api\.network\.test_load_balancer.*)" r="$r|(?:tempest\.scenario\.test_load_balancer.*)" r="$r|(?:tempest\.api\.network\.admin\.test_load_balancer.*)" r="$r|(?:tempest\.api\.network\.admin\.test_lbaas.*)" r="$r|(?:tempest\.api\.network\.test_fwaas_extensions.*)" r="$r|(?:tempest\.api\.network\.test_metering_extensions.*)" r="$r|(?:tempest\.thirdparty\.boto\.test_s3.*)" # TODO(dalvarez): remove this exclusion when https://bugs.launchpad.net/tempest/+bug/1728886 is fixed r="$r|(?:tempest\.scenario\.test_network_basic_ops\.TestNetworkBasicOps\.test_port_security_macspoofing_port)" # exclude some unrelated stuff to make networking-ovn targeted runs go faster r="$r|(?:tempest\.api\.identity*)" r="$r|(?:tempest\.api\.image*)" r="$r|(?:tempest\.api\.volume*)" r="$r|(?:tempest\.api\.compute\.images*)" r="$r|(?:tempest\.api\.compute\.keypairs*)" r="$r|(?:tempest\.api\.compute\.certificates*)" r="$r|(?:tempest\.api\.compute\.flavors*)" r="$r|(?:tempest\.api\.compute\.test_quotas*)" r="$r|(?:tempest\.api\.compute\.test_versions*)" r="$r|(?:tempest\.api\.compute\.volumes*)" r="$r|(?:tempest\.api\.compute\.admin\.test_flavor*)" r="$r|(?:tempest\.api\.compute\.admin\.test_volume*)" r="$r|(?:tempest\.api\.compute\.admin\.test_hypervisor*)" r="$r|(?:tempest\.api\.compute\.admin\.test_aggregate*)" r="$r|(?:tempest\.api\.compute\.admin\.test_quota*)" r="$r|(?:tempest\.scenario\.test_volume*)" # End list of exclusions. r="$r)" if [[ "${OVN_OPTS}" == *"neutron-api-scenario-tests"* ]] ; then r="$r((^neutron_tempest_plugin\.api)|(^neutron_tempest_plugin\.scenario)).*$" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin neutron-tempest-plugin git://git.openstack.org/openstack/neutron-tempest-plugin" export DEVSTACK_GATE_TEMPEST=1 export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1 else # only run tempest.api/scenario/thirdparty tests (part of the default for 'full') r="$r(tempest\.(api|scenario|thirdparty)).*$" fi if [ -z $DEVSTACK_GATE_GRENADE ]; then # Do not run the tempest test cases on grenade jobs. By not setting this, # we still do run the tempest smoke tests. The pre-upgraded stack too runs # just the smoke tests. This is how openstack/neutron runs its post-upgrade # tempest tests. export DEVSTACK_GATE_TEMPEST_REGEX="$r" fi networking-ovn-4.0.0/devstack/override-defaults0000666000175100017510000000120113245511145021676 0ustar zuulzuul00000000000000Q_PLUGIN=${Q_PLUGIN:-"ml2"} Q_AGENT=${Q_AGENT:-""} Q_ML2_PLUGIN_MECHANISM_DRIVERS=${Q_ML2_PLUGIN_MECHANISM_DRIVERS:-ovn,logger} Q_ML2_PLUGIN_TYPE_DRIVERS=${Q_ML2_PLUGIN_TYPE_DRIVERS:-local,flat,vlan,geneve} Q_ML2_TENANT_NETWORK_TYPE=${Q_ML2_TENANT_NETWORK_TYPE:-"geneve"} Q_ML2_PLUGIN_GENEVE_TYPE_OPTIONS=${Q_ML2_PLUGIN_GENEVE_TYPE_OPTIONS:-"vni_ranges=1:65536"} ML2_L3_PLUGIN="networking_ovn.l3.l3_ovn.OVNL3RouterPlugin,trunk" # This function is invoked by DevStack's Neutron plugin setup # code and is being overridden here since the OVN devstack # plugin will handle the install. function neutron_plugin_install_agent_packages { : } networking-ovn-4.0.0/devstack/lib/0000775000175100017510000000000013245511554017105 5ustar zuulzuul00000000000000networking-ovn-4.0.0/devstack/lib/networking-ovn0000666000175100017510000004656213245511164022033 0ustar zuulzuul00000000000000#!/bin/bash # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # devstack/plugin.sh # Functions to control the configuration and operation of the OVN service # Dependencies: # # ``functions`` file # ``DEST`` must be defined # ``STACK_USER`` must be defined # ``stack.sh`` calls the entry points in this order: # # - install_ovn # - configure_ovn # - configure_ovn_plugin # - init_ovn # - start_ovn # - stop_ovn # - cleanup_ovn # Save trace setting _XTRACE_NETWORKING_OVN=$(set +o | grep xtrace) set +o xtrace # Libraries that could be installed from source GITREPO["ovsdbapp"]=${OVSDBAPP_REPO:-${GIT_BASE}/openstack/ovsdbapp.git} GITBRANCH["ovsdbapp"]=${OVSDBAPP_BRANCH:-master} GITDIR["ovsdbapp"]=$DEST/ovsdbapp # Defaults # -------- # The git repo to use OVN_REPO=${OVN_REPO:-https://github.com/openvswitch/ovs.git} OVN_REPO_NAME=$(basename ${OVN_REPO} | cut -f1 -d'.') # The project directory NETWORKING_OVN_DIR=$DEST/networking-ovn # The branch to use from $OVN_REPO OVN_BRANCH=${OVN_BRANCH:-master} # How to connect to ovsdb-server hosting the OVN SB database. OVN_SB_REMOTE=${OVN_SB_REMOTE:-tcp:$SERVICE_HOST:6642} # How to connect to ovsdb-server hosting the OVN NB database OVN_NB_REMOTE=${OVN_NB_REMOTE:-tcp:$SERVICE_HOST:6641} # A UUID to uniquely identify this system. If one is not specified, a random # one will be generated. A randomly generated UUID will be saved in a file # 'ovn-uuid' so that the same one will be re-used if you re-run DevStack. OVN_UUID=${OVN_UUID:-} # Whether or not to build the openvswitch kernel module from ovs. This is required # unless the distro kernel includes ovs+conntrack support. OVN_BUILD_MODULES=$(trueorfalse True OVN_BUILD_MODULES) # Whether or not to install the ovs python module from ovs source. This can be # used to test and validate new ovs python features. This should only be used # for development purposes since the ovs python version is controlled by OpenStack # requirements. OVN_INSTALL_OVS_PYTHON_MODULE=$(trueorfalse False OVN_INSTALL_OVS_PYTHON_MODULE) # GENEVE overlay protocol overhead. Defaults to 38 bytes plus the IP version # overhead (20 bytes for IPv4 (default) or 40 bytes for IPv6) which is determined # based on the ML2 overlay_ip_version option. The ML2 framework will use this to # configure the MTU DHCP option. OVN_GENEVE_OVERHEAD=${OVN_GENEVE_OVERHEAD:-38} # This sets whether to create a public network and bridge. # If set to True, a public network and subnet(s) will be created, and a router # will be created to route the default private network to the public one. OVN_L3_CREATE_PUBLIC_NETWORK=$(trueorfalse False OVN_L3_CREATE_PUBLIC_NETWORK) # ml2/config for neutron_sync_mode OVN_NEUTRON_SYNC_MODE=${OVN_NEUTRON_SYNC_MODE:-log} # The type of OVN L3 Scheduler to use. The OVN L3 Scheduler determines the # hypervisor/chassis where a routers gateway should be hosted in OVN. The # default OVN L3 scheduler is leastloaded OVN_L3_SCHEDULER=${OVN_L3_SCHEDULER:-leastloaded} # Neutron directory NEUTRON_DIR=$DEST/neutron OVN_META_CONF=$NEUTRON_CONF_DIR/networking_ovn_metadata_agent.ini # Set variables for building OVS from source OVS_REPO=$OVN_REPO OVS_REPO_NAME=$OVN_REPO_NAME OVS_BRANCH=$OVN_BRANCH OVS_PREFIX=/usr/local OVS_SBINDIR=$OVS_PREFIX/sbin OVS_BINDIR=$OVS_PREFIX/bin OVS_RUNDIR=$OVS_PREFIX/var/run/openvswitch OVS_SHAREDIR=$OVS_PREFIX/share/openvswitch OVS_SCRIPTDIR=$OVS_SHAREDIR/scripts OVS_DATADIR=$DATA_DIR/ovs NETWORKING_OVN_BIN_DIR=$(get_python_exec_prefix) NETWORKING_OVN_METADATA_BINARY="networking-ovn-metadata-agent" # Utility Functions # ----------------- # There are some ovs functions OVN depends on that must be sourced from # the ovs neutron plugins. After doing this, the OVN overrides must be # re-sourced. source $TOP_DIR/lib/neutron_plugins/ovs_base source $TOP_DIR/lib/neutron_plugins/openvswitch_agent source $NETWORKING_OVN_DIR/devstack/override-defaults source $NETWORKING_OVN_DIR/devstack/network_utils.sh # NOTE(rtheis): Function copied from DevStack _neutron_ovs_base_setup_bridge # and _neutron_ovs_base_add_bridge with the call to neutron-ovs-cleanup # removed. The call is not relevant for OVN, as it is specific to the use # of Neutron's OVS agent and hangs when running stack.sh because # neutron-ovs-cleanup uses the OVSDB native interface. function ovn_base_setup_bridge { local bridge=$1 local addbr_cmd="ovs-vsctl --no-wait -- --may-exist add-br $bridge" if [ "$OVS_DATAPATH_TYPE" != "system" ] ; then addbr_cmd="$addbr_cmd -- set Bridge $bridge datapath_type=${OVS_DATAPATH_TYPE}" fi $addbr_cmd ovs-vsctl --no-wait br-set-external-id $bridge bridge-id $bridge } function _ovn_run_process { local service=$1 local cmd="$2" local group=$3 local user=${4:-$STACK_USER} local systemd_service="devstack@$service.service" local unit_file="$SYSTEMD_DIR/$systemd_service" local environment="OVS_RUNDIR=$OVS_RUNDIR OVS_DBDIR=$OVS_DATADIR OVS_LOGDIR=$LOGDIR" echo "Starting $service executed command": $cmd write_user_unit_file $systemd_service "$cmd" "$group" "$user" iniset -sudo $unit_file "Service" "Type" "forking" iniset -sudo $unit_file "Service" "RemainAfterExit" "yes" iniset -sudo $unit_file "Service" "KillMode" "mixed" iniset -sudo $unit_file "Service" "LimitNOFILE" "65536" iniset -sudo $unit_file "Service" "Environment" "$environment" $SYSTEMCTL daemon-reload $SYSTEMCTL enable $systemd_service $SYSTEMCTL restart $systemd_service local testcmd="test -e $OVS_RUNDIR/$service.pid" test_with_retry "$testcmd" "$service did not start" $SERVICE_TIMEOUT 1 sudo ovs-appctl -t $service vlog/set console:off syslog:info file:info } # Entry Points # ------------ # cleanup_ovn() - Remove residual data files, anything left over from previous # runs that a clean run would need to clean up function cleanup_ovn { local _pwd=$(pwd) cd $DEST/$OVN_REPO_NAME sudo make uninstall sudo make distclean cd $_pwd } # configure_ovn() - Set config files, create data dirs, etc function configure_ovn { echo "Configuring OVN" if [ -z "$OVN_UUID" ] ; then if [ -f ./ovn-uuid ] ; then OVN_UUID=$(cat ovn-uuid) else OVN_UUID=$(uuidgen) echo $OVN_UUID > ovn-uuid fi fi # Metadata if is_service_enabled networking-ovn-metadata-agent; then sudo install -d -o $STACK_USER $NEUTRON_CONF_DIR configure_neutron_rootwrap mkdir -p $NETWORKING_OVN_DIR/etc/neutron/plugins/ml2 (cd $NETWORKING_OVN_DIR && exec ./tools/generate_config_file_samples.sh) cp $NETWORKING_OVN_DIR/etc/networking_ovn_metadata_agent.ini.sample $OVN_META_CONF configure_root_helper_options $OVN_META_CONF iniset $OVN_META_CONF DEFAULT debug $ENABLE_DEBUG_LOG_LEVEL iniset $OVN_META_CONF DEFAULT nova_metadata_host $SERVICE_HOST iniset $OVN_META_CONF DEFAULT metadata_workers $API_WORKERS iniset $OVN_META_CONF DEFAULT state_path $NEUTRON_STATE_PATH iniset $OVN_META_CONF ovs ovsdb_connection unix:$OVS_RUNDIR/db.sock iniset $OVN_META_CONF ovn ovn_sb_connection $OVN_SB_REMOTE fi } function configure_ovn_plugin { echo "Configuring Neutron for OVN" if is_service_enabled q-svc ; then # NOTE(arosen) needed for tempest export NETWORK_API_EXTENSIONS=$(python -c \ 'from networking_ovn.common import extensions ;\ print ",".join(extensions.ML2_SUPPORTED_API_EXTENSIONS)') export NETWORK_API_EXTENSIONS=$NETWORK_API_EXTENSIONS,$(python -c \ 'from networking_ovn.common import extensions ;\ print ",".join(extensions.ML2_SUPPORTED_API_EXTENSIONS_OVN_L3)') populate_ml2_config /$Q_PLUGIN_CONF_FILE ml2_type_geneve max_header_size=$OVN_GENEVE_OVERHEAD populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn ovn_nb_connection="$OVN_NB_REMOTE" populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn ovn_sb_connection="$OVN_SB_REMOTE" populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn neutron_sync_mode="$OVN_NEUTRON_SYNC_MODE" populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn ovn_l3_scheduler="$OVN_L3_SCHEDULER" populate_ml2_config /$Q_PLUGIN_CONF_FILE securitygroup enable_security_group="$Q_USE_SECGROUP" inicomment /$Q_PLUGIN_CONF_FILE securitygroup firewall_driver if is_service_enabled networking-ovn-metadata-agent; then populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn ovn_metadata_enabled=True else populate_ml2_config /$Q_PLUGIN_CONF_FILE ovn ovn_metadata_enabled=False fi fi if is_service_enabled q-dhcp ; then die $LINENO "The q-dhcp service must be disabled with OVN." fi iniset $NEUTRON_CONF DEFAULT dhcp_agent_notification False if is_service_enabled q-l3 ; then die $LINENO "The q-l3 service must be disabled with OVN." fi # NOTE(rtheis): OVN currently lacks support for metadata so enabling # config drive is required to provide metadata to instances. if is_service_enabled n-api-meta ; then if is_service_enabled networking-ovn-metadata-agent ; then iniset $NOVA_CONF neutron service_metadata_proxy True else iniset $NOVA_CONF DEFAULT force_config_drive True fi fi } # init_ovn() - Initialize databases, etc. function init_ovn { # clean up from previous (possibly aborted) runs # create required data files # Assumption: this is a dedicated test system and there is nothing important # in the ovn, ovn-nb, or ovs databases. We're going to trash them and # create new ones on each devstack run. mkdir -p $OVS_DATADIR rm -f $OVS_DATADIR/*.db rm -f $OVS_DATADIR/.*.db.~lock~ } # install_ovn() - Collect source and prepare function install_ovn { echo "Installing OVN and dependent packages" # If OVS is already installed, remove it, because we're about to re-install # it from source. for package in openvswitch openvswitch-switch openvswitch-common; do if is_package_installed $package ; then uninstall_package $package fi done if ! is_neutron_enabled ; then # NOTE(rtheis): networking-ovn depends on neutron, so ensure it at # least gets installed and its configuration directory exists (which # is needed by the multinode job). install_neutron sudo install -d -o $STACK_USER $NEUTRON_CONF_DIR fi # Install tox, used to generate the config (see devstack/override-defaults) pip_install tox source $NEUTRON_DIR/devstack/lib/ovs remove_ovs_packages sudo rm -f $OVS_RUNDIR/* compile_ovs $OVN_BUILD_MODULES sudo mkdir -p $OVS_RUNDIR sudo chown $(whoami) $OVS_RUNDIR sudo mkdir -p $OVS_PREFIX/var/log/openvswitch sudo chown $(whoami) $OVS_PREFIX/var/log/openvswitch # Archive log files and create new local log_archive_dir=$LOGDIR/archive mkdir -p $log_archive_dir for logfile in ovs-vswitchd.log ovn-northd.log ovn-controller.log ovn-controller-vtep.log ovs-vtep.log ovsdb-server.log ovsdb-server-nb.log ovsdb-server-sb.log; do if [ -f "$LOGDIR/$logfile" ] ; then mv "$LOGDIR/$logfile" "$log_archive_dir/$logfile.${CURRENT_LOG_TIME}" fi done # Install ovsdbapp from source if requested if use_library_from_git "ovsdbapp"; then git_clone_by_name "ovsdbapp" setup_dev_lib "ovsdbapp" fi setup_develop $DEST/networking-ovn # Install ovs python module from ovs source. if [[ "$OVN_INSTALL_OVS_PYTHON_MODULE" == "True" ]]; then sudo pip uninstall -y ovs sudo pip install -e $DEST/$OVS_REPO_NAME/python fi } function start_ovs { echo "Starting OVS" if is_service_enabled ovn-controller || is_service_enabled ovn-controller-vtep ; then # ovsdb-server and ovs-vswitchd are used privately in OVN as openvswitch service names. enable_service ovsdb-server enable_service ovs-vswitchd ovsdb-tool create $OVS_DATADIR/conf.db $OVS_SHAREDIR/vswitch.ovsschema if is_service_enabled ovn-controller-vtep; then ovsdb-tool create $OVS_DATADIR/vtep.db $OVS_SHAREDIR/vtep.ovsschema fi local dbcmd="$OVS_SBINDIR/ovsdb-server --remote=punix:$OVS_RUNDIR/db.sock --pidfile --detach --log-file" dbcmd+=" --remote=db:Open_vSwitch,Open_vSwitch,manager_options" if is_service_enabled ovn-controller-vtep; then dbcmd+=" --remote=db:hardware_vtep,Global,managers $OVS_DATADIR/vtep.db" fi dbcmd+=" $OVS_DATADIR/conf.db" _ovn_run_process ovsdb-server "$dbcmd" echo "Configuring OVSDB" ovs-vsctl --no-wait set open_vswitch . system-type="devstack" ovs-vsctl --no-wait set open_vswitch . external-ids:system-id="$OVN_UUID" ovs-vsctl --no-wait set open_vswitch . external-ids:ovn-remote="$OVN_SB_REMOTE" ovs-vsctl --no-wait set open_vswitch . external-ids:ovn-bridge="br-int" ovs-vsctl --no-wait set open_vswitch . external-ids:ovn-encap-type="geneve,vxlan" ovs-vsctl --no-wait set open_vswitch . external-ids:ovn-encap-ip="$HOST_IP" ovn_base_setup_bridge br-int ovs-vsctl --no-wait set bridge br-int fail-mode=secure other-config:disable-in-band=true local ovscmd="$OVS_SBINDIR/ovs-vswitchd --log-file --pidfile --detach" _ovn_run_process ovs-vswitchd "$ovscmd" "$STACK_USER" "root" if is_provider_network || [[ $Q_USE_PROVIDERNET_FOR_PUBLIC == "True" ]]; then ovn_base_setup_bridge $OVS_PHYSICAL_BRIDGE ovs-vsctl set open . external-ids:ovn-bridge-mappings=${PHYSICAL_NETWORK}:${OVS_PHYSICAL_BRIDGE} fi if is_service_enabled ovn-controller-vtep ; then ovn_base_setup_bridge br-v vtep-ctl add-ps br-v vtep-ctl set Physical_Switch br-v tunnel_ips=$HOST_IP enable_service ovs-vtep local vtepcmd="$OVS_SCRIPTDIR/ovs-vtep --log-file --pidfile --detach br-v" _ovn_run_process ovs-vtep "$vtepcmd" "$STACK_USER" "root" vtep-ctl set-manager tcp:$HOST_IP:6640 fi fi cd $_pwd } # start_ovn() - Start running processes, including screen function start_ovn { echo "Starting OVN" if is_service_enabled ovn-northd ; then local cmd="$OVS_SCRIPTDIR/ovn-ctl --no-monitor start_northd" _ovn_run_process ovn-northd "$cmd" ovn-nbctl --db=unix:$OVS_RUNDIR/ovnnb_db.sock set-connection ptcp:6641:0.0.0.0 -- set connection . inactivity_probe=60000 ovn-sbctl --db=unix:$OVS_RUNDIR/ovnsb_db.sock set-connection ptcp:6642:0.0.0.0 -- set connection . inactivity_probe=60000 sudo ovs-appctl -t $OVS_RUNDIR/ovnnb_db.ctl vlog/set console:off syslog:info file:info sudo ovs-appctl -t $OVS_RUNDIR/ovnsb_db.ctl vlog/set console:off syslog:info file:info fi if is_service_enabled ovn-controller ; then local cmd="$OVS_SCRIPTDIR/ovn-ctl --no-monitor start_controller" _ovn_run_process ovn-controller "$cmd" "$STACK_USER" "root" fi if is_service_enabled ovn-controller-vtep ; then local cmd="$OVS_BINDIR/ovn-controller-vtep --log-file --pidfile --detach --ovnsb-db=$OVN_SB_REMOTE" _ovn_run_process ovn-controller-vtep "$cmd" "$STACK_USER" "root" fi if is_service_enabled networking-ovn-metadata-agent; then run_process networking-ovn-metadata-agent "$NETWORKING_OVN_BIN_DIR/$NETWORKING_OVN_METADATA_BINARY --config-file $OVN_META_CONF" # Format logging setup_logging $OVN_META_CONF fi if is_service_enabled br-ex-tcpdump ; then # tcpdump monitor on br-ex for ARP, reverse ARP and ICMP v4 / v6 packets sudo ip link set dev $PUBLIC_BRIDGE up run_process br-ex-tcpdump "/usr/sbin/tcpdump -i $PUBLIC_BRIDGE arp or rarp or icmp or icmp6 -enlX" $STACK_USER root fi if is_service_enabled br-int-flows ; then run_process br-int-flows "/bin/sh -c \"set +e; while true; do echo ovs-ofctl dump-flows br-int; ovs-ofctl dump-flows br-int ; sleep 30; done; \"" $STACK_USER root fi } # stop_ovn() - Stop running processes (non-screen) function stop_ovn { if is_service_enabled networking-ovn-metadata-agent; then sudo pkill -9 -f haproxy || : stop_process networking-ovn-metadata-agent fi if is_service_enabled ovn-controller-vtep ; then stop_process ovn-controller-vtep fi if is_service_enabled ovn-controller ; then stop_process ovn-controller fi if is_service_enabled ovn-northd ; then stop_process ovn-northd fi if is_service_enabled ovs-vtep ; then stop_process ovs-vtep fi if is_service_enabled ovs-vswitchd ; then stop_process ovs-vswitchd fi if is_service_enabled ovsdb-server ; then stop_process ovsdb-server fi } # stop_ovs_dp() - Stop OVS datapath function stop_ovs_dp { sudo ovs-dpctl dump-dps | sudo xargs -n1 ovs-dpctl del-dp sudo rmmod vport_geneve sudo rmmod vport_vxlan sudo rmmod openvswitch } function disable_libvirt_apparmor { if ! sudo aa-status --enabled ; then return 0 fi # NOTE(arosen): This is used as a work around to allow newer versions # of libvirt to work with ovs configured ports. See LP#1466631. # requires the apparmor-utils install_package apparmor-utils # disables apparmor for libvirtd sudo aa-complain /etc/apparmor.d/usr.sbin.libvirtd } function create_public_bridge { # Create the public bridge that OVN will use # This logic is based on the devstack neutron-legacy _neutron_configure_router_v4 and _v6 local ext_gw_ifc ext_gw_ifc=$(get_ext_gw_interface) ovs-vsctl --may-exist add-br $ext_gw_ifc -- set bridge $ext_gw_ifc protocols=OpenFlow13 ovs-vsctl set open . external-ids:ovn-bridge-mappings=$PHYSICAL_NETWORK:$ext_gw_ifc if [ -n "$FLOATING_RANGE" ]; then local cidr_len=${FLOATING_RANGE#*/} sudo ip addr add $PUBLIC_NETWORK_GATEWAY/$cidr_len dev $ext_gw_ifc fi # Ensure IPv6 RAs are accepted on the interface with the default route. # This is needed for neutron-based devstack clouds to work in # IPv6-only clouds in the gate. Please do not remove this without # talking to folks in Infra. This fix is based on a devstack fix for # neutron L3 agent: https://review.openstack.org/#/c/359490/. default_route_dev=$(ip route | grep ^default | awk '{print $5}') sudo sysctl -w net.ipv6.conf.$default_route_dev.accept_ra=2 sudo sysctl -w net.ipv6.conf.all.forwarding=1 if [ -n "$IPV6_PUBLIC_RANGE" ]; then local ipv6_cidr_len=${IPV6_PUBLIC_RANGE#*/} sudo ip -6 addr add $IPV6_PUBLIC_NETWORK_GATEWAY/$ipv6_cidr_len dev $ext_gw_ifc # NOTE(numans): Commenting the below code for now as this is breaking # the CI after xenial upgrade. # https://bugs.launchpad.net/networking-ovn/+bug/1648670 # sudo ip -6 route replace $FIXED_RANGE_V6 via $IPV6_PUBLIC_NETWORK_GATEWAY dev $ext_gw_ifc fi sudo ip link set $ext_gw_ifc up } # Restore xtrace $_XTRACE_NETWORKING_OVN networking-ovn-4.0.0/devstack/plugin.sh0000666000175100017510000000460213245511145020171 0ustar zuulzuul00000000000000#!/bin/bash # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # devstack/plugin.sh # networking-ovn actions for devstack plugin framework # Save trace setting _XTRACE_OVN_PLUGIN=$(set +o | grep xtrace) set +o xtrace source $DEST/networking-ovn/devstack/lib/networking-ovn source $TOP_DIR/lib/neutron-legacy # main loop if is_service_enabled q-svc || is_service_enabled ovn-northd || is_service_enabled ovn-controller || is_service_enabled ovn-controller-vtep ; then if [[ "$1" == "stack" && "$2" == "install" ]]; then install_ovn configure_ovn init_ovn # We have to start at install time, because Neutron's post-config # phase runs ovs-vsctl. start_ovs disable_libvirt_apparmor elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then configure_ovn_plugin if is_service_enabled nova; then create_nova_conf_neutron fi start_ovn # If not previously set by another process, set the OVN_*_DB # variables to enable OVN commands from any node. grep -lq 'OVN' ~/.bash_profile || echo -e "\n# Enable OVN commands from any node.\nexport OVN_NB_DB=$OVN_NB_REMOTE\nexport OVN_SB_DB=$OVN_SB_REMOTE" >> ~/.bash_profile elif [[ "$1" == "stack" && "$2" == "extra" ]]; then if [[ "$OVN_L3_CREATE_PUBLIC_NETWORK" == "True" ]]; then if [[ "$NEUTRON_CREATE_INITIAL_NETWORKS" != "True" ]]; then echo "OVN_L3_CREATE_PUBLIC_NETWORK=True is being ignored because" echo "NEUTRON_CREATE_INITIAL_NETWORKS is set to False" else create_public_bridge fi fi fi if [[ "$1" == "unstack" ]]; then stop_ovn stop_ovs_dp cleanup_ovn fi fi # Restore xtrace $_XTRACE_OVN_PLUGIN # Tell emacs to use shell-script-mode ## Local variables: ## mode: shell-script ## End: networking-ovn-4.0.0/devstack/upgrade/0000775000175100017510000000000013245511554017766 5ustar zuulzuul00000000000000networking-ovn-4.0.0/devstack/upgrade/resources.sh0000777000175100017510000000207113245511145022335 0ustar zuulzuul00000000000000#!/bin/bash set -o errexit source $GRENADE_DIR/grenaderc source $GRENADE_DIR/functions source $TOP_DIR/openrc admin admin OVN_TEST_NETWORK=ovn-test-net function early_create { : } function create { local net_id net_id=$(openstack network create $OVN_TEST_NETWORK -f value -c id) resource_save ovn net_id $net_id } function verify_noapi { : } function verify { local net_id net_id=$(resource_get ovn net_id) # verifiy will be called in base stage as well. But ovn-nbctl will be # installed only during the target stage. [ -z $(which ovn-nbctl || true) ] || ovn-nbctl list Logical_Switch neutron-$net_id } function destroy { local net_id net_id=$(resource_get ovn net_id) openstack network delete $net_id } case $1 in "early_create") early_create ;; "create") create ;; "verify_noapi") verify_noapi ;; "verify") verify ;; "destroy") destroy ;; "force_destroy") set +o errexit destroy ;; esac networking-ovn-4.0.0/devstack/upgrade/upgrade.sh0000777000175100017510000000701713245511145021757 0ustar zuulzuul00000000000000echo "*********************************************************************" 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) set -o xtrace # Set for DevStack compatibility source $GRENADE_DIR/grenaderc source $GRENADE_DIR/functions source $TARGET_DEVSTACK_DIR/stackrc set -o errexit TOP_DIR=$TARGET_DEVSTACK_DIR # Get functions from current DevStack source $TARGET_DEVSTACK_DIR/lib/apache source $TARGET_DEVSTACK_DIR/lib/tls source $TARGET_DEVSTACK_DIR/lib/keystone [[ -r $TARGET_DEVSTACK_DIR/lib/neutron ]] && source $TARGET_DEVSTACK_DIR/lib/neutron source $TARGET_DEVSTACK_DIR/lib/neutron-legacy source $TARGET_DEVSTACK_DIR/lib/neutron_plugins/services/l3 source $TARGET_DEVSTACK_DIR/lib/database source $TARGET_DEVSTACK_DIR/lib/nova NW_OVN_DEVSTACK_DIR=$(dirname "$0")/.. source $NW_OVN_DEVSTACK_DIR/lib/networking-ovn export OVN_NEUTRON_SYNC_MODE=repair set -x # Restart rabbitmq. Without this, the tempest test cases on the upgraded stack # fails randomly due to rabbitmq connection problems. sudo service rabbitmq-server restart # We are no more starting OVS agent, delete the dead agents from neutron dead_agents=$(neutron --os-cloud devstack-admin agent-list --alive False -f value -c id || /bin/true) for agent in $dead_agents; do neutron --os-cloud devstack-admin agent-delete $agent || /bin/true done # stop neutron and its agents as the neutron configuration file is going to # be modified now stop_neutron || /bin/true #Re use the exisiting vswitch db ovs_db_file=$(/usr/share/openvswitch/scripts/ovs-ctl --help | grep DBDIR | awk '{gsub(/\:/, ""); printf $2"/"$1"\n"}') mkdir -p $DATA_DIR/ovs cp $ovs_db_file $DATA_DIR/ovs/conf.db install_ovn #uprade the db to the latest ovsschema OVS_SHARE_ROOT=/usr/local/share/openvswitch/ /bin/bash -c ". $OVS_SHARE_ROOT/scripts/ovs-lib; upgrade_db $DATA_DIR/ovs/conf.db $OVS_SHARE_ROOT/vswitch.ovsschema" configure_ovn start_ovs # We need to reconfigure br-ex because install_ovn must have removed the # ovs kernel module thereby removing the br-ex interface. start_ovs # must have recreated the br-ex interface. sudo ip addr add $PUBLIC_NETWORK_GATEWAY/${FLOATING_RANGE#*/} dev br-ex sudo ip link set br-ex up # Reset the openflow protocol in the vswitchd Bridge tables for br in br-int br-ex br-tun; do ovs-vsctl set Bridge $br protocols=[] || /bin/true done disable_libvirt_apparmor upgrade_project ovn $RUN_DIR $BASE_DEVSTACK_BRANCH $TARGET_DEVSTACK_BRANCH neutron_plugin_configure_common Q_PLUGIN_CONF_FILE=$Q_PLUGIN_CONF_PATH/$Q_PLUGIN_CONF_FILENAME Q_ML2_PLUGIN_MECHANISM_DRIVERS=ovn,logger Q_ML2_PLUGIN_TYPE_DRIVERS=local,flat,vlan,geneve,vxlan Q_ML2_TENANT_NETWORK_TYPE="geneve" neutron_plugin_configure_service configure_ovn_plugin if is_service_enabled nova; then create_nova_conf_neutron fi start_ovn ensure_services_started ovn-controller ovn-northd start_neutron_service_and_check start_neutron_agents set +x set +o xtrace echo "*********************************************************************" echo "SUCCESS: End $0" echo "*********************************************************************" networking-ovn-4.0.0/devstack/upgrade/settings0000666000175100017510000000074013245511145021550 0ustar zuulzuul00000000000000register_project_for_upgrade networking-ovn devstack_localrc base disable_service ovn-northd ovn-controller devstack_localrc base enable_service q-agt q-meta q-metering s-account s-container s-object s-proxy devstack_localrc target enable_plugin networking-ovn http://git.openstack.org/openstack/networking-ovn devstack_localrc target PUBLIC_BRIDGE=br-ex devstack_localrc target enable_service s-account s-container s-object s-proxy devstack_localrc target disable_service q-agt networking-ovn-4.0.0/LICENSE0000666000175100017510000002363713245511145015551 0ustar zuulzuul00000000000000 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. networking-ovn-4.0.0/HACKING.rst0000666000175100017510000000024413245511145016327 0ustar zuulzuul00000000000000networking-ovn Style Commandments =============================================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ networking-ovn-4.0.0/tox.ini0000666000175100017510000000620413245511145016046 0ustar zuulzuul00000000000000[tox] minversion = 1.6 envlist = py35,py27,pep8 skipsdist = True [testenv] usedevelop = True install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} setenv = VIRTUAL_ENV={envdir} PYTHONWARNINGS=default::DeprecationWarning deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt whitelist_externals = bash rm commands = {toxinidir}/tools/ostestr_compat_shim.sh {posargs} passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY [testenv:pep8] basepython = python2.7 commands = flake8 {toxinidir}/tools/coding-checks.sh --pylint '{posargs}' doc8 doc/source devstack releasenotes/source vagrant rally-jobs neutron-db-manage --subproject=networking-ovn check_migration [testenv:venv] commands = {posargs} [testenv:functional] setenv = {[testenv]setenv} OS_TEST_PATH=./networking_ovn/tests/functional OS_TEST_TIMEOUT=120 deps = {[testenv]deps} -r{toxinidir}/networking_ovn/tests/functional/requirements.txt [testenv:functional-py35] basepython = python3.5 setenv = {[testenv]setenv} OS_TEST_PATH=./networking_ovn/tests/functional OS_TEST_TIMEOUT=120 deps = {[testenv]deps} [testenv:dsvm] # Fake job to define environment variables shared between dsvm jobs setenv = OS_TEST_TIMEOUT=120 OS_LOG_PATH={env:OS_LOG_PATH:/opt/stack/logs} commands = false [testenv:dsvm-functional] setenv = {[testenv:functional]setenv} {[testenv:dsvm]setenv} deps = {[testenv:functional]deps} commands = {toxinidir}/tools/ostestr_compat_shim.sh {posargs} [testenv:dsvm-functional-py35] basepython = python3.5 setenv = {[testenv:functional]setenv} {[testenv:dsvm]setenv} deps = {[testenv:functional]deps} commands = {toxinidir}/tools/ostestr_compat_shim.sh {posargs} [testenv:cover] commands = python setup.py test --coverage --coverage-package-name=networking_ovn --testr-args='{posargs}' coverage report [testenv:docs] basepython = python2.7 commands = rm -rf doc/build doc8 doc/source devstack releasenotes/source vagrant rally-jobs sphinx-build -W -b html doc/source doc/build/html [testenv:debug] commands = oslo_debug_helper -t networking_ovn/tests {posargs} [testenv:genconfig] commands = mkdir -p etc/neutron/plugins/ml2 oslo-config-generator --config-file etc/oslo-config-generator/ml2_conf.ini oslo-config-generator --config-file etc/oslo-config-generator/networking_ovn_metadata_agent.ini whitelist_externals = mkdir [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [doc8] # File extensions to check extensions = .rst [flake8] # E123, E125 skipped as they are invalid PEP-8. # TODO(dougwig) -- uncomment this to test for remaining linkages # N530 direct neutron imports not allowed show-source = True ignore = E123,E125,N530 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.tmp import-order-style = pep8 [hacking] import_exceptions = networking_ovn local-check-factory = neutron_lib.hacking.checks.factory networking-ovn-4.0.0/.mailmap0000666000175100017510000000013113245511145016145 0ustar zuulzuul00000000000000# Format is: # # networking-ovn-4.0.0/tools/0000775000175100017510000000000013245511554015673 5ustar zuulzuul00000000000000networking-ovn-4.0.0/tools/coding-checks.sh0000777000175100017510000000304113245511145020727 0ustar zuulzuul00000000000000#!/bin/sh # This script is copied from neutron and adapted for networking-ovn. set -eu usage () { echo "Usage: $0 [OPTION]..." echo "Run networking_ovn's coding check(s)" echo "" echo " -Y, --pylint [] Run pylint check on the entire networking_ovn module or just files changed in basecommit (e.g. HEAD~1)" echo " -h, --help Print this usage message" echo exit 0 } join_args() { if [ -z "$scriptargs" ]; then scriptargs="$opt" else scriptargs="$scriptargs $opt" fi } process_options () { i=1 while [ $i -le $# ]; do eval opt=\$$i case $opt in -h|--help) usage;; -Y|--pylint) pylint=1;; *) join_args;; esac i=$((i+1)) done } run_pylint () { local target="${scriptargs:-all}" if [ "$target" = "all" ]; then files="networking_ovn" else case "$target" in *HEAD~[0-9]*) files=$(git diff --diff-filter=AM --name-only $target -- "*.py");; *) echo "$target is an unrecognized basecommit"; exit 1;; esac fi echo "Running pylint..." echo "You can speed this up by running it on 'HEAD~[0-9]' (e.g. HEAD~1, this change only)..." if [ -n "${files}" ]; then pylint --rcfile=.pylintrc --output-format=colorized ${files} else echo "No python changes in this commit, pylint check not required." exit 0 fi } scriptargs= pylint=1 process_options $@ if [ $pylint -eq 1 ]; then run_pylint exit 0 fi networking-ovn-4.0.0/tools/tox_install.sh0000777000175100017510000000440013245511145020566 0ustar zuulzuul00000000000000#!/usr/bin/env bash # Many of neutron's repos suffer from the problem of depending on neutron, # but it not existing on pypi. # This wrapper for tox's package installer will use the existing package # if it exists, else use zuul-cloner if that program exists, else grab it # from neutron master via a hard-coded URL. That last case should only # happen with devs running unit tests locally. # From the tox.ini config page: # install_command=ARGV # default: # pip install {opts} {packages} ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner BRANCH_NAME=master neutron_installed=$(echo "import neutron" | python 2>/dev/null ; echo $?) NEUTRON_DIR=$HOME/neutron set -e set -x install_cmd="pip install -c$1" shift # The devstack based functional tests have neutron checked out in # $NEUTRON_DIR on the test systems - with the change to test in it. # Use this directory if it exists, so that this script installs the # neutron version to test here. # Note that the functional tests use sudo to run tox and thus # variables used for zuul-cloner to check out the correct version are # lost. if [ -d "$NEUTRON_DIR" ]; then echo "FOUND Neutron code at $NEUTRON_DIR - using" $install_cmd -U -e $NEUTRON_DIR elif [ $neutron_installed -eq 0 ]; then echo "ALREADY INSTALLED" > /tmp/tox_install.txt location=$(python -c "import neutron; print(neutron.__file__)") echo "ALREADY INSTALLED at $location" echo "Neutron already installed; using existing package" elif [ -x "$ZUUL_CLONER" ]; then echo "ZUUL CLONER" > /tmp/tox_install.txt # Make this relative to current working directory so that # git clean can remove it. We cannot remove the directory directly # since it is referenced after $install_cmd -e. mkdir -p .tmp NEUTRON_DIR=$(/bin/mktemp -d -p $(pwd)/.tmp) pushd $NEUTRON_DIR $ZUUL_CLONER --cache-dir \ /opt/git \ --branch $BRANCH_NAME \ git://git.openstack.org \ openstack/neutron cd openstack/neutron $install_cmd -e . popd else echo "PIP HARDCODE" > /tmp/tox_install.txt if [ -z "$NEUTRON_PIP_LOCATION" ]; then NEUTRON_PIP_LOCATION="git+https://git.openstack.org/openstack/neutron@$BRANCH_NAME#egg=neutron" fi $install_cmd -U -e ${NEUTRON_PIP_LOCATION} fi $install_cmd -U $* exit $? networking-ovn-4.0.0/tools/generate_config_file_samples.sh0000777000175100017510000000144013245511145024071 0ustar zuulzuul00000000000000#!/bin/sh # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. set -e GEN_CMD=oslo-config-generator if ! type "$GEN_CMD" > /dev/null; then echo "ERROR: $GEN_CMD not installed on the system." exit 1 fi for file in `ls etc/oslo-config-generator/*`; do $GEN_CMD --config-file=$file done set -x networking-ovn-4.0.0/tools/ostestr_compat_shim.sh0000777000175100017510000000025113245511145022314 0ustar zuulzuul00000000000000#!/bin/sh # preserve old behavior of using an arg as a regex when '--' is not present case $@ in (*--*) ostestr $@;; ('') ostestr;; (*) ostestr --regex "$@" esac networking-ovn-4.0.0/.pylintrc0000666000175100017510000000526613245511145016407 0ustar zuulzuul00000000000000# The format of this file isn't really documented; just use --generate-rcfile [MASTER] # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=.git,tests [MESSAGES CONTROL] # TODO: This list is copied from neutron, the options which do not need to be # suppressed have been already removed, some of the remaining options will be # removed by code adjustment. disable= # "F" Fatal errors that prevent further processing import-error, # "I" Informational noise # "E" Error for important programming issues (likely bugs) no-member, # "W" Warnings for stylistic problems or minor programming issues abstract-method, arguments-differ, attribute-defined-outside-init, broad-except, dangerous-default-value, fixme, global-statement, no-init, protected-access, redefined-builtin, redefined-outer-name, signature-differs, unused-argument, unused-import, unused-variable, useless-super-delegation, # "C" Coding convention violations bad-continuation, invalid-name, len-as-condition, misplaced-comparison-constant, missing-docstring, superfluous-parens, ungrouped-imports, wrong-import-order, # "R" Refactor recommendations duplicate-code, no-else-return, no-self-use, too-few-public-methods, too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements [BASIC] # Variable names can be 1 to 31 characters long, with lowercase and underscores variable-rgx=[a-z_][a-z0-9_]{0,30}$ # Argument names can be 2 to 31 characters long, with lowercase and underscores argument-rgx=[a-z_][a-z0-9_]{1,30}$ # Method names should be at least 3 characters long # and be lowercased with underscores method-rgx=([a-z_][a-z0-9_]{2,}|setUp|tearDown)$ # Module names matching module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Don't require docstrings on tests. no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ [FORMAT] # Maximum number of characters on a single line. max-line-length=79 [VARIABLES] # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= [CLASSES] # List of interface methods to ignore, separated by a comma. ignore-iface-methods= [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules= # should use oslo_serialization.jsonutils json [TYPECHECK] # List of module names for which member attributes should not be checked ignored-modules=six.moves,_MovedItems [REPORTS] # Tells whether to display a full report or only the messages reports=no networking-ovn-4.0.0/AUTHORS0000664000175100017510000000741213245511552015605 0ustar zuulzuul0000000000000000037997 Aaron Rosen Akihiro Motoki Akihiro Motoki Ali Sanhaji Amitabha Biswas Andreas Jaeger Anh Tran Armando Migliaccio Arslan Qadeer Assaf Muller Babu Shanmugam Bertrand Lallau Bertrand Lallau Boden R Brad Behle Brian Haley Cao Xuan Hoang Chanda Mendon Chandra S Vejendla Changxun Zhou Chris Cuzner Daniel Alvarez Daniel Mellado Davanum Srinivas Dong Jun Dongcan Ye Doug Hellmann Doug Wiegley Duan Jiong Dustin Lundquist Emilien Macchi Flavio Fernandes Flavio Percoco Gal Sagie Gary Kotton Guoshuai Li Han Zhou Henry Gessau Hong Hui Xiao Hong Hui Xiao Ihar Hrachyshka JUNJIE NAN Jakub Libosvar James E. Blair Jeff Feng Jeremy Stanley John Kasperski Kevin Benton Kyle Mestery LIU Yulong Le Hou Li, Chen Lucas Alvares Gomes Luong Anh Tuan Matthew Kassawara Miguel Angel Ajo Miguel Lavalle Monty Taylor Murali Rangachari Na Nirapada Ghosh Numan Siddique Oleg Bondarev OpenStack Release Bot RYAN D. MOATS Rachappa Goni Ramu Ramamurthy Richard Theis Russell Bryant Ryan Moats Salvatore Salvatore Orlando Sebastien Dupont Simon Pasquier Sisir Chowdhury SÅ‚awek KapÅ‚oÅ„ski Terry Wilson Tong Li Tong Liu Vu Cong Tuan XieYingYun YAMAMOTO Takashi Yushiro FURUKAWA Zongkai LI Zuul armando-migliaccio bailinzhang chen-li da52700 gengchc2 gong yong sheng lilintan lzklibj melissaml mirecki nizam pranabjb qinchunhua ramu.ramamurthy venkata anil venkatamahesh xurong00037997 yaowei zhangyanxian zhangyanxian zongkai networking-ovn-4.0.0/README.rst0000666000175100017510000000154413245511164016225 0ustar zuulzuul00000000000000========================================================= networking-ovn - OpenStack Neutron integration with OVN ========================================================= OVN provides virtual networking for Open vSwitch and is a component of the Open vSwitch project. This project provides integration between OpenStack Neutron and OVN. * Free software: Apache license * Source: https://git.openstack.org/cgit/openstack/networking-ovn * Bugs: https://bugs.launchpad.net/networking-ovn * Mailing list: https://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev * IRC: #openstack-neutron-ovn on Freenode. * Docs: https://docs.openstack.org/networking-ovn/latest Team and repository tags ------------------------ .. image:: https://governance.openstack.org/badges/networking-ovn.svg :target: https://governance.openstack.org/reference/tags/index.html networking-ovn-4.0.0/networking_ovn.egg-info/0000775000175100017510000000000013245511554021276 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn.egg-info/SOURCES.txt0000664000175100017510000002652213245511554023171 0ustar zuulzuul00000000000000.coveragerc .mailmap .pylintrc .stestr.conf .testr.conf AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst babel.cfg requirements.txt setup.cfg setup.py test-requirements.txt tox.ini devstack/README.rst devstack/computenode-local.conf.sample devstack/db-local.conf.sample devstack/devstackgatekuryrrc devstack/devstackgaterc devstack/local.conf.sample devstack/network_utils.sh devstack/override-defaults devstack/plugin.sh devstack/vtep-local.conf.sample devstack/lib/networking-ovn devstack/upgrade/resources.sh devstack/upgrade/settings devstack/upgrade/upgrade.sh doc/source/conf.py doc/source/index.rst doc/source/_static/.placeholder doc/source/admin/containers.rst doc/source/admin/dpdk.rst doc/source/admin/faq.rst doc/source/admin/features.rst doc/source/admin/index.rst doc/source/admin/ovn.rst doc/source/admin/troubleshooting.rst doc/source/admin/tutorial.rst doc/source/admin/refarch/launch-instance-provider-network.rst doc/source/admin/refarch/launch-instance-selfservice-network.rst doc/source/admin/refarch/provider-networks.rst doc/source/admin/refarch/refarch.rst doc/source/admin/refarch/routers.rst doc/source/admin/refarch/selfservice-networks.rst doc/source/admin/refarch/figures/ovn-architecture-centralized-routing1.graffle doc/source/admin/refarch/figures/ovn-architecture-centralized-routing1.png doc/source/admin/refarch/figures/ovn-architecture-centralized-routing1.svg doc/source/admin/refarch/figures/ovn-architecture1.graffle doc/source/admin/refarch/figures/ovn-architecture1.png doc/source/admin/refarch/figures/ovn-architecture1.svg doc/source/admin/refarch/figures/ovn-compute1.graffle doc/source/admin/refarch/figures/ovn-compute1.png doc/source/admin/refarch/figures/ovn-compute1.svg doc/source/admin/refarch/figures/ovn-hw.graffle doc/source/admin/refarch/figures/ovn-hw.png doc/source/admin/refarch/figures/ovn-hw.svg doc/source/admin/refarch/figures/ovn-services.graffle doc/source/admin/refarch/figures/ovn-services.png doc/source/admin/refarch/figures/ovn-services.svg doc/source/configuration/index.rst doc/source/configuration/ml2_conf.rst doc/source/configuration/networking_ovn_metadata_agent.rst doc/source/configuration/samples/ml2_conf.rst doc/source/configuration/samples/networking_ovn_metadata_agent.rst doc/source/contributor/contributing.rst doc/source/contributor/index.rst doc/source/contributor/testing.rst doc/source/contributor/design/data_model.rst doc/source/contributor/design/database_consistency.rst doc/source/contributor/design/index.rst doc/source/contributor/design/metadata_api.rst doc/source/contributor/design/native_dhcp.rst doc/source/contributor/design/ovn_worker.rst doc/source/install/index.rst etc/oslo-config-generator/ml2_conf.ini etc/oslo-config-generator/networking_ovn_metadata_agent.ini migration/README.rst migration/hosts.sample migration/migrate-to-ovn.yml networking_ovn/__init__.py networking_ovn/_i18n.py networking_ovn/ovn_db_sync.py networking_ovn/version.py networking_ovn.egg-info/PKG-INFO networking_ovn.egg-info/SOURCES.txt networking_ovn.egg-info/dependency_links.txt networking_ovn.egg-info/entry_points.txt networking_ovn.egg-info/not-zip-safe networking_ovn.egg-info/pbr.json networking_ovn.egg-info/requires.txt networking_ovn.egg-info/top_level.txt networking_ovn/agent/__init__.py networking_ovn/agent/metadata_agent.py networking_ovn/agent/metadata/__init__.py networking_ovn/agent/metadata/agent.py networking_ovn/agent/metadata/driver.py networking_ovn/agent/metadata/ovsdb.py networking_ovn/agent/metadata/server.py networking_ovn/cmd/__init__.py networking_ovn/cmd/neutron_ovn_db_sync_util.py networking_ovn/cmd/eventlet/__init__.py networking_ovn/cmd/eventlet/agents/__init__.py networking_ovn/cmd/eventlet/agents/metadata.py networking_ovn/common/__init__.py networking_ovn/common/acl.py networking_ovn/common/config.py networking_ovn/common/constants.py networking_ovn/common/exceptions.py networking_ovn/common/extensions.py networking_ovn/common/maintenance.py networking_ovn/common/ovn_client.py networking_ovn/common/utils.py networking_ovn/conf/__init__.py networking_ovn/conf/agent/__init__.py networking_ovn/conf/agent/metadata/__init__.py networking_ovn/conf/agent/metadata/config.py networking_ovn/db/__init__.py networking_ovn/db/head.py networking_ovn/db/maintenance.py networking_ovn/db/models.py networking_ovn/db/revision.py networking_ovn/db/migration/__init__.py networking_ovn/db/migration/alembic_migrations/README networking_ovn/db/migration/alembic_migrations/__init__.py networking_ovn/db/migration/alembic_migrations/env.py networking_ovn/db/migration/alembic_migrations/script.py.mako networking_ovn/db/migration/alembic_migrations/versions/CONTRACT_HEAD networking_ovn/db/migration/alembic_migrations/versions/EXPAND_HEAD networking_ovn/db/migration/alembic_migrations/versions/initial_branchpoint.py networking_ovn/db/migration/alembic_migrations/versions/pike/contract/1d271ead4eb6_initial.py networking_ovn/db/migration/alembic_migrations/versions/pike/expand/ac094507b7f4_initial.py networking_ovn/db/migration/alembic_migrations/versions/pike/expand/e229b8aad9f2_add_journal_and_maintenance_tables.py networking_ovn/db/migration/alembic_migrations/versions/queens/expand/5c198d2723b6_add_ovn_revision_resource_type_as_pk.py networking_ovn/db/migration/alembic_migrations/versions/queens/expand/bc9e24bb9da2_drop_journaling_related_tables.py networking_ovn/db/migration/alembic_migrations/versions/queens/expand/f48286668608_add_ovn_revision_numbers_table.py networking_ovn/l3/__init__.py networking_ovn/l3/l3_ovn.py networking_ovn/l3/l3_ovn_scheduler.py networking_ovn/locale/en_GB/LC_MESSAGES/networking_ovn.po networking_ovn/ml2/__init__.py networking_ovn/ml2/mech_driver.py networking_ovn/ml2/qos_driver.py networking_ovn/ml2/trunk_driver.py networking_ovn/ovsdb/__init__.py networking_ovn/ovsdb/commands.py networking_ovn/ovsdb/impl_idl_ovn.py networking_ovn/ovsdb/ovn_api.py networking_ovn/ovsdb/ovsdb_monitor.py networking_ovn/tests/__init__.py networking_ovn/tests/base.py networking_ovn/tests/contrib/README networking_ovn/tests/contrib/gate_hook.sh networking_ovn/tests/contrib/post_test_hook.sh networking_ovn/tests/functional/__init__.py networking_ovn/tests/functional/base.py networking_ovn/tests/functional/requirements.txt networking_ovn/tests/functional/test_impl_idl.py networking_ovn/tests/functional/test_mech_driver.py networking_ovn/tests/functional/test_ovn_db_resources.py networking_ovn/tests/functional/test_ovn_db_sync.py networking_ovn/tests/functional/test_ovsdb_monitor.py networking_ovn/tests/functional/test_qos_driver.py networking_ovn/tests/functional/test_revision_numbers.py networking_ovn/tests/functional/test_router.py networking_ovn/tests/functional/test_trunk_driver.py networking_ovn/tests/functional/db/__init__.py networking_ovn/tests/functional/db/test_migrations.py networking_ovn/tests/functional/resources/__init__.py networking_ovn/tests/functional/resources/process.py networking_ovn/tests/unit/__init__.py networking_ovn/tests/unit/fakes.py networking_ovn/tests/unit/test_ovn_db_sync.py networking_ovn/tests/unit/test_ovn_parent_tag.py networking_ovn/tests/unit/test_ovn_vtep.py networking_ovn/tests/unit/agent/__init__.py networking_ovn/tests/unit/agent/metadata/__init__.py networking_ovn/tests/unit/agent/metadata/test_agent.py networking_ovn/tests/unit/agent/metadata/test_driver.py networking_ovn/tests/unit/agent/metadata/test_server.py networking_ovn/tests/unit/cmd/__init__.py networking_ovn/tests/unit/cmd/test_neutron_ovn_db_sync_util.py networking_ovn/tests/unit/common/__init__.py networking_ovn/tests/unit/common/test_acl.py networking_ovn/tests/unit/common/test_maintenance.py networking_ovn/tests/unit/db/__init__.py networking_ovn/tests/unit/db/base.py networking_ovn/tests/unit/db/test_maintenance.py networking_ovn/tests/unit/db/test_revision.py networking_ovn/tests/unit/l3/__init__.py networking_ovn/tests/unit/l3/test_l3_ovn.py networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py networking_ovn/tests/unit/ml2/__init__.py networking_ovn/tests/unit/ml2/test_mech_driver.py networking_ovn/tests/unit/ml2/test_qos_driver.py networking_ovn/tests/unit/ml2/test_trunk_driver.py networking_ovn/tests/unit/ovsdb/__init__.py networking_ovn/tests/unit/ovsdb/test_commands.py networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py networking_ovn/tests/unit/ovsdb/test_ovsdb_monitor.py networking_ovn/tests/unit/ovsdb/schemas/ovn-nb.ovsschema networking_ovn/tests/unit/ovsdb/schemas/ovn-sb.ovsschema playbooks/legacy/tempest-post-common.yml playbooks/legacy/grenade-dsvm-networking-ovn/post.yaml playbooks/legacy/grenade-dsvm-networking-ovn/run.yaml playbooks/legacy/install-dsvm-networking-ovn-kuryr/post.yaml playbooks/legacy/install-dsvm-networking-ovn-kuryr/run.yaml playbooks/legacy/networking-ovn-dsvm-functional/post.yaml playbooks/legacy/networking-ovn-dsvm-functional/run.yaml playbooks/legacy/networking-ovn-dsvm-functional-py35/post.yaml playbooks/legacy/networking-ovn-dsvm-functional-py35/run.yaml playbooks/legacy/rally-dsvm-networking-ovn/post.yaml playbooks/legacy/rally-dsvm-networking-ovn/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-multinode/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-multinode/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/run.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src/post.yaml playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release-ovsdbapp-src/run.yaml rally-jobs/README.rst rally-jobs/ovn.yaml rally-jobs/extra/README.rst rally-jobs/plugins/README.rst rally-jobs/plugins/__init__.py releasenotes/notes/.placeholder releasenotes/notes/SRIOV-port-binding-support-bug-1515005.yaml releasenotes/notes/bug-1606458-b9f809b3914bb203.yaml releasenotes/notes/distributed-fip-0f5915ef9fd00626.yaml releasenotes/notes/internal_dns_support-83737015a1019222.yaml releasenotes/notes/maintenance-thread-ee65c1ad317204c7.yaml releasenotes/notes/networking-ovn-0df373f5a7b22d19.yaml releasenotes/notes/ovn-native-nat-9bbc92f16edcf2f5.yaml releasenotes/notes/ovn_dhcpv6-729158d634aa280e.yaml releasenotes/notes/ovsdb-ssl-support-213ff378777cf946.yaml releasenotes/notes/ovsdb_connection-cef6b02c403163a3.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/unreleased.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po tools/coding-checks.sh tools/generate_config_file_samples.sh tools/ostestr_compat_shim.sh tools/tox_install.sh vagrant/README.rst vagrant/Vagrantfile vagrant/provisioning/id_rsa vagrant/provisioning/id_rsa.pub vagrant/provisioning/provider-setup.sh vagrant/provisioning/setup-base.sh vagrant/provisioning/setup-compute.sh vagrant/provisioning/setup-controller.sh vagrant/provisioning/setup-db.sh vagrant/provisioning/setup-vtep.sh vagrant/provisioning/virtualbox.conf.yml zuul.d/legacy-networking-ovn-jobs.yaml zuul.d/project.yamlnetworking-ovn-4.0.0/networking_ovn.egg-info/dependency_links.txt0000664000175100017510000000000113245511552025342 0ustar zuulzuul00000000000000 networking-ovn-4.0.0/networking_ovn.egg-info/entry_points.txt0000664000175100017510000000126613245511552024577 0ustar zuulzuul00000000000000[console_scripts] networking-ovn-metadata-agent = networking_ovn.cmd.eventlet.agents.metadata:main neutron-ovn-db-sync-util = networking_ovn.cmd.neutron_ovn_db_sync_util:main [neutron.db.alembic_migrations] networking-ovn = networking_ovn.db.migration:alembic_migrations [neutron.ml2.mechanism_drivers] ovn = networking_ovn.ml2.mech_driver:OVNMechanismDriver ovn-sync = networking_ovn.cmd.neutron_ovn_db_sync_util:OVNMechanismDriver [neutron.service_plugins] ovn-router = networking_ovn.l3.l3_ovn:OVNL3RouterPlugin [oslo.config.opts] networking_ovn = networking_ovn.common.config:list_opts networking_ovn.metadata.agent = networking_ovn.conf.agent.metadata.config:list_metadata_agent_opts networking-ovn-4.0.0/networking_ovn.egg-info/not-zip-safe0000664000175100017510000000000113245511506023521 0ustar zuulzuul00000000000000 networking-ovn-4.0.0/networking_ovn.egg-info/requires.txt0000664000175100017510000000027013245511552023673 0ustar zuulzuul00000000000000futurist>=1.2.0 netaddr>=0.7.18 neutron-lib>=1.13.0 oslo.config>=5.1.0 ovs>=2.8.0 ovsdbapp>=0.8.0 pbr!=2.1.0,>=2.0.0 pyOpenSSL>=16.2.0 tenacity>=3.2.1 Babel!=2.4.0,>=2.3.4 six>=1.10.0 networking-ovn-4.0.0/networking_ovn.egg-info/pbr.json0000664000175100017510000000005613245511552022753 0ustar zuulzuul00000000000000{"git_version": "329d6d8", "is_release": true}networking-ovn-4.0.0/networking_ovn.egg-info/top_level.txt0000664000175100017510000000001713245511552024024 0ustar zuulzuul00000000000000networking_ovn networking-ovn-4.0.0/networking_ovn.egg-info/PKG-INFO0000664000175100017510000000350213245511552022371 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: networking-ovn Version: 4.0.0 Summary: OpenStack Neutron integration with OVN Home-page: https://docs.openstack.org/networking-ovn/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: ========================================================= networking-ovn - OpenStack Neutron integration with OVN ========================================================= OVN provides virtual networking for Open vSwitch and is a component of the Open vSwitch project. This project provides integration between OpenStack Neutron and OVN. * Free software: Apache license * Source: https://git.openstack.org/cgit/openstack/networking-ovn * Bugs: https://bugs.launchpad.net/networking-ovn * Mailing list: https://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev * IRC: #openstack-neutron-ovn on Freenode. * Docs: https://docs.openstack.org/networking-ovn/latest Team and repository tags ------------------------ .. image:: https://governance.openstack.org/badges/networking-ovn.svg :target: https://governance.openstack.org/reference/tags/index.html Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 networking-ovn-4.0.0/.coveragerc0000666000175100017510000000015113245511145016647 0ustar zuulzuul00000000000000[run] branch = True source = networking_ovn omit = networking_ovn/tests/* [report] ignore_errors = True networking-ovn-4.0.0/zuul.d/0000775000175100017510000000000013245511554015754 5ustar zuulzuul00000000000000networking-ovn-4.0.0/zuul.d/legacy-networking-ovn-jobs.yaml0000666000175100017510000001133313245511145024023 0ustar zuulzuul00000000000000- job: name: legacy-networking-ovn-dsvm-base parent: legacy-dsvm-base irrelevant-files: - ^(test-|)requirements.txt$ - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ - ^setup.cfg$ - ^tools/.*$ - ^tox.ini$ - ^vagrant/.*$ required-projects: - openstack-infra/devstack-gate - openstack/neutron - openstack/networking-ovn - openstack/tempest - job: name: legacy-networking-ovn-dsvm-base-multinode parent: legacy-dsvm-base-multinode irrelevant-files: - ^(test-|)requirements.txt$ - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ - ^setup.cfg$ - ^tools/.*$ - ^tox.ini$ - ^vagrant/.*$ required-projects: - openstack-infra/devstack-gate - openstack/neutron - openstack/networking-ovn - openstack/tempest - job: name: networking-ovn-grenade-dsvm parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/grenade-dsvm-networking-ovn/run.yaml post-run: playbooks/legacy/grenade-dsvm-networking-ovn/post.yaml timeout: 9000 required-projects: - openstack-dev/grenade - openstack-infra/devstack-gate - openstack/networking-ovn - job: name: networking-ovn-install-dsvm-kuryr parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/install-dsvm-networking-ovn-kuryr/run.yaml post-run: playbooks/legacy/install-dsvm-networking-ovn-kuryr/post.yaml timeout: 7500 required-projects: - openstack-infra/devstack-gate - openstack/kuryr - openstack/networking-ovn - job: name: networking-ovn-dsvm-functional parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/networking-ovn-dsvm-functional/run.yaml post-run: playbooks/legacy/networking-ovn-dsvm-functional/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - job: name: networking-ovn-dsvm-functional-py35 parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/networking-ovn-dsvm-functional-py35/run.yaml post-run: playbooks/legacy/networking-ovn-dsvm-functional-py35/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - job: name: networking-ovn-rally-dsvm parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/rally-dsvm-networking-ovn/run.yaml post-run: playbooks/legacy/rally-dsvm-networking-ovn/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/rally - job: name: networking-ovn-tempest-dsvm-multinode parent: legacy-networking-ovn-dsvm-base-multinode run: playbooks/legacy/tempest-dsvm-networking-ovn-multinode/run.yaml post-run: playbooks/legacy/tempest-dsvm-networking-ovn-multinode/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/tempest nodeset: legacy-ubuntu-xenial-2-node - job: name: networking-ovn-tempest-dsvm-neutron-api-scenario-ovs-release parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/run.yaml post-run: playbooks/legacy/tempest-dsvm-networking-ovn-neutron-api-scenario-ovs-release/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/neutron - openstack/neutron-tempest-plugin - openstack/tempest - job: name: networking-ovn-tempest-dsvm-ovs-master parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/run.yaml post-run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/tempest - job: name: networking-ovn-tempest-dsvm-ovs-master-python3 parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/run.yaml post-run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-master-python3/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/tempest - job: name: networking-ovn-tempest-dsvm-ovs-release parent: legacy-networking-ovn-dsvm-base run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/run.yaml post-run: playbooks/legacy/tempest-dsvm-networking-ovn-ovs-release/post.yaml timeout: 7800 required-projects: - openstack-infra/devstack-gate - openstack/networking-ovn - openstack/tempest networking-ovn-4.0.0/zuul.d/project.yaml0000666000175100017510000000344313245511145020310 0ustar zuulzuul00000000000000- project: check: jobs: - networking-ovn-tempest-dsvm-ovs-master: voting: false - networking-ovn-tempest-dsvm-ovs-release - networking-ovn-rally-dsvm - networking-ovn-dsvm-functional - networking-ovn-dsvm-functional-py35: branches: ^(?!stable/newton).*$ - networking-ovn-install-dsvm-kuryr - networking-ovn-tempest-dsvm-neutron-api-scenario-ovs-release: voting: false # TripleO jobs that deploy OVN. # Note we don't use a project-template here, so it's easier # to disable voting on one specific job if things go wrong. # tripleo-ci-centos-7-scenario007-multinode-oooq will only # run on stable/pike while the -container will run in Queens # and beyond. # If you need any support to debug these jobs in case of # failures, please reach us on #tripleo IRC channel. - tripleo-ci-centos-7-scenario007-multinode-oooq - tripleo-ci-centos-7-scenario007-multinode-oooq-container gate: jobs: - networking-ovn-tempest-dsvm-ovs-release - networking-ovn-rally-dsvm - networking-ovn-dsvm-functional - networking-ovn-dsvm-functional-py35: branches: ^(?!stable/newton).*$ - networking-ovn-install-dsvm-kuryr - tripleo-ci-centos-7-scenario007-multinode-oooq - tripleo-ci-centos-7-scenario007-multinode-oooq-container experimental: jobs: - networking-ovn-tempest-dsvm-ovs-master-python3: voting: false - networking-ovn-grenade-dsvm: voting: false branches: ^(?!(driverfixes|stable/(mitaka|newton))).*$ - networking-ovn-tempest-dsvm-multinode: voting: false branches: ^(?!stable/newton).*$ networking-ovn-4.0.0/setup.cfg0000666000175100017510000000372513245511554016365 0ustar zuulzuul00000000000000[metadata] name = networking-ovn summary = OpenStack Neutron integration with OVN description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org home-page = https://docs.openstack.org/networking-ovn/latest/ 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 :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 [files] packages = networking_ovn [global] setup-hooks = pbr.hooks.setup_hook [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html [compile_catalog] directory = networking_ovn/locale domain = networking_ovn [update_catalog] domain = networking_ovn output_dir = networking_ovn/locale input_file = networking_ovn/locale/networking_ovn.pot [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext mapping_file = babel.cfg output_file = networking_ovn/locale/networking_ovn.pot [wheel] universal = 1 [entry_points] console_scripts = neutron-ovn-db-sync-util = networking_ovn.cmd.neutron_ovn_db_sync_util:main networking-ovn-metadata-agent = networking_ovn.cmd.eventlet.agents.metadata:main oslo.config.opts = networking_ovn = networking_ovn.common.config:list_opts networking_ovn.metadata.agent = networking_ovn.conf.agent.metadata.config:list_metadata_agent_opts neutron.ml2.mechanism_drivers = ovn = networking_ovn.ml2.mech_driver:OVNMechanismDriver ovn-sync = networking_ovn.cmd.neutron_ovn_db_sync_util:OVNMechanismDriver neutron.service_plugins = ovn-router = networking_ovn.l3.l3_ovn:OVNL3RouterPlugin neutron.db.alembic_migrations = networking-ovn = networking_ovn.db.migration:alembic_migrations [egg_info] tag_build = tag_date = 0 networking-ovn-4.0.0/babel.cfg0000666000175100017510000000002113245511145016250 0ustar zuulzuul00000000000000[python: **.py] networking-ovn-4.0.0/rally-jobs/0000775000175100017510000000000013245511554016611 5ustar zuulzuul00000000000000networking-ovn-4.0.0/rally-jobs/ovn.yaml0000666000175100017510000001517513245511145020306 0ustar zuulzuul00000000000000--- NeutronNetworks.create_and_list_networks: - runner: type: "constant" times: 100 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: # worst case is other 19 writers have created # resources, but quota reservation hasn't cleared # yet on any of them. This value could be 100 # without concurrency. see bug/1623390 network: 119 sla: max_avg_duration_per_atomic: neutron.list_networks: 15 # reduce as perf is fixed failure_rate: max: 0 NeutronNetworks.create_and_list_subnets: - args: subnets_per_network: 2 runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: subnet: -1 network: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_list_routers: - args: network_create_args: subnet_create_args: subnet_cidr_start: "1.1.0.0/30" subnets_per_network: 2 router_create_args: runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 router: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_list_ports: - args: network_create_args: port_create_args: ports_per_network: 50 runner: type: "constant" times: 8 concurrency: 4 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 router: -1 # ((ports per net + 1 dhcp) * times) + (concurrency-1) # see bug/1623390 for concurrency explanation port: 811 sla: max_avg_duration_per_atomic: neutron.list_ports: 15 # reduce as perf is fixed failure_rate: max: 0 NeutronNetworks.create_and_update_networks: - args: network_create_args: {} network_update_args: admin_state_up: False name: "_updated" runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_update_subnets: - args: network_create_args: {} subnet_create_args: {} subnet_cidr_start: "1.4.0.0/16" subnets_per_network: 2 subnet_update_args: enable_dhcp: True name: "_subnet_updated" runner: type: "constant" times: 100 concurrency: 20 context: users: tenants: 1 users_per_tenant: 5 quotas: neutron: network: -1 subnet: -1 port: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_update_routers: - args: network_create_args: {} subnet_create_args: {} subnet_cidr_start: "1.1.0.0/30" subnets_per_network: 2 router_create_args: {} router_update_args: admin_state_up: False name: "_router_updated" runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 router: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_update_ports: - args: network_create_args: {} port_create_args: {} ports_per_network: 5 port_update_args: admin_state_up: False device_id: "dummy_id" device_owner: "dummy_owner" name: "_port_updated" runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 port: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_delete_networks: - args: network_create_args: {} runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_delete_subnets: - args: network_create_args: {} subnet_create_args: {} subnet_cidr_start: "1.1.0.0/30" subnets_per_network: 2 runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_delete_routers: - args: network_create_args: {} subnet_create_args: {} subnet_cidr_start: "1.1.0.0/30" subnets_per_network: 2 router_create_args: {} runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 subnet: -1 router: -1 sla: failure_rate: max: 0 NeutronNetworks.create_and_delete_ports: - args: network_create_args: {} port_create_args: {} ports_per_network: 5 runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 1 users_per_tenant: 1 quotas: neutron: network: -1 port: -1 sla: failure_rate: max: 0 Quotas.neutron_update: - args: max_quota: 1024 runner: type: "constant" times: 40 concurrency: 20 context: users: tenants: 20 users_per_tenant: 1 sla: failure_rate: max: 0 networking-ovn-4.0.0/rally-jobs/plugins/0000775000175100017510000000000013245511554020272 5ustar zuulzuul00000000000000networking-ovn-4.0.0/rally-jobs/plugins/README.rst0000666000175100017510000000061213245511145021756 0ustar zuulzuul00000000000000Rally plugins ============= All ``*.py`` modules from this directory will be auto-loaded by Rally and all plugins will be discoverable. There is no need of any extra configuration and there is no difference between writing them here and in rally code base. Note that it is better to push all interesting and useful benchmarks to Rally code base, this simplifies administration for Operators. networking-ovn-4.0.0/rally-jobs/plugins/__init__.py0000666000175100017510000000000013245511145022367 0ustar zuulzuul00000000000000networking-ovn-4.0.0/rally-jobs/README.rst0000666000175100017510000000177513245511145020310 0ustar zuulzuul00000000000000Rally job related files ======================= This directory contains rally tasks and plugins that are run by OpenStack CI. Structure --------- * plugins - directory where you can add rally plugins. Almost everything in Rally is a plugin. Benchmark context, Benchmark scenario, SLA checks, Generic cleanup resources, .... * extra - all files from this directory will be copy pasted to gates, so you are able to use absolute paths in rally tasks. Files will be located in ~/.rally/extra/* * ovn.yaml is a task that is run in gates against OpenStack with Neutron service configured with OVN ML2 driver Useful links ------------ * More about Rally: https://rally.readthedocs.org/en/latest/ * Rally release notes: https://rally.readthedocs.org/en/latest/release_notes.html * How to add rally-gates: https://rally.readthedocs.org/en/latest/gates.html * About plugins: https://rally.readthedocs.org/en/latest/plugins.html * Plugin samples: https://github.com/openstack/rally/tree/master/samples/plugins networking-ovn-4.0.0/rally-jobs/extra/0000775000175100017510000000000013245511554017734 5ustar zuulzuul00000000000000networking-ovn-4.0.0/rally-jobs/extra/README.rst0000666000175100017510000000025513245511145021423 0ustar zuulzuul00000000000000Extra files =========== All files from this directory will be copy pasted to gates, so you are able to use absolute path in rally tasks. Files will be in ~/.rally/extra/* networking-ovn-4.0.0/etc/0000775000175100017510000000000013245511554015306 5ustar zuulzuul00000000000000networking-ovn-4.0.0/etc/oslo-config-generator/0000775000175100017510000000000013245511554021511 5ustar zuulzuul00000000000000networking-ovn-4.0.0/etc/oslo-config-generator/ml2_conf.ini0000666000175100017510000000021013245511145023700 0ustar zuulzuul00000000000000[DEFAULT] output_file = etc/neutron/plugins/ml2/ml2_conf.ini.sample wrap_width = 79 namespace = networking_ovn namespace = neutron.ml2 networking-ovn-4.0.0/etc/oslo-config-generator/networking_ovn_metadata_agent.ini0000666000175100017510000000022513245511145030276 0ustar zuulzuul00000000000000[DEFAULT] output_file = etc/networking_ovn_metadata_agent.ini.sample wrap_width = 79 namespace = networking_ovn.metadata.agent namespace = oslo.log networking-ovn-4.0.0/CONTRIBUTING.rst0000666000175100017510000000102713245511145017172 0ustar zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps in this page: https://docs.openstack.org/infra/manual/developers.html 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/networking-ovn networking-ovn-4.0.0/.stestr.conf0000666000175100017510000000011413245511145016776 0ustar zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./networking_ovn/tests/unit} top_dir=./ networking-ovn-4.0.0/vagrant/0000775000175100017510000000000013245511554016175 5ustar zuulzuul00000000000000networking-ovn-4.0.0/vagrant/README.rst0000666000175100017510000001162713245511164017672 0ustar zuulzuul00000000000000================================================= Automatic deployment using Vagrant and VirtualBox ================================================= The Vagrant scripts deploy OpenStack with Open Virtual Network (OVN) using four nodes to implement a minimal variant of the reference architecture: #. Database node containing the OVN northbound (NB) and southbound (SB) databases via the Open vSwitch (OVS) database and ``ovn-northd`` services. #. Controller node containing the Identity service, Image service, control plane portion of the Compute service, control plane portion of the Networking service including the ``networking-ovn`` ML2 driver, and the dashboard. In addition, the controller node is configured as an NFS server to support instance live migration between the two compute nodes. #. Two compute nodes containing the Compute hypervisor, ``ovn-controller`` service for OVN, DHCP and metadata agents for the Networking service, OVS services. In addition, the compute nodes are configured as NFS clients to support instance live migration between them. #. Optionally a node to run the HW VTEP simulator. This node is not started by default but can be started by running "vagrant up ovn-vtep" after doing a normal "vagrant up". During deployment, Vagrant creates three VirtualBox networks: #. Vagrant management network for deployment and VM access to external networks such as the Internet. Becomes the VM ``eth0`` network interface. #. OpenStack management network for the OpenStack control plane, OVN control plane, and OVN overlay networks. Becomes the VM ``eth1`` network interface. #. OVN provider network that connects OpenStack instances to external networks such as the Internet. Becomes the VM ``eth2`` network interface. Requirements ------------ The default configuration requires approximately 12 GB of RAM and supports launching approximately four OpenStack instances using the ``m1.tiny`` flavor. You can change the amount of resources for each VM in the ``virtualbox.conf.yml`` file. Deployment ---------- #. Install `VirtualBox `_ and `Vagrant `_. #. Clone the ``networking-ovn`` repository into your home directory and change to the ``vagrant`` directory:: $ git clone https://git.openstack.org/openstack/networking-ovn.git $ cd networking-ovn/vagrant #. Install plug-ins for Vagrant:: $ vagrant plugin install vagrant-cachier $ vagrant plugin install vagrant-vbguest #. If necessary, adjust any configuration in the ``virtualbox.conf.yml`` file. * If you change any IP addresses or networks, avoid conflicts with the host. * For evaluating large MTUs, adjust the ``mtu`` option. You must also change the MTU on the equivalent ``vboxnet`` interfaces on the host to the same value after Vagrant creates them. For example:: # ip link set dev vboxnet0 mtu 9000 # ip link set dev vboxnet1 mtu 9000 #. Launch the VMs and grab some coffee:: $ vagrant up #. After the process completes, you can use the ``vagrant status`` command to determine the VM status:: $ vagrant status Current machine states: ovn-db running (virtualbox) ovn-controller running (virtualbox) ovn-vtep running (virtualbox) ovn-compute1 running (virtualbox) ovn-compute2 running (virtualbox) #. You can access the VMs using the following commands:: $ vagrant ssh ovn-db $ vagrant ssh ovn-controller $ vagrant ssh ovn-vtep $ vagrant ssh ovn-compute1 $ vagrant ssh ovn-compute2 Note: If you prefer to use the VM console, the password for the ``root`` account is ``vagrant``. Since ovn-controller is set as the primary in the Vagrantfile, the command ``vagrant ssh`` (without specifying the name) will connect ssh to that virtual machine. #. Access OpenStack services via command-line tools on the ``ovn-controller`` node or via the dashboard from the host by pointing a web browser at the IP address of the ``ovn-controller`` node. Note: By default, OpenStack includes two accounts: ``admin`` and ``demo``, both using password ``password``. #. On Linux hosts, you can enable instances to access external networks such as the Internet by enabling IP forwarding and configuring SNAT from the IP address range of the provider network interface (typically vboxnet1) on the host to the external network interface on the host. For example, if the ``eth0`` network interface on the host provides external network connectivity:: # sysctl -w net.ipv4.ip_forward=1 # sysctl -p # iptables -t nat -A POSTROUTING -s 10.10.0.0/16 -o eth0 -j MASQUERADE Note: These commands do not persist after rebooting the host. #. After completing your tasks, you can destroy the VMs:: $ vagrant destroy networking-ovn-4.0.0/vagrant/provisioning/0000775000175100017510000000000013245511554020723 5ustar zuulzuul00000000000000networking-ovn-4.0.0/vagrant/provisioning/setup-compute.sh0000666000175100017510000000707113245511164024075 0ustar zuulzuul00000000000000#!/usr/bin/env bash # Script Arguments: # $1 - ovn-controller IP address # $2 - ovn-db IP address OVN_CONTROLLER_IP=$1 OVN_DB_IP=$2 cp networking-ovn/devstack/computenode-local.conf.sample devstack/local.conf sed -i -e 's//'$OVN_CONTROLLER_IP'/g' devstack/local.conf sudo umount /opt/stack/data/nova/instances # Get the IP address ipaddress=$(ip -4 addr show eth1 | grep -oP "(?<=inet ).*(?=/)") # Fixup HOST_IP with the local IP address sed -i -e 's//'$ipaddress'/g' devstack/local.conf # Adjust some things in local.conf cat << DEVSTACKEOF >> devstack/local.conf # Set this to the address of the main DevStack host running the rest of the # OpenStack services. Q_HOST=$1 HOSTNAME=$(hostname) OVN_SB_REMOTE=tcp:$OVN_DB_IP:6642 OVN_NB_REMOTE=tcp:$OVN_DB_IP:6641 # Enable logging to files. LOGFILE=/opt/stack/log/stack.sh.log # Use provider network for public. Q_USE_PROVIDERNET_FOR_PUBLIC=True OVS_PHYSICAL_BRIDGE=br-provider PHYSICAL_NETWORK=provider # Until OVN supports NAT, the private network IP address range # must not conflict with IP address ranges on the host. Change # as necessary for your environment. NETWORK_GATEWAY=172.16.1.1 FIXED_RANGE=172.16.1.0/24 DEVSTACKEOF # Add unique post-config for DevStack here using a separate 'cat' with # single quotes around EOF to prevent interpretation of variables such # as $Q_DHCP_CONF_FILE. cat << 'DEVSTACKEOF' >> devstack/local.conf # Set the availablity zone name (default is nova) for the DHCP service. [[post-config|$Q_DHCP_CONF_FILE]] [AGENT] availability_zone = nova DEVSTACKEOF devstack/stack.sh # Build the provider network in OVN. You can enable instances to access # external networks such as the Internet by using the IP address of the host # vboxnet interface for the provider network (typically vboxnet1) as the # gateway for the subnet on the neutron provider network. Also requires # enabling IP forwarding and configuring SNAT on the host. See the README for # more information. source /vagrant/provisioning/provider-setup.sh provider_setup # Add host route for the private network, at least until the native L3 agent # supports NAT. # FIXME(mkassawara): Add support for IPv6. source devstack/openrc admin admin ROUTER_GATEWAY=`neutron port-list -c fixed_ips -c device_owner | grep router_gateway | awk -F'ip_address' '{ print $2 }' | cut -f3 -d\"` sudo ip route add $FIXED_RANGE via $ROUTER_GATEWAY # NFS Setup sudo apt-get update sudo apt-get install -y nfs-common sudo mkdir -p /opt/stack/data/nova/instances sudo chmod o+x /opt/stack/data/nova/instances sudo chown vagrant:vagrant /opt/stack/data/nova/instances sudo sh -c "echo \"$OVN_CONTROLLER_IP:/opt/stack/data/nova/instances /opt/stack/data/nova/instances nfs defaults 0 0\" >> /etc/fstab" sudo mount /opt/stack/data/nova/instances sudo chown vagrant:vagrant /opt/stack/data/nova/instances sudo sh -c "echo \"listen_tls = 0\" >> /etc/libvirt/libvirtd.conf" sudo sh -c "echo \"listen_tcp = 1\" >> /etc/libvirt/libvirtd.conf" sudo sh -c "echo -n \"auth_tcp =\" >> /etc/libvirt/libvirtd.conf" sudo sh -c 'echo " \"none\"" >> /etc/libvirt/libvirtd.conf' sudo sh -c "sed -i 's/env libvirtd_opts\=\"\-d\"/env libvirtd_opts\=\"-d -l\"/g' /etc/init/libvirt-bin.conf" sudo sh -c "sed -i 's/libvirtd_opts\=\"\-d\"/libvirtd_opts\=\"\-d \-l\"/g' /etc/default/libvirt-bin" sudo /etc/init.d/libvirt-bin restart # Set the OVN_*_DB variables to enable OVN commands using a remote database. echo -e "\n# Enable OVN commands using a remote database. export OVN_NB_DB=$OVN_NB_REMOTE export OVN_SB_DB=$OVN_SB_REMOTE" >> ~/.bash_profile networking-ovn-4.0.0/vagrant/provisioning/setup-db.sh0000666000175100017510000000114613245511164023003 0ustar zuulzuul00000000000000#!/usr/bin/env bash cp networking-ovn/devstack/db-local.conf.sample devstack/local.conf if [ "$1" != "" ]; then sed -i -e 's//'$1'/g' devstack/local.conf fi # Get the IP address ipaddress=$(ip -4 addr show eth1 | grep -oP "(?<=inet ).*(?=/)") # Adjust some things in local.conf cat << DEVSTACKEOF >> devstack/local.conf # Set this to the address of the main DevStack host running the rest of the # OpenStack services. Q_HOST=$1 HOST_IP=$ipaddress HOSTNAME=$(hostname) # Enable logging to files. LOGFILE=/opt/stack/log/stack.sh.log DEVSTACKEOF devstack/stack.sh networking-ovn-4.0.0/vagrant/provisioning/id_rsa0000666000175100017510000000321213245511145022103 0ustar zuulzuul00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAv2EyHk382N9LGMPAGbAG9rea6qcO+I+qj7OscU1k8GxnYO0B hHYPMzfT1RgeBDelNyM22SNiySr4iTQDBQxgunrUCdTaNu5dmzYT68gieqnH+CRR jpLxXecH2hcvKyFx5qmhMt4zE3QWCDv2JJiB5CGoV7sGCy1aTYbFJBeKwMUMpBwP 67rpBdVcpNjeSkw8FKDvPVx1p1O0YgeI9JoIL0qka6FFgiii/wf5jgr0w/JW15VI 2pYwpHhdnBt3M3BV2HK5cA6nwFhUfAG0HLP5lUGW9/Hk5ng/Wl7cz8nLAdgXf4Uc IUffO5SH+9/H5VhTMDpaRPgxWuOw1/UGLgf57wIDAQABAoIBAD/+5X6Cv6lZycfn NWahmUKJFRGgeX4etH9HKsPciINpDIy51EcSH3UWFwzr+qWYYfP1H5MupQr2BpQC w3u9rt7M0fjTp4C05rJPPAwdKYJxIcBVjLwrYPDwn4yLMievEGJ8mL3k1ZmMuQ1Z 165XHSBHLP7hOF0mdkr0ZRnzkV9yMPjZAI6xnkt/q6EvO34wSZu3/qsmptipHgqB QQAjPIvJwr7DMoLVpBjLlfGihUB5NAVC0RU+7SIiTAUg0atUzucp+sQMnWlWKVvM 3+nHGC8gR4fUy30LDgxd4eqFyG8EYpTzpN/0bgM3kdwiQTkR/lGvhwmok/o6Nz0n 67ve12ECgYEA41lug+TitPrq9VaLacTBpDOafsmIY30sylBJClcbkQ94NEQyNASg TsXxRtvYvKuHy0i2xZwagqEyReaTfsScmyFOk/SRFqjgmb3eWYtgF04MtAAmLy9G 5UmPLEm6lLuQGCI7CqLAv3PFCR7W7dX5VYwkteDejZ0NlLeNqKJjIvECgYEA139W ocUBbWu4ea68JB/qOGrxCMQKn6K3l9kA7tuu+3a0le0G7LF6dr+X1wvboCF/w8CZ ZqKm35yZoAzyFmfn8oGtJdgbz4Sl3/vZReg86Ca//m4LMe1FkkimT3UW+BKprtEJ 5GiKKWYElknMthbDTpL5EouciPhG0bYKuIMBKt8CgYAL+LqcEWJqu0fCEYOX1zeH KPx6rqwS6RWBtcaS19FoyxK+VdT67j9uxneVDqCUFsg4ySRutXCj7k8SZTjhFQNW G+PiYJ9/PPdOwTPDLVarA34hwFxCYc/u5Pe4Ek3T5SiKTMslHTrfGf6HI2uX7IuL mKyaMzQk6t87NIsuFRb5UQKBgQCzciEEslUe9b127k9S0ZSriDnQb9bc2ZWCB7zk KeELGu0Dj43dmWh968sX0pL/RAXtTrsuoTDOMcwnX8BTchDOerdhNRTrd+zcmA50 TRAyzNnBl4cQ+yCc0IxUzA7lYj0UCpPvNDIgiQg20Zt64XefPXnUvJcL45qtVKaW wNg/BwKBgFyhjxftMwAJJF2Hcq5s8QvNhznBgLtne7jnQkHU4qcJx6tcR1hy0Jqe 8/zkr5+41EaFU2jjGn8cnUrlS/Vc/HZg3rmHYycX5wg9hrg1j4hokSHjsGL6Y7yn 8oXIWJSqpxuMjfRh1Tb81Fg05emrMjTy6aLuGS0siUlTPzflD0RI -----END RSA PRIVATE KEY-----networking-ovn-4.0.0/vagrant/provisioning/setup-base.sh0000666000175100017510000000670113245511164023332 0ustar zuulzuul00000000000000#!/bin/sh # Script Arguments: # $1 - MTU # $2 - ovn-db IP address # $3 - ovn-db short name # $4 - ovn-controller IP address # $5 - ovn-controller short name # $6 - ovn-compute1 IP address # $7 - ovn-compute1 short name # $8 - ovn-compute2 IP address # $9 - ovn-compute2 short name # $10 - ovn-vtep IP address # $11 - ovn-vtep short name MTU=$1 OVN_DB_IP=$2 OVN_DB_NAME=$3 OVN_CONTROLLER_IP=$4 OVN_CONTROLLER_NAME=$5 OVN_COMPUTE1_IP=$6 OVN_COMPUTE1_NAME=$7 OVN_COMPUTE2_IP=$8 OVN_COMPUTE2_NAME=$9 OVN_VTEP_IP=$10 OVN_VTEP_NAME=$11 BASE_PACKAGES="git bridge-utils ebtables python-pip python-dev build-essential ntp" DEBIAN_FRONTEND=noninteractive sudo apt-get -qqy update DEBIAN_FRONTEND=noninteractive sudo apt-get install -qqy $BASE_PACKAGES echo export LC_ALL=en_US.UTF-8 >> ~/.bash_profile echo export LANG=en_US.UTF-8 >> ~/.bash_profile # FIXME(mestery): Remove once Vagrant boxes allow apt-get to work again sudo rm -rf /var/lib/apt/lists/* sudo apt-get install -y git # FIXME(mestery): By default, Ubuntu ships with /bin/sh pointing to # the dash shell. # .. # .. # The dots above represent a pause as you pick yourself up off the # floor. This means the latest version of "install_docker.sh" to load # docker fails because dash can't interpret some of it's bash-specific # things. It's a bug in install_docker.sh that it relies on those and # uses a shebang of /bin/sh, but that doesn't help us if we want to run # docker and specifically Kuryr. So, this works around that. sudo update-alternatives --install /bin/sh sh /bin/bash 100 if [ ! -d "devstack" ]; then git clone https://git.openstack.org/openstack-dev/devstack.git fi # If available, use repositories on host to facilitate testing local changes. # Vagrant requires that shared folders exist on the host, so additionally # check for the ".git" directory in case the parent exists but lacks # repository contents. if [ ! -d "networking-ovn/.git" ]; then git clone https://git.openstack.org/openstack/networking-ovn.git fi # Use networking-ovn in vagrant home directory when stacking. sudo mkdir /opt/stack sudo chown vagrant:vagrant /opt/stack ln -s ~/networking-ovn /opt/stack/networking-ovn # We need swap space to do any sort of scale testing with the Vagrant config. # Without this, we quickly run out of RAM and the kernel starts whacking things. sudo rm -f /swapfile1 sudo dd if=/dev/zero of=/swapfile1 bs=1024 count=8388608 sudo chown root:root /swapfile1 sudo chmod 0600 /swapfile1 sudo mkswap /swapfile1 sudo swapon /swapfile1 # Configure MTU on VM interfaces. Also requires manually configuring the same MTU on # the equivalent 'vboxnet' interfaces on the host. sudo ip link set dev eth1 mtu $MTU sudo ip link set dev eth2 mtu $MTU # Migration setup sudo sh -c "echo \"$OVN_DB_IP $OVN_DB_NAME\" >> /etc/hosts" sudo sh -c "echo \"$OVN_CONTROLLER_IP $OVN_CONTROLLER_NAME\" >> /etc/hosts" sudo sh -c "echo \"$OVN_COMPUTE1_IP $OVN_COMPUTE1_NAME\" >> /etc/hosts" sudo sh -c "echo \"$OVN_COMPUTE2_IP $OVN_COMPUTE2_NAME\" >> /etc/hosts" sudo sh -c "echo \"$OVN_VTEP_IP $OVN_VTEP_NAME\" >> /etc/hosts" # Non-interactive SSH setup cp networking-ovn/vagrant/provisioning/id_rsa ~/.ssh/id_rsa cat networking-ovn/vagrant/provisioning/id_rsa.pub >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/id_rsa echo "Host *" >> ~/.ssh/config echo " StrictHostKeyChecking no" >> ~/.ssh/config chmod 600 ~/.ssh/config sudo cp ~vagrant/.ssh/id_rsa /root/.ssh sudo cp ~vagrant/.ssh/authorized_keys /root/.ssh sudo cp ~vagrant/.ssh/config /root/.ssh/config networking-ovn-4.0.0/vagrant/provisioning/setup-controller.sh0000666000175100017510000000664413245511164024611 0ustar zuulzuul00000000000000#!/usr/bin/env bash # Script Arguments: # $1 - ovn-db IP address # $2 - provider network starting IP address # $3 - provider network ending IP address # $4 - provider network gateway # $5 - provider network network # $6 - ovn vm subnet ovnip=$1 start_ip=$2 end_ip=$3 gateway=$4 network=$5 ovn_vm_subnet=$6 cp networking-ovn/devstack/local.conf.sample devstack/local.conf # Get the IP address ipaddress=$(ip -4 addr show eth1 | grep -oP "(?<=inet ).*(?=/)") # Adjust some things in local.conf cat << DEVSTACKEOF >> devstack/local.conf # Good to set these HOST_IP=$ipaddress HOSTNAME=$(hostname) SERVICE_HOST_NAME=${HOST_NAME} SERVICE_HOST=$ipaddress OVN_SB_REMOTE=tcp:$ovnip:6642 OVN_NB_REMOTE=tcp:$ovnip:6641 # Enable logging to files. LOGFILE=/opt/stack/log/stack.sh.log # Disable the ovn-northd service on the controller node because the # architecture includes a separate OVN database server. disable_service ovn-northd # Disable the ovn-controller service because the architecture lacks services # on the controller node that depend on it. disable_service ovn-controller # Disable the nova compute service on the controller node because the # architecture only deploys it on separate compute nodes. disable_service n-cpu # Disable cinder services and tempest to reduce deployment time. disable_service c-api c-sch c-vol tempest # Until OVN supports NAT, the private network IP address range # must not conflict with IP address ranges on the host. Change # as necessary for your environment. NETWORK_GATEWAY=172.16.1.1 FIXED_RANGE=172.16.1.0/24 # Use provider network for public. Q_USE_PROVIDERNET_FOR_PUBLIC=True OVS_PHYSICAL_BRIDGE=br-provider PHYSICAL_NETWORK=provider PUBLIC_NETWORK_NAME=provider PUBLIC_NETWORK_GATEWAY="$gateway" PUBLIC_PHYSICAL_NETWORK=provider PUBLIC_SUBNET_NAME=provider-v4 IPV6_PUBLIC_SUBNET_NAME=provider-v6 Q_FLOATING_ALLOCATION_POOL="start=$start_ip,end=$end_ip" FLOATING_RANGE="$network" DEVSTACKEOF # Add unique post-config for DevStack here using a separate 'cat' with # single quotes around EOF to prevent interpretation of variables such # as $NEUTRON_CONF. cat << 'DEVSTACKEOF' >> devstack/local.conf # Enable two DHCP agents per neutron subnet with support for availability # zones. Requires two or more compute nodes. [[post-config|/$NEUTRON_CONF]] [DEFAULT] network_scheduler_driver = neutron.scheduler.dhcp_agent_scheduler.AZAwareWeightScheduler dhcp_load_type = networks dhcp_agents_per_network = 2 # Configure the Compute service (nova) metadata API to use the X-Forwarded-For # header sent by the Networking service metadata proxies on the compute nodes. [[post-config|$NOVA_CONF]] [DEFAULT] use_forwarded_for = True DEVSTACKEOF devstack/stack.sh # Make the provider network shared and enable DHCP for its v4 subnet. source devstack/openrc admin admin neutron net-update --shared $PUBLIC_NETWORK_NAME neutron subnet-update --enable_dhcp=True $PUBLIC_SUBNET_NAME # NFS server setup sudo apt-get update sudo apt-get install -y nfs-kernel-server nfs-common sudo mkdir -p /opt/stack/data/nova/instances sudo touch /etc/exports sudo sh -c "echo \"/opt/stack/data/nova/instances $ovn_vm_subnet(rw,sync,fsid=0,no_root_squash)\" >> /etc/exports" sudo service nfs-kernel-server restart sudo service idmapd restart # Set the OVN_*_DB variables to enable OVN commands using a remote database. echo -e "\n# Enable OVN commands using a remote database. export OVN_NB_DB=$OVN_NB_REMOTE export OVN_SB_DB=$OVN_SB_REMOTE" >> ~/.bash_profile networking-ovn-4.0.0/vagrant/provisioning/setup-vtep.sh0000666000175100017510000000127313245511164023375 0ustar zuulzuul00000000000000#!/usr/bin/env bash OVN_DB_IP=$2 cp networking-ovn/devstack/vtep-local.conf.sample devstack/local.conf if [ "$1" != "" ]; then sed -i -e 's//'$1'/g' devstack/local.conf fi # Get the IP address ipaddress=$(ip -4 addr show eth1 | grep -oP "(?<=inet ).*(?=/)") # Adjust some things in local.conf cat << DEVSTACKEOF >> devstack/local.conf # Set this to the address of the main DevStack host running the rest of the # OpenStack services. Q_HOST=$1 HOST_IP=$ipaddress HOSTNAME=$(hostname) OVN_SB_REMOTE=tcp:$OVN_DB_IP:6642 OVN_NB_REMOTE=tcp:$OVN_DB_IP:6641 # Enable logging to files. LOGFILE=/opt/stack/log/stack.sh.log DEVSTACKEOF devstack/stack.sh networking-ovn-4.0.0/vagrant/provisioning/id_rsa.pub0000666000175100017510000000061013245511145022667 0ustar zuulzuul00000000000000ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC/YTIeTfzY30sYw8AZsAb2t5rqpw74j6qPs6xxTWTwbGdg7QGEdg8zN9PVGB4EN6U3IzbZI2LJKviJNAMFDGC6etQJ1No27l2bNhPryCJ6qcf4JFGOkvFd5wfaFy8rIXHmqaEy3jMTdBYIO/YkmIHkIahXuwYLLVpNhsUkF4rAxQykHA/ruukF1Vyk2N5KTDwUoO89XHWnU7RiB4j0mggvSqRroUWCKKL/B/mOCvTD8lbXlUjaljCkeF2cG3czcFXYcrlwDqfAWFR8AbQcs/mVQZb38eTmeD9aXtzPycsB2Bd/hRwhR987lIf738flWFMwOlpE+DFa47DX9QYuB/nv vagrant@ovnnetworking-ovn-4.0.0/vagrant/provisioning/virtualbox.conf.yml0000666000175100017510000000174213245511164024574 0ustar zuulzuul00000000000000--- box: "ubuntu/trusty64" provider_network: "10.10.0.0/16" provider_gateway: "10.10.0.1" provider_start_ip: "10.10.0.101" provider_end_ip: "10.10.255.250" ovn_vm_subnet: "192.168.33.0/24" ovndb: short_name: "ovn-db" host_name: "ovn-db.devstack.dev" ip: "192.168.33.11" prov-ip: "10.10.0.11" memory: 2048 cpus: 2 mtu: 1500 ovncontroller: short_name: "ovn-controller" host_name: "ovn-controller.devstack.dev" ip: "192.168.33.12" prov-ip: "10.10.0.12" memory: 6144 cpus: 2 mtu: 1500 ovnvtep: short_name: "ovn-vtep" host_name: "ovn-vtep.devstack.dev" ip: "192.168.33.13" prov-ip: "10.10.0.13" memory: 512 cpus: 1 mtu: 1500 ovncompute1: short_name: "ovn-compute1" host_name: "ovn-compute1.devstack.dev" ip: "192.168.33.31" prov-ip: "10.10.0.31" memory: 1536 cpus: 1 mtu: 1500 ovncompute2: short_name: "ovn-compute2" host_name: "ovn-compute2.devstack.dev" ip: "192.168.33.32" prov-ip: "10.10.0.32" memory: 1536 cpus: 1 mtu: 1500 networking-ovn-4.0.0/vagrant/provisioning/provider-setup.sh0000666000175100017510000000062713245511164024253 0ustar zuulzuul00000000000000#!/bin/bash function provider_setup { # Save the existing address from eth2 and add it to br-provider PROVADDR=$(ip -4 addr show eth2 | grep -oP "(?<=inet ).*(?= brd)") if [ -n "$PROVADDR" ]; then sudo ip addr flush dev eth2 sudo ip addr add $PROVADDR dev br-provider sudo ip link set br-provider up sudo ovs-vsctl --may-exist add-port br-provider eth2 fi } networking-ovn-4.0.0/vagrant/Vagrantfile0000666000175100017510000003153613245511164020371 0ustar zuulzuul00000000000000# -*- mode: ruby -*- # vi: set ft=ruby : require 'yaml' require 'ipaddr' vagrant_config = YAML.load_file("provisioning/virtualbox.conf.yml") Vagrant.configure(2) do |config| config.vm.box = vagrant_config['box'] if Vagrant.has_plugin?("vagrant-cachier") # Configure cached packages to be shared between instances of the same base box. # More info on http://fgrehm.viewdocs.io/vagrant-cachier/usage config.cache.scope = :box end #config.vm.synced_folder File.expand_path("..") + "/devstack", "/home/vagrant/devstack" config.vm.synced_folder File.expand_path(".."), "/home/vagrant/networking-ovn" # Use the ipaddr library to calculate the netmask of a given network net = IPAddr.new vagrant_config['provider_network'] netmask = net.inspect().split("/")[1].split(">")[0] # Build the common args for the setup-base.sh scripts. setup_base_common_args = "#{vagrant_config['ovndb']['ip']} #{vagrant_config['ovndb']['short_name']} " + "#{vagrant_config['ovncontroller']['ip']} #{vagrant_config['ovncontroller']['short_name']} " + "#{vagrant_config['ovncompute1']['ip']} #{vagrant_config['ovncompute1']['short_name']} " + "#{vagrant_config['ovncompute2']['ip']} #{vagrant_config['ovncompute2']['short_name']} " + "#{vagrant_config['ovnvtep']['ip']} #{vagrant_config['ovnvtep']['short_name']} " # Bring up the Devstack ovsdb/ovn-northd node on Virtualbox config.vm.define "ovn-db" do |ovndb| ovndb.vm.host_name = vagrant_config['ovndb']['host_name'] ovndb.vm.network "private_network", ip: vagrant_config['ovndb']['ip'] ovndb.vm.network "private_network", ip: vagrant_config['ovndb']['prov-ip'], netmask: netmask ovndb.vm.provision "shell", path: "provisioning/setup-base.sh", privileged: false, :args => "#{vagrant_config['ovndb']['mtu']} #{setup_base_common_args}" ovndb.vm.provision "shell", path: "provisioning/setup-db.sh", privileged: false, :args => "#{vagrant_config['ovncontroller']['ip']}" ovndb.vm.provider "virtualbox" do |vb| vb.memory = vagrant_config['ovndb']['memory'] vb.cpus = vagrant_config['ovndb']['cpus'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] end ovndb.vm.provider 'parallels' do |vb, override| vb.memory = vagrant_config['ovndb']['memory'] vb.cpus = vagrant_config['ovndb']['cpus'] vb.customize ['set', :id, '--nested-virt', 'on'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end ovndb.vm.provider 'libvirt' do |vb, override| vb.memory = vagrant_config['ovndb']['memory'] vb.cpus = vagrant_config['ovndb']['cpus'] vb.nested = true vb.graphics_type = 'spice' vb.video_type = 'qxl' vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end end # Bring up the Devstack controller node on Virtualbox config.vm.define "ovn-controller", primary: true do |ovncontroller| ovncontroller.vm.host_name = vagrant_config['ovncontroller']['host_name'] ovncontroller.vm.network "private_network", ip: vagrant_config['ovncontroller']['ip'] ovncontroller.vm.network "private_network", ip: vagrant_config['ovncontroller']['prov-ip'], netmask: netmask ovncontroller.vm.provision "shell", path: "provisioning/setup-base.sh", privileged: false, :args => "#{vagrant_config['ovncontroller']['mtu']} #{setup_base_common_args}" ovncontroller.vm.provision "shell", path: "provisioning/setup-controller.sh", privileged: false, :args => "#{vagrant_config['ovndb']['ip']} #{vagrant_config['provider_start_ip']} #{vagrant_config['provider_end_ip']} " + "#{vagrant_config['provider_gateway']} #{vagrant_config['provider_network']} #{vagrant_config['ovn_vm_subnet']}" ovncontroller.vm.provider "virtualbox" do |vb| vb.memory = vagrant_config['ovncontroller']['memory'] vb.cpus = vagrant_config['ovncontroller']['cpus'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] end ovncontroller.vm.provider 'parallels' do |vb, override| vb.memory = vagrant_config['ovncontroller']['memory'] vb.cpus = vagrant_config['ovncontroller']['cpus'] vb.customize ['set', :id, '--nested-virt', 'on'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end ovncontroller.vm.provider 'libvirt' do |vb, override| vb.memory = vagrant_config['ovncontroller']['memory'] vb.cpus = vagrant_config['ovncontroller']['cpus'] vb.nested = true vb.graphics_type = 'spice' vb.video_type = 'qxl' vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end end config.vm.define "ovn-vtep", autostart: false do |ovnvtep| ovnvtep.vm.host_name = vagrant_config['ovnvtep']['host_name'] ovnvtep.vm.network "private_network", ip: vagrant_config['ovnvtep']['ip'] ovnvtep.vm.network "private_network", ip: vagrant_config['ovnvtep']['prov-ip'], netmask: netmask ovnvtep.vm.provision "shell", path: "provisioning/setup-base.sh", privileged: false, :args => "#{vagrant_config['ovnvtep']['mtu']} #{setup_base_common_args}" ovnvtep.vm.provision "shell", path: "provisioning/setup-vtep.sh", privileged: false, :args => "#{vagrant_config['ovncontroller']['ip']} #{vagrant_config['ovndb']['ip']}" ovnvtep.vm.provider "virtualbox" do |vb| vb.memory = vagrant_config['ovnvtep']['memory'] vb.cpus = vagrant_config['ovnvtep']['cpus'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] end ovnvtep.vm.provider 'parallels' do |vb, override| vb.memory = vagrant_config['ovnvtep']['memory'] vb.cpus = vagrant_config['ovnvtep']['cpus'] vb.customize ['set', :id, '--nested-virt', 'on'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end ovnvtep.vm.provider 'libvirt' do |vb, override| vb.memory = vagrant_config['ovnvtep']['memory'] vb.cpus = vagrant_config['ovnvtep']['cpus'] vb.nested = true vb.graphics_type = 'spice' vb.video_type = 'qxl' vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end end # Bring up the first Devstack compute node on Virtualbox config.vm.define "ovn-compute1" do |ovncompute1| ovncompute1.vm.host_name = vagrant_config['ovncompute1']['host_name'] ovncompute1.vm.network "private_network", ip: vagrant_config['ovncompute1']['ip'] ovncompute1.vm.network "private_network", ip: vagrant_config['ovncompute1']['prov-ip'], netmask: netmask ovncompute1.vm.provision "shell", path: "provisioning/setup-base.sh", privileged: false, :args => "#{vagrant_config['ovncompute1']['mtu']} #{setup_base_common_args}" ovncompute1.vm.provision "shell", path: "provisioning/setup-compute.sh", privileged: false, :args => "#{vagrant_config['ovncontroller']['ip']} #{vagrant_config['ovndb']['ip']}" ovncompute1.vm.provider "virtualbox" do |vb| vb.memory = vagrant_config['ovncompute1']['memory'] vb.cpus = vagrant_config['ovncompute1']['cpus'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] end ovncompute1.vm.provider 'parallels' do |vb, override| vb.memory = vagrant_config['ovncompute1']['memory'] vb.cpus = vagrant_config['ovncompute1']['cpus'] vb.customize ['set', :id, '--nested-virt', 'on'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end ovncompute1.vm.provider 'libvirt' do |vb, override| vb.memory = vagrant_config['ovncompute1']['memory'] vb.cpus = vagrant_config['ovncompute1']['cpus'] vb.nested = true vb.graphics_type = 'spice' vb.video_type = 'qxl' vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end end # Bring up the second Devstack compute node on Virtualbox config.vm.define "ovn-compute2" do |ovncompute2| ovncompute2.vm.host_name = vagrant_config['ovncompute2']['host_name'] ovncompute2.vm.network "private_network", ip: vagrant_config['ovncompute2']['ip'] ovncompute2.vm.network "private_network", ip: vagrant_config['ovncompute2']['prov-ip'], netmask: netmask ovncompute2.vm.provision "shell", path: "provisioning/setup-base.sh", privileged: false, :args => "#{vagrant_config['ovncompute2']['mtu']} #{setup_base_common_args}" ovncompute2.vm.provision "shell", path: "provisioning/setup-compute.sh", privileged: false, :args => "#{vagrant_config['ovncontroller']['ip']} #{vagrant_config['ovndb']['ip']}" ovncompute2.vm.provider "virtualbox" do |vb| vb.memory = vagrant_config['ovncompute2']['memory'] vb.cpus = vagrant_config['ovncompute2']['cpus'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] end ovncompute2.vm.provider 'parallels' do |vb, override| vb.memory = vagrant_config['ovncompute2']['memory'] vb.cpus = vagrant_config['ovncompute2']['cpus'] vb.customize ['set', :id, '--nested-virt', 'on'] vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end ovncompute2.vm.provider 'libvirt' do |vb, override| vb.memory = vagrant_config['ovncompute2']['memory'] vb.cpus = vagrant_config['ovncompute2']['cpus'] vb.nested = true vb.graphics_type = 'spice' vb.video_type = 'qxl' vb.customize [ 'modifyvm', :id, '--nicpromisc3', "allow-all" ] vb.customize [ "guestproperty", "set", :id, "/VirtualBox/GuestAdd/VBoxService/--timesync-set-threshold", 10000 ] override.vm.box = ENV.fetch('VAGRANT_OVN_VM_BOX', 'boxcutter/ubuntu1404') end end end networking-ovn-4.0.0/PKG-INFO0000664000175100017510000000350213245511554015630 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: networking-ovn Version: 4.0.0 Summary: OpenStack Neutron integration with OVN Home-page: https://docs.openstack.org/networking-ovn/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description-Content-Type: UNKNOWN Description: ========================================================= networking-ovn - OpenStack Neutron integration with OVN ========================================================= OVN provides virtual networking for Open vSwitch and is a component of the Open vSwitch project. This project provides integration between OpenStack Neutron and OVN. * Free software: Apache license * Source: https://git.openstack.org/cgit/openstack/networking-ovn * Bugs: https://bugs.launchpad.net/networking-ovn * Mailing list: https://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev * IRC: #openstack-neutron-ovn on Freenode. * Docs: https://docs.openstack.org/networking-ovn/latest Team and repository tags ------------------------ .. image:: https://governance.openstack.org/badges/networking-ovn.svg :target: https://governance.openstack.org/reference/tags/index.html Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 networking-ovn-4.0.0/ChangeLog0000664000175100017510000013107213245511551016306 0ustar zuulzuul00000000000000CHANGES ======= 4.0.0 ----- * Tempest: Enable pagination, sorting and project-id API extensions * Enable trunk tempest tests * Use neutron-tempest-plugin for API and scenario tests * Zuul: Remove project name * Updated from global requirements * Check for router port correctness * Use metadata IP as DHCP service IP for subnet without gateway IP * Fix RDO OVN scenario job by checking DB schema (iii) * Fix router port creation * Maintenance: Avoid code duplication * Maintenance task: Ordering resources by type * Name is error when finding revision\_number in CheckRevisionNumber 4.0.0.0b3 --------- * Updated from global requirements * Functional tests: Leave fixtures to remove the dbs * Enable tempest IPv6 scenario tests now that we have RA support in OVN master * Fix trunk with subport functional tests * functional tests: Register SQLAlchemy models * Check for subnets correctness * Check for floating ips correctness * Update LRP when a router port is updated * Use placement-client rather than placement-api * Updated from global requirements * Neutron-server start error when not use ovn l3 * Check for security group correctness * Check for routers correctness * enable ovn metadata in computenode conf sample * drop is\_ovn\_service\_enabled in devstack * Updated from global requirements * Check for sg\_rules correctness * Refactor Routers * Cleanup and add general ovsdb capturing yaml for rally * Check for ports correctness * Make ovn\_client create\_network idempotent * Remove duplicate code * Switch to get\_reader\_session * Fix RDO OVN scenario job by checking DB schema (ii) * Optimize inconsistency detection (Part 2) * Optimize inconsistency detection (Part 1) * Check for correctness when updating networks * Fix RDO OVN scenario job by checking DB schema * Bug in updating duplicate rules in security groups * Make use of native OVN IPv6 Router Advertisement support * Add security group tests to test\_mech\_driver.py * Updated from global requirements * Add @abstractmethod to get\_floatingip\_by\_ips * Refactor Floating IPs related methods * Refactor security groups * Add native DNS support * Retry connection to OVSDB from metadata agent * Add a comment on local.conf.sample on how to enable Rally * Use systemd service for all ovs/ovn process * Exclude some unrelated test cases in tempest gate * Imported Translations from Zanata * Fix subnet enabled DHCP failed when subnet has port * Add DHCP functional test case to cover an untested branch * Fix functional tests * Initialize privsep in networking-ovn-metadata-agent * Fix minor typos in the Neutron/OVN database consistency spec * Refactor subnet dhcp options methods * Fix unittests * Simplify create\_network of OVNClient * get rid of update\_port in OVNMechanismDriver * Optimize updating routes in \_subnet\_update * Remove redundant SetLSwitchPortCommand in trunk driver * Update links in CONTRIBUTING and README * networking-ovn hides some other xtrace logs improperly * \_enable\_subnet\_dhcp\_options(): Consolidate all commands in one transaction 4.0.0.0b2 --------- * Cleanup \_add\_router\_ext\_gw() * Ignore floating IP port for (create, update)\_port * Imported Translations from Zanata * Insert a new virtual service to log br-int flows * SR-IOV support for a networking-ovn deployment * Updated from global requirements * Correct order of args passed to del\_port in medata teardown\_datapath * Format logging for metadata agent * Tempest test\_port\_security\_macspoofing\_port was skipped for wrong reason * Remove parameter metadata\_port\_ip * Remove vport\_vxlan kernel module when excuting unstack.sh * Replace br-vtep with br-v for ovs-vtep process in devstack * Idea proposal: Neutron/OVN database consistency problem * Fix functional and rally tests * Collect functional test logs and add logstash index * Add a background tcpdump on br-ex for ARP and ICMPv4/v6 * Collect OVS databases in tempest logs * Drop journaling related code * zuul: run TripleO jobs with new zuulv3 layout * Updated from global requirements * Remove setting of version/release from releasenotes * Fix tox debug target * Add release note for distributed fip * Fix race condition on ovn\_client on startup * update\_router(): Consolidate all commands in one transaction * Set dhcp\_agent\_notification to False for devstack * update\_network should consider qos if or not name of network changes * support distributed floating ip * use qos api def from neutron-lib * use l3 api def from neutron-lib * Updated from global requirements * Log a warning when trying to wait for metadata on a non existent port * Replace add/del lswitch cmds w/ ovsdbapp equivs * Fix may\_exists/may\_exist inconsistency * Remove triplicated update\_router\_routes() method * Zuul: add file extension to playbook path * Create metadata port when it is found in neutron but not in OVN * Fix a redundant check about ovsdb lock in OvnWorker * Ignore dhcp opt sync for network device port * use external net api def from lib 4.0.0.0b1 --------- * Add .stestr to .gitignore * Convert SB API to use ovsdbapp * Simplify the L3 {create, update}\_router() methods * Add neutron scenario tests * Create Metadata port in OVN when found missing in Neutron * Modification of devstack broke OVN metadata agent * Move networking-ovn legacy jobs to our tree * Remove "fake\_api" from functional tests * Pass arg name is\_gw\_port to calling of set\_lrouter\_port\_in\_lswitch\_port * Do not ignore the QOS options on port update * Test with OVS branch-2.8 for latest-release * Small refactor of using DEVICE\_OWNER\_PREFIXES * Eliminate ovsdb error in dsvm functional test * Use shim tool for dsvm functional test * Correct an issue of dsvm dscpv6 test case itself * Fix dead links in the documentation * neutron-ovn-db-sync-util: sync metadata ports * Updated from global requirements * Fix OVSDB test connection failures * Deprecate containers.rst document * Don't create metadata port if it already exists * Use constants for device owner * Create the metadata port with the same project\_id as its network * Consider router ports of type 'HA\_REPLICATED\_INT' during sync * Remove SCREEN\_LOGDIR from devstack * Neutron API tests configuration * consume common constants from lib * Allow tempest to skip agent tests * Add "nat-addresses" option for support of garp feature * Updated from global requirements * Fix security group rule tcp/udp port range failed * use new payload objects for \*\_INIT callbacks * Updated from global requirements * Use the OVN \_i18n module * remove neutron trunk object import * Use shim tool for ostestr * Update import for ml2 config * Only monitor the necessary SB tables for changes * Use ovsdbapp RowEvent * Fix unit and functional tests * DHCP options for subnet synchronize each time * Set requested-chassis with binding host\_id * Delete dummy files * Python3.5 RuntimeError: dictionary changed size during iteration * Log error for missing metadata port only if metadata is enabled * Add DNS db mixin in l3 plugin * Track router and floatingip quota usage using TrackedResource * Enhance devstackgaterc to support neutron-api tests * Qos testing failed due to other non-QoS keys in options * Handle the admin\_state\_up flag in router update correctly * Remove agent/dhcp\_agent\_scheduler extensions * Add mac from allowed\_address\_pairs to ovn lport addresses * schedule gateway on chassis with external connectivity * Updated from global requirements * Small refactor of metadata bits * Support ACL name and severity columns * Sync neutron db with OVN southbound db * Support for L3 gateway HA * subports: add binding support to them * Imported Translations from Zanata * Add allowed\_address\_pairs in address\_set * Fix gate mtu tests * Rename OVN metadata agent configuration file * Update reno for stable/pike * refarch: Update documentation and diagrams 3.0.0.0rc1 ---------- * Tox docs: Force the use of python2.7 * Prepare for using ovsdbapp > 0.4.0 * Make the metadata support work on multinode * Make Metadata agent independent from other config files * call provisioning\_complete conditionally * Add auto-generated config reference * Improve oslo-config-generator setting * Updated from global requirements * Fix gate issues * Rename metadata proxy config dir * use qos constants from neutron-lib * doc: Fix list and link formatting * doc: Add link to tutorial * use qos DriverBase from neutron-lib * Replace br-int with ovs\_integration\_bridge of OVN metadata conf * Rename 'ns-metadata-proxy' config dir to 'ovn-metadata-proxy' * Updated from global requirements * Update URLs in documents according to document migration * add functional test for QoS * Disable ovn\_metadata by default * Configure ovn\_metadata\_enabled option in devstack * Metadata agent support in networking-ovn * doc: fix indent level * rearrange existing documentation to fit the new standard layout * DevStack: Support to install ovsdbapp from git master * Switch from oslosphinx to openstackdocstheme * Add if\_exist parameter to SetLRouterPortInLSwitchPortCommand * Add security groups and security group rules to OVNClient * Add database migration tests * Fix a few typos * Remove unused parameter * Use flake8-import-order plugin * Tox/Pylint: Enforce the use of python2 * Enable pylint 1.4.5 * use service type constants from neutron\_lib plugins * Updated from global requirements * Updated from global requirements * Remove DevStack workaround * Disable horizon for the tests in the gate * Independent log level for OVSDB * Remove two log traces that are no longer useful * Optimize the for loop in bind\_port() * Eliminate DeprecationWarning of \_ translation * Bug fix for an exception log * POC of ML2/OVS to OVN migration using ansible * Revert "Docs: Do not turn warnings into errors" * Remove pbr warnerrors in favor of sphinx check * Add journal and maintenance skeleton * Add network and subnet resources to OVNClient * Functional tests: Install SSL dependencies before compiling OVS 3.0.0.0b2 --------- * Support subnet DHCP enabling and disabling * Updated from global requirements * Gate failed due to validation tests * Inherit from ovsdbapp API classes * Add base migration scripts * Add OVNClient for Ports and L3 resources * Updated from global requirements * use worker from neutron-lib * readme: Update tutorial and talk links * Remove sleep() in \_sync() to make it callable * Remove BEFORE\_UPDATE event from \_subnet\_gateway\_ip\_update * Updated from global requirements * Use the ovsdbapp library * Switch to SUBNET resource from SUBNET\_GATEWAY * use extra\_dhcp\_opt api-def from neutron-lib * use is\_port\_trusted from neutron-lib * DevStack local.conf.sample: Disable Cinder and reference Horizon * Add OVN Trunk Driver Functional test * Updated from global requirements * use MechanismDriver from neutron-lib * Proposed support for Metadata API * Eliminate a DeprecationWarning of \_ translation * Note why extra devstackgaterc files exist * Fix bug of trunk plugin loading * use neutron-lib port security api-def * Fix ValueError When mapping muti physnets to a same OVS bridge * fix no update OVNNB static-route when change external subnet's gateway\_ip * use neutron-lib constants rather than plugin constants * pep8: stop ignoring \_ builtin usage * Stop translating log messages * Fix gate failures * Add direction to known bandwidth\_limit\_rules parameters * Fix intermittent failure of nat unit test cases * Sync rally config from Neutron * Fix OVNL3RouterPlugin.\_sb\_ovn mock error in OVNL3ExtrarouteTests * Fix the L3 unit tests race condition * consume neutron-lib callbacks * Acknowledge Neutron that DHCP has changed * Extend ACLs protocol support * Bug Fix For QOS * Fix unit/functional tests related to Neutron ovsdbapp use * Disable new N537 hacking check from next neutron-lib * Call the ovs 'Stream' class ssl\_set\_\* functions only if required * Pass original mac to \_get\_ovn\_dhcpv6\_opts when syncing * Repair master CI unit test and python3.5 dsvm functional test * Updated from global requirements * stop scanning for ocata release notes at 2.0.0 * Remove time.sleep() from unit-tests * Log the exception when disassociating floatingip fails 3.0.0.0b1 --------- * Support connecting OVN DB over SSL * docs: Remove dpdk-snapshot repo * docs: Update distro version note * Enhance local.conf.sample documentation * Docs: Uses DevStack script to create the user * Docs: Do not turn warnings into errors * Updated from global requirements * Allow tuning the probe interval from the IDL session * Remove DHCP and L3 agent remnants * Remove subunit-trace fork * Add "sudo make distclean" to cleanup\_ovn for devstack * Update local.conf.sample to enable automatic host discovery for cell * Optimize the link address for fetching git code * Implement disassociate\_floatingips in l3\_ovn.py * Fix OvnBaseConnection.get\_schema\_helper bug in some case * Support dsvm-functional test of provnet ports syncing * Fix some reST field lists in docstrings * Sync provnet lsp missing in OVN DB for provider network * Updated from global requirements * Switch trunk/cbs/buildlogs to use https * Correct written errors of assert\_called\_once\_with * Updated from global requirements * Use neutron-lib's get\_random\_mac * Add DHCP requests allowing ACL rule for OVN native DHCP * Refresh devstack testing docs * local.conf.sample: Disable q-meta * Double functional testing timeout to 120s * Switch back to ovs master * Use neutron-lib's context module * Updated from global requirements * Add missing @abc.abstractmethod * Prevent unnecessary floating IP dissociate during updating extra attributes * dsvm-functional test\_dhcp\_options failed in CentOS 7.3 * Add TCP connection functional test, TCP may use more than unix socket * Improve OVN DB sync dsvm-functional test for external gateway * Support OVS branch arg to devstackgaterc * Updated from global requirements * Update hacking version * Fix typo * Remove unused logging import * support functional tests with python 3.5 in CI jobs * Remove devstackgatenativeservicesrc * Get all security groups via api in OVN DB sync functional test * Integrate l3 gw test to l3\_ovn unit test * Hack to fix gate jobs * support functional tests with python 3.5 * fix deepcopy dict\_keys exception in python3.5 * Ignore syncing floating IP ports when handling DB sync * Re-enable three tests in OVNL3ExtrarouteTests * Clarify that native L3 is always used * doc: Remove modindex link * Improve OVN DB sync unit test for snat, gateway route and fip * doc: Remove references to the DHCP agent * Add Run Unit Tests and Run Functional Tests in the Testing documentation * Improve OVN impl idl unit test for snat and fip * Remove get\_router when handling floating IP * Support distributed NAT in networking-ovn native L3 routing * Remove support for py34 * Sync devstackgaterc with devstackgatenativeservicesrc * Use the neutron-lib imports in l3\_ovn * Add "enable\_service placement-api" to computenode-local.conf.sample * Updated from global requirements * Return id of created entity for LSwitch and LPort * Typo fix: diferent => different * Change assertEqual(A, None) by optimal assert like assertIsNone(A) * Update reno for stable/ocata 2.0.0 ----- * Enable placement-api in devstack CI jobs * Re-enable a passing test 2.0.0.0b3 --------- * Add two more good blog posts I just found * Issue of router updating with subnet and fixed-ip * Replace six.itervalues() with .values() Replace six.iterkeys() with "for key in dict" * Revise initial status of create\_floatingip * Use neutron-lib portbindings api-def * fix CI jobs broken after neutron.agent.ovsdb.native.Connection change * Remove support for py33 * Use neutron-lib provider net api-def * Updated from global requirements * Need not add ext gw router ip to peer nat\_addresses options * Adapt neutron-ovn-db-sync-util to ml2 abbr * fix some parameter description mistakes * neutron-ovn-db-sync-util cmd call trace * Resolve create\_nova\_conf\_neutron not found issue * Some change for unit test test\_update\_router\_with\_ext\_gw * Devstack failed for OVS DB connection modification * Fix wrong column change to Logical\_Switch\_Port when neutron port update * Remove redundant comment in local.conf.sample * Eliminate ovsdb error vlog when creating some LSPs * Fix wrong type for enable\_snat * readme: Add another blog post * Fix issue of adding router interface from dashboard with specified IP 2.0.0.0b2 --------- * Fix skydive git repo location * readme: Update blog, talk, and tutorial links * correcting variables seting in get\_ovn\_port\_options * Fix the CI failures after xenial uprade * Fix dhcp\_options fake row value in Class TestNBImplIdlOvn * Increase ovsdb timeout default value * Fix typo in devstack/local.conf.sample * Log OVS IDL library errors to neutron logs when used by networking-ovn * Retry port create to deal with race condition * Add system-id when start ovs * Improve neutron-ovn-db-sync-util error messages * Fix file permissions * Drop docs related to L3 agent * Show team and repo badges on README * Switch to using plugins directory in lieu of neutron manager * Replace six.iteritems() with .items() * Update GW SNAT rule in if already exists for 'dnat\_and\_snat' type * Fix a few grammatical errors * Finalize ovs transaction mutate support * Exclude the failing tempest test in devstackgaterc job * Exclude the failing tempest test in native tempest job 2.0.0.0b1 --------- * format string is error * Replaces uuid.uuid4 with uuidutils.generate\_uuid() * Updated from global requirements * rally: Sync rally job config from neutron * Drop L3 agent from CI jobs * Neutron lib integrations (L3) * Updated from global requirements * Disable q-dhcp in the devstackgaterc job * Remove workarounds for python ovs mutate bug * NAT support (SNAT, FloatingIP) * Updated from global requirements * Updated from global requirements * Skip set of OVSDB connection manager target * Updated from global requirements * Updated from global requirements * README: Add more blog links * Drop MANIFEST.in - it's not needed by pbr * Update .coveragerc after the removal of openstack directory * Fix file permissions * Use diff\_list\_of\_dict from neutron-lib * enhance DHCP with improved transaction * Support native OVN DHCPv6 * Fix tox\_install.sh * Fix the KeyError in neutron-ovn-db-sync-util * Use parse\_mappings from neutron-lib * Update tox install to support constraints and branches * Enable release notes translation * Refuse port binding if not supported for host * Updated from global requirements * Improve test coverage for add DHCP options command * Workaround OVS transaction mutate bug * Add unit tests for neutron-ovn-db-sync-util command * Fix 'uuid' to 'UUID' * Configure subnode for devstack multinode * Fix typos in native\_dhcp.rst & troubleshooting.rst * Revert "Workaround DHCP agent gate issues" * Updated from global requirements * Docstrings should not start with a space * Replace retrying with tenacity * Initial unit tests for impl\_idl\_ovn * Increase OVSDB monitor unit test coverage * Increase trunk driver unit test coverage * Increase ACL unit test coverage * Fix for vtep port * Add unit tests for ACL ovsdb commands * Fix test waiting for ovn-northd to start * Workaround DHCP agent gate issues * Add unit tests for lswitch and lrouter port ovsdb commands * Add unit tests for lrouter and addrset ovsdb commands * Update port provisioning block registration * Configure vxlan encap on computes for vtep * Support a mixed DPDK and non-DPDK environment * Remove ovsdb\_connection config option deprecated in Newton * Updated from global requirements * Add unit tests for update ovsdb commands * Add unit tests for lrouter static routes ovsdb commands * Update reno for stable/newton 1.0.0.0rc1 ---------- * clean stale port dhcpv4 options within port op * Fix dhcp\_disabled ip\_version error * ovn-remote port should be 6642 * OVN trunk driver to support vlan-aware-vms * Updated from global requirements * Add multi-provider and multi-segment support * Add OVS transaction mutate support * Add sync support for DHCP\_Options * Remove unnecessary mocks for impl\_idl\_ovn in unit tests * Clean imports in code * Add DB sync support for lrouter port networks * Fix order of arguments in assertEqual * Enhance port dhcpv4 options handling * Update segment\_data for TestOvnSbSync * Add \_\_ne\_\_ built-in function for networking\_ovn * BugFix: L3 scheduler scheduling distributed routers * Deployment fixes for DHCP and metadata support * Fail deployment if q-dhcp and OVN\_NATIVE\_DHCP enabled * gaterc: Avoid running entire tempest test suite on grenade job 1.0.0.0b3 --------- * Updated from global requirements * Accept IPv6 RAs on the interface with the default route * Remove unused devstackgatenativel3rc file * Enable q-meta service in devstackgaterc * Cleanup gate stack user configuration for functional tests * Updated from global requirements * Add initial OVS DB connection retry support * Fix workspace directory ownership for functional tests * Add functional test for OvnSbSynchronizer * Fix delete subnet without DHCP enabled * Add os-testr test requirement for neutron-lib job * align security\_group\_opts register * Increase log level for OVN port status updates * Fix UT based on improved ML2 driver error handling * Remove extra driver initialization during tests * Remove reference to neutron.i18n * Add script for neutron-lib source periodic job * Add unit tests for DHCP options ovsdb commands * Add initial unit tests for ovsdb commands * Remove ovs-vsctl set-manager call * Don't include openstack/common in flake8 exclude list * Grenade upgrade verification changes * OVN NB sync ignores ACLs on lswitch without ports * Use existing transactions to update port DHCP options * Enable DeprecationWarning in test environments * Rename native-l3 rc file to nativeservices * Sync support of SGs and ports to Address sets * doc: Update list of RPM repos for OVS master * Support installing ovs python module from ovs source * Doc and release note updates for native DHCP * Updated from global requirements * Add 'revisions' to supported extensions * Create segment\_host mapping after new segment * Use OVN native DHCP on kuryr gate job * Update address set with security group name update * OVN L3 service plugin does not need agent RPC * Add Python 3.5 classifier and venv * Clear stale SegmentHostMapping when sync ovn sb db * Fail address set update if doesn't exist on port create * Avoid error when removing addresses from address set * Fix pep8 failures * Filter duplicate DHCP ACLs * Fix the failing functional test "test\_ovsdb\_monitor\_lock" * Don't update ACLs on security group update * Support native OVN DHCPv4 * Functional Test: Sync ACLs * Add OVN L3 Router Scheduler * Fix add\_router\_interface * Use OVSDB transactions for address sets * Fix OVN L3 unit tests * Update the home-page info with the developer documentation * Vagrant: Don't duplicate networking-ovn directory * Fix provider network setup * Only update port ACLs if necessary * Change tunnel MTU calculation to support IPv6 * [doc] Prettify logical flow examples * [doc] Update refarch router section * Fix details for align lrotuer port networks * Update instance sections of reference architecture * Fix metadata typos in updated self-service flows * align lrouter port networks * Update self service network with new OVN flows * Update OVN data in provider network section * Spelling mistake:addded should be added * Grenade plugin for testing OVN migration from ML2/OVS * Updated from global requirements * faq: Update HA to reflect ovsdb replication * [docs] Clarify node requirements 1.0.0.0b2 --------- * Update segment host binding * Use OVN address set to implement remote security groups * Update OVN reference architecture documentation * Use AFTER\_INIT in lieu of AFTER\_CREATE * Show statistics after running coverage * Update to neutron-ovs-cleanup gate failure fix * Fix gate failures * Fix provider configuration in Vagrant file * Add functional tests for ovsdb-monitor * tox.ini: Stop using zuul-cloner for venv * Moving functions from devstack/plugin.sh to a library file * Fix py34 unit test failures * Remove 'origin/' in OVN\_BRANCH * OVSDB Monitor: fix comment * Updated from global requirements * Make provider networking devstack examples work * Functional Test: Sync lrouter static routes * Functional Test: Create OVN NB DB resources * Use blank default for Q\_AGENT * Remove unneeded /etc/neutron/dnsmasq.conf configuration * Support vlan tenant network type * Support geneve tenant network type * Updated from global requirements * Fix vagrant provider network setup * Fix binding profile tag error message * Rename ovn\_nb\_sync to ovn\_db\_sync * Add functional tests and gate hook functions * Finish refactoring for Logical\_Switch\_Port * Support ML2 option to enable security groups * Fix port range for icmp type * Additional refactoring for Logical\_Switch\_Port * Use Logical\_Switch\_Port in NB * Use ml2\_conf.ini for OVN configuration options * Fix ML2 driver unit tests * Fix unit test failure * Remove ML2 mechanism driver \_ovn property * config: Correct wording on help text * Fix gate-tempest-dsvm-networking-ovn-native-l3-nv job * Add more config validation to neutron-ovn-db-sync-util * Get information of Chassis from OVN SB DB * Enable ML2 driver unit test * Vagrant: Add support for running a HW VTEP node * devstack: Add support for running the vtep emulator * Add notes for ML2 driver port binding 500 errors * Vagrant: Fix incorrect provider configuration * Use PROTO\_NAME\_IPV6\_ICMP\_LEGACY from neutron lib * Remove unused POT file * Sync static routes * Updated from global requirements * Convert core plugin to ML2 mechanism driver * refactor sg/sgr event handing and add test coverage * Add more ACL unit tests * OVN NB sync may delete all ACLs for a port * neutron\_sync\_mode not honored when syncing ACLs * Add OVN NB sync support for ML2 * README update * Fix ML2 attribute error accessing \_ovn * Add driver unit tests for ML2 * Fix update ACLs for rules with remote security groups * drop leading \_ for publicly used acl methods * Migrate core\_plugin to common acl code * Move \_refresh\_remote\_security\_group to common/acl * Add QoS support to ML2 driver * Move \_update\_acls\_for\_security\_group/\_add\_acls to common/acl * Move \_add\_sg\_rule\_acl\_for\_port to common/acl * Move \_acl\_remote\_group\_id to common/acl * Remove unused oslo.concurrency requirement * Updated from global requirements * OVN NB sync doesn't ignore non-neutron routers * Move \_acl\_remote\_match\_ip to common/acl * Move \_get\_sg\_from\_cache to common/acl * Move \_get\_sg\_ports\_from\_cache to common/acl * Move \_get\_subnet\_from\_cache to common/acl * Change override-defaults to not override Q\_PLUGIN * Add comment on local.conf.sample on how to enable ml2 plugin * Add support for static routes * Leverage common/acl in existing core\_plugin * ACL updates for protocol names and numbers * Vagrant: Add support for parallels and libvirt * Fix port binding for ML2 * Convert TCP/UDP port numbers to protocol names * make acl match recognize ipv6 icmp alias and number * Updated from global requirements * Add L3 unit tests for ML2 * Fix admin context usage for ML2 * Add initial ML2 mechanism driver support * Updated from global requirements * Add configuration for router admin state * Updated from global requirements * Use real UUID for versions objects * Remove attributes deprecated warnings * Use exceptions from neutron\_lib * Updated from global requirements * Switch to use setup\_develop of devstack * Correct blog link 'ovn-l3-deepdive' in README.rst * Add release note for address scope * Fixed vagrant download link * Improve networking-ovn/testing.rst * Create bridge for OVN L3 in devstack * neutron\_acls(list) is duplicated in sync\_acls * Add initial newton release note * Switch to using hacking checks from neutron-lib * Remove unused constant in test\_ovsdb\_monitor * Router and Router Port sync enhancement for neutron-ovn-nb-sync tool * plugin: Add the address scopes mixin * Fix releasenotes * Updated from global requirements * DevStack: Fix stack.sh when OFFLINE=True * Updated from global requirements * devstack: Tweak sample config * Fix typo * Fix coverage option and execution * Use constants from neutron-lib * devstack: Enable skydive-agent in local.conf.sample * Updated from global requirements * Fix possible AttributeError exceptions in \_handle\_qos\_notification() * improve performance of get\_networks * Add tests to OVN Native L3 gate job * QOS: use neutron api's for policy & network/port binding * Fix failure when ovn sync port info with neutron * devstack: Use more OVN default paths * Use Neutron DB for provider network details * Start ovn-northd in the devstack screen as a foreground process * Update ACL without compare for add/del sg rule * Add Provider Network enabling for devstack * Vagrant: Fix OVN NB and SB database connection * Vagrant: Consolidate base package install commands * Sync ACLs between Neutron and NB database * devstack: Add Skydive for OVN devstack * Handle OVN NB and SB databases with separate ovsdb-server procs * Doc: Update features for Mitaka * Support Allowed address pairs * Support port security API extension * Correct the use of STOP\_EVENT * use utils.ovn\_name to replace string joint * make comment more specific that we are talking about the ovn lport\_name * Vagrant: Synchronize clock for live migration * Reduce number of OVSDB row changes during ACL updates * Multiple consumers for PLUGIN topic * Drop unnecessary comment * Add in missing space * Translations: ensure that the locale directory * LOG.warn is deprecated in python3 * Create devstack gaterc file for new ovn gate job * devstack: Don't enable tempest in rally job * Add provnet info to API results * Use "https" instead of "http" in the default URL for OVN\_REPO * Re-enable some IPv6 tests * Run cross-tenant traffic tempest test * Doc: Add missing Neutron API extensions * Update supported API extensions for tempest * Fix QoS unit tests * install: Add references to RPM locations * Updated from global requirements * Vagrant: Add live migration support * Add in missing spaces * OVN: : make use of neutron\_lib constants * Provider network model optimization * Resolve auto addr subnet create failure due to race condition * Replace infinite while loop with test\_with\_retry * Fix errors in OVN testing document * Make ovn-controller the primary virtual machine in Vagrantfile * Store all the fixed ips of a port in one entry in Logical\_Port.addresses * Updated from global requirements * Support neutron-vpnaas when OVN\_L3\_MODE=False * Remove limitation on networking-ovn git location * Updated from global requirements * Change ovn-northd invocation to single log * Minor ACL syntax simplification * Document active/passive HA without shared storage * Delete ACLs of ports on provider network when port is deleted * Docs: Add reference architecture * Vagrant: Update readme * Network availability zone deployment support * DevStack: Adjust MTU value for GENEVE * Change OVN\_L3\_MODE default to True * Vagrant: Bump compute nodes to 1.5GB of memory a piece * Add notes for switching ovn\_l3\_mode from False to True * Vagrant: Modify services * DevStack: Support disabling all OVN services * Enabling qos support through Logical\_Port.options * doc: Document HOST\_IP setting requirement * Fix several ovsdb verify() issues * doc: Rename doc to "install" * Neutron ovn northbound db sync tool * doc: Note DPDK support in the features doc * DPDK support for OVN * Vagrant: make provider network creation configurable * devstack: Add devstackgatekuryrrc file for OVN + kuryr job * Refactor ovn\_nb\_sync.OvnNbSynchronizer * devstack: Source ovs devstack lib later * Add extension fields on network and port create * testing: Remove comment about looking at patch ports * Document neutron API extensions supported * Add support for net-mtu extension * Vagrant: Generify post-config options for DevStack * Use compile\_ovs() from Neutron tree * Resolve NetworkInUse race condition * Devstack: Improve DHCP and metadata agents * Add network availability zone support * Docs: Implement doc8 linter * Devstack: Add native MTU option * Vagrant: Workaround shebang insanities * Vagrant: Increase the size of the provider network subnet * Fix gate problem with tox * Use uppercase 'S' in word "OpenStack" * Vagrant: Stop sharing the ~/devstack directory * Update the HA section of the FAQ * Vagrant: Share local directories * Vagrant: Modify OpenStack services * Removed unnecessary code from the plugin * Create periodic status task to check dhcp agents in Ovn Worker * Add msec resolution to ovn-northd console logs * Vagrant: Add docs for instance external net access * Sync neutron db and OVN NB db from the Ovn Worker context * doc: Provide a method for access to private net * Remove the debug logs in RowEvent.matches for unmatched conditions * Fix format of externals whitelist * Vagrant: Consistency with upstream docs * doc: Use ovn-sbctl show * doc: Further reflect that OVN L3 is the default * Add a dhcp acl to allow dhcp-client to dhcp-server * Vagrant: Fix various networking bits * Update translation setup * Make OVS related logs multi-stack friendly * Update DevStack testing documentation * extensions: Add the "subnet\_allocation" extension to OVN * Vagrant: Enable OVN commands to use a remote DB * Updated from global requirements * Make VM interface MTUs configurable * Change 'ifconfig' to 'ip' * Support modifying external network's attribute * Fix add port mac address which fixed\_ips is None * Vagrant: Adjust HOST\_IP for compute nodes * Vagrant: Fix issue with boxes * Revert "Deployment: Update with OVN DB requirements" * Modify docs build environment * Vagrant: Completely redo the Vagrant configuration * Run northd and ovn-controller with --pidfile * HOST\_IP is missing in computenode-local.conf.sample * Fix in create\_router and update\_router * Devstack: cleanup datapath * Expose ovs-vswitchd log to file * Make master networking-ovn work with stable/liberty * Deployment: Update with OVN DB requirements * Updated from global requirements * devstack: Move tox install * Drop l3 directory * Pass environment variables of proxy to tox * rename ip -> ip\_version * Fix a typo of 'security\_group' * Add port 'up' and 'down' notification * Support remote\_group\_id across networks by matching IP addresses * docs: Remove one last mention of the OVN mechanism driver * Use elevated context when updating ACLs for remote-group-id * Updated from global requirements * Fix in ovn plugin * Remove deprecation warnings from unit tests * devstack: Add config for building ovs modules * Fix direction problem for sec-group rules with remote\_ip\_prefix * LOG.warn -> LOG.warning * Trivial optimize in create\_network * Introduce data structure called OvnPortInfo * Remove redundant None with dictionary get * remove python 2.6 trove classifier * Enable monitor2 back * Ensure that portbindings are tested * [docs] Improve deployment tool integration * Undo neutron network if lswitch was not created * Disable ovsdb monitor2 * Fix remote\_group handling when remote\_group is not self * Remove the openstack-common.conf file * doc: Add deployment tool integration guide * devstack: Print dmesg after loading modules * Fix update\_port for security group change when remote\_group involved * Fix up imports * doc: Fix some doc errors * Add in constants for ACL actions * Move ACL\_\* definitions to the constants file * Move closing ): to new line to improve readability * Add helper method \_drop\_all\_ip\_traffic\_to\_port and tests * Import clean ups * Convert code to use contextlib instead of multiple nested withs * Make sure correct branch of neutron is pulled in * devstack plugin: avoid explicit OVS module dependency * devstack: Switch back to ovs master * devstack: Update list of kernel modules to load * Add Rally to networking-ovn * Add testing documentation for using OVN L3 mode * Add a FAQ entry about distributed L3 routing * Update router interface add/delete for ovsdb ovn-nb schema change * Add Guru's talk from OVS Conference * Add reno for release notes management * OVN: use the \_ from the networking\_ovn.\_i18n file * OVN: remove translations for debug logs * OVN: provide details on how to get DHCP port * OVN: remove white space from testing.rst * Remove deprecated parameters * Add in missing spaces * Use choices for neutron\_sync\_mode option * Switch to internal \_i18n pattern, as per oslo\_i18n guidelines * Add missing lo translation * Setup for translation * fix few pieces of inaccurate information in vagrant readme.rst * Add gsagie's latest blog post on OVN L3 * Remove unneeded test file * Enable dhcp\_opt tests * Update the extra dhcp options in port update * Note that you should disable q-l3 with OVN\_L3\_MODE * Updated from global requirements * Fix typos with topy * Make vagrant README.rst more accurate * Updated steps floatingip create and assign * make devstack testing doc a bit more clear on host name * Enable L3 API unit tests to be run * Use trueorfalse for OVN\_L3\_MODE * Add/delete router interfaces * Add a FAQ to the docs * Allow DHCP responses to reach clients * Port type and options were not passed to ovn * Populate mac and ip address of lport * Update documentation links * Prevent DBError caused by wrong param passed to get\_security\_group * fix the doc error * Revert "Support OFFLINE mode" * Fix some doc typos * Update Vagrant configuration to deploy 3-node OVN setup * Move Vagrant configuration into the vagrant directory * Support OFFLINE mode * Install kernel-devel on Fedora/CentOS/etc * Add etc to .gitignore * Check valid option combos in \_get\_ovn\_port\_options * Remove unused method \_delete\_ports() * Remove duplicate call to \_get\_allowed\_mac\_addresses\_from\_port() * Add helper method \_update\_port\_in\_ovn() * Pull out ovn config options to helper method * Fix argument order in assertEqual to (expect, obs) * Updated from global requirements * Add security group support using OVN ACLs * Exclude some more tempest tests * support OVN NB Logical Router name Update * Don't log error on expected condition * Update macs to addresses because of OVN schema update for l3 * Remove retry decorator as neutron has moved them into api layer * Don't log an error in ovsdb commands * Add provider network support * Explicitly install tox * Adjust devstack README formatting * tox.ini: Fix cover by giving the source directory explicitly * Updated from global requirements * Fix rpc code due to neutron changes * Add topics.REPORTS listener * Neutron VTEP integration * Automated setup using Vagrant + Virtualbox * Skip test\_dualnet\_multi\_prefix\_dhcpv6\_stateless * Add if\_exists arg to all update/delete commands in ovsdb/commands.py * Change ignore-errors to ignore\_errors * Updated from global requirements * devstack: Remove ovs during unstack * Stop logging deadlock tracebacks * Add subtransactions=True on transaction for delete\_port/network * Make networking-ovn use its own config file * devstack: Remove API\_WORKERS from local.conf.sample * Removing unused dependency: discover * Use callbacks to initialize the OVSDB connection after forking * Stop doing any magic cloning of neutron during CI * devstackgaterc: Run some security group tests * Updated from global requirements * Don't set MYSQL\_DRIVER in local.conf.sample * Run more tests in tempest job * Updated from global requirements * Fix TestOvnPlugin.test\_create\_port\_security * Fix OVN\_PORT\_BINDING\_PROFILE * Sync oslo.config update from global requirements * Sync pbr update from global requirements * devstack: Set VM MTU to 1400 * devstack: Use setup\_package instead of setup\_develop * Adjust test-requierments change * Add APIs for deleting logical router port * Make vnc work on compute node * Add changes from global reqs * make needs to be installed as well to build ovs * Update\_network: use appropriate identifier for network\_id * Fix logical port delete * Note that we'll use "allow-related" for security groups * Remove ml2 entry point from setup.cfg * Add create/delete router to plugin * Align unit tests to plugin * Add logical route delete and logical router port add API's * Align OVN sync module to work with the plugin * Remove l3 router plugin entry point from setup.cfg * Remove security groups callback code * Add new docs link to README * Convert plugin away from ML2 * Create Logical Router OVSDB API implementation * Updated from global requirements * Skip provider network tempest api tests for now * Set MYSQL\_DRIVER=MySQL-python in local.conf.sample * Increase ovsdb timeout * Add support for port admin\_state\_up * Update AddLogicalPortCommand to work with new ovsdb schema * Skip allowed-address-pair tempest tests * Add unit tests for allowed\_address\_pairs port security part * Remove blank lines from requirements.txt * Add check for port\_security\_enabled attribute * Updated from global requirements * Set API\_WORKERS=0 in local.conf.sample * Add .sw? to .gitignore * Remove random \_ in code * Start disabling failing tests * Updated from global requirements * Updated from global requirements * Control list of enabled services for tempest job * Disable apparmor for libvirtd * Updated from global requirements * devstack: OVN is now in master OVS branch * We should whitelist bash rather than sh * Fix error messages where self.name is invalid * Updates to reflect move from stackforge to openstack * Update .gitreview file for project rename * devstack: Add devstackgaterc file * Updated from global requirements * Fix import in l3 plugin stub * docs: separate design docs from general docs * Add new blog link to documentation * devstack: Add license header to plugin.sh * devstack: enable q-meta * Enable exception raising when OVSDB transaction fails * Updated from global requirements * Add notes about kernel support for geneve * Add common pitfalls document * Change default sync mode to 'log' * Drop TODO file in favor of just using bugs * Add Unit tests for OVN Mech driver * testing: Add latest blog post to references * Remove unneeded \_\_init\_\_ * devstack: Add multi-node support * devstack: Don't force enable key,mysql,rabbit * drop ovn.filters * Changes in TODO document * requirements.txt: Add ovs * Remove incorrect comment * Set port security for logical port * Extract strings into constants file * Add additional links to the testing document * Remove Logical Switch neutron:network\_id External Id * Fix local.conf example * Fixes in TODO * Add sync mechanism between neutron DB and OVN-NB DB * docs: Add additional resources links * docs: clean up index * devstack: Run ovn-controller with sudo * Reduce the number of OVSDB transactions * Drop version= from setup.cfg * devstack: Update integration bridge config * docs: Add testing howto doc * Move TODO out of published docs * devstack: Give example of custom ovs git repo * devstack: Fix git repo name assumptions * docs: Remove content from usage.rst * devstack: Change tunnel encap to geneve * local.conf.sample: Enable the rest of OpenStack * Security Groups Data Model * Extract ovn\_name method to utils file * Support parent\_name and tag in binding:profile * Add support for lport parent\_name and tag * ovsdb: s/if\_exist/if\_exists/ * Remove if\_exists handling from MacCommand * Fix if\_exists in DelLSwitchCommand * Fix Logical Port MAC setting * Fix Logical Switch External Id * Security Groups API handler * Change transaction timeout configuration * devstack: rename ovn-nbd to ovn-northd * Remove set\_lport\_up\_status() * todo: note neutronclient binding:profile patch * Add router\_interface add/delete into L3 service plugin * First patch of OVSDB support for OVN * devstack: Start ovn-nbd and ovn-controller * Update README contents * Update todo * devstack: Workaround devstack job config issue * devstack: Create br-int * todo: Add security groups, track assignees * doc: Fix formatting errors * Devstack: Install OVS python binding * Change OVN schema files names * devstack: Put ovs source in $DEST * Changes to plugin.sh * todo: Add container integration to the todo * Update todo * Use abstraction API for OVN configuration * Add in common configuration file * Add comment for nbctl current implementation * Setup networking-ovn to use pretty\_tox.sh * devstack: clean up db lock files * Set lport name to be the port ID * devstack: install and run ovs+ovn * Add OVSDB abstraction between API and implementation * Git ignore hidden directories * Ensure that ports are configured correctly * Correct usage of the external-id for networks * devstack: fix up example config to reflect ML2 * Ensure that commands passed to execute are a list * Fix attribute access * Add CRUD for ports * Add CRUD for networks * Set VIF type and details * tox.ini: only test py27 and pep8 * Set the initial networking-ovn version * Remove python 2.6 support * Fix plugin.sh stages * Add entry point definitions * Enable external hacking tools * Add skeleton for L3 service plugin * Fix typo in documentation * Add document mapping Neutron to ovn-nb db * Remove (incorrect) copyrights * Revert "Change from ML2 to monolithic plugin" * Add devstack support * Change from ML2 to monolithic plugin * Add a TODO document * Add skeleton of ML2 MechanismDriver * Add neutron as a requirement * Initial Cookiecutter Commit * Added .gitreview networking-ovn-4.0.0/test-requirements.txt0000666000175100017510000000125013245511145020770 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 flake8-import-order==0.12 # LGPLv3 python-subunit>=1.0.0 # Apache-2.0/BSD sphinx!=1.6.6,>=1.6.2 # BSD openstackdocstheme>=1.18.1 # Apache-2.0 doc8>=0.6.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 os-testr>=1.0.0 # Apache-2.0 pylint==1.4.5 # GPLv2 testresources>=2.0.0 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD WebTest>=2.0.27 # MIT testtools>=2.2.0 # MIT reno>=2.5.0 # Apache-2.0 networking-ovn-4.0.0/networking_ovn/0000775000175100017510000000000013245511554017604 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/0000775000175100017510000000000013245511554020531 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/agent/0000775000175100017510000000000013245511554021627 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/agent/metadata/0000775000175100017510000000000013245511554023407 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/agent/metadata/config.py0000666000175100017510000001326513245511145025233 0ustar zuulzuul00000000000000# Copyright 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 itertools import shlex from neutron_lib.utils import host from oslo_config import cfg from oslo_privsep import priv_context from networking_ovn._i18n import _ DEDUCE_MODE = 'deduce' USER_MODE = 'user' GROUP_MODE = 'group' ALL_MODE = 'all' SOCKET_MODES = (DEDUCE_MODE, USER_MODE, GROUP_MODE, ALL_MODE) SHARED_OPTS = [ cfg.StrOpt('metadata_proxy_socket', default='$state_path/metadata_proxy', help=_('Location for Metadata Proxy UNIX domain socket.')), cfg.StrOpt('metadata_proxy_user', default='', help=_("User (uid or name) running metadata proxy after " "its initialization (if empty: agent effective " "user).")), cfg.StrOpt('metadata_proxy_group', default='', help=_("Group (gid or name) running metadata proxy after " "its initialization (if empty: agent effective " "group).")), cfg.StrOpt('ovs_integration_bridge', default='br-int', help=_('Name of Open vSwitch bridge to use')) ] METADATA_PROXY_HANDLER_OPTS = [ cfg.StrOpt('auth_ca_cert', help=_("Certificate Authority public key (CA cert) " "file for ssl")), cfg.HostAddressOpt('nova_metadata_host', default='127.0.0.1', deprecated_name='nova_metadata_ip', help=_("IP address or DNS name of Nova metadata " "server.")), cfg.PortOpt('nova_metadata_port', default=8775, help=_("TCP Port used by Nova metadata server.")), cfg.StrOpt('metadata_proxy_shared_secret', default='', help=_('When proxying metadata requests, Neutron signs the ' 'Instance-ID header with a shared secret to prevent ' 'spoofing. You may select any string for a secret, ' 'but it must match here and in the configuration used ' 'by the Nova Metadata Server. NOTE: Nova uses the same ' 'config key, but in [neutron] section.'), secret=True), cfg.StrOpt('nova_metadata_protocol', default='http', choices=['http', 'https'], help=_("Protocol to access nova metadata, http or https")), cfg.BoolOpt('nova_metadata_insecure', default=False, help=_("Allow to perform insecure SSL (https) requests to " "nova metadata")), cfg.StrOpt('nova_client_cert', default='', help=_("Client certificate for nova metadata api server.")), cfg.StrOpt('nova_client_priv_key', default='', help=_("Private key of client certificate.")) ] UNIX_DOMAIN_METADATA_PROXY_OPTS = [ cfg.StrOpt('metadata_proxy_socket_mode', default=DEDUCE_MODE, choices=SOCKET_MODES, help=_("Metadata Proxy UNIX domain socket mode, 4 values " "allowed: " "'deduce': deduce mode from metadata_proxy_user/group " "values, " "'user': set metadata proxy socket mode to 0o644, to " "use when metadata_proxy_user is agent effective user " "or root, " "'group': set metadata proxy socket mode to 0o664, to " "use when metadata_proxy_group is agent effective " "group or root, " "'all': set metadata proxy socket mode to 0o666, to use " "otherwise.")), cfg.IntOpt('metadata_workers', default=host.cpu_count() // 2, help=_('Number of separate worker processes for metadata ' 'server (defaults to half of the number of CPUs)')), cfg.IntOpt('metadata_backlog', default=4096, help=_('Number of backlog requests to configure the ' 'metadata server socket with')) ] OVS_OPTS = [ cfg.StrOpt('ovsdb_connection', default='unix:/usr/local/var/run/openvswitch/db.sock', help=_('The connection string for the native OVSDB backend.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use unix:FILE for unix domain socket connection.')), cfg.IntOpt('ovsdb_connection_timeout', default=180, help=_('Timeout in seconds for the OVSDB ' 'connection transaction')) ] def register_meta_conf_opts(opts, cfg=cfg.CONF, group=None): cfg.register_opts(opts, group=group) def list_metadata_agent_opts(): return [ ('DEFAULT', itertools.chain( SHARED_OPTS, METADATA_PROXY_HANDLER_OPTS, UNIX_DOMAIN_METADATA_PROXY_OPTS) ), ('ovs', OVS_OPTS) ] def get_root_helper(conf): return conf.AGENT.root_helper def setup_privsep(): priv_context.init(root_helper=shlex.split(get_root_helper(cfg.CONF))) networking-ovn-4.0.0/networking_ovn/conf/agent/metadata/__init__.py0000666000175100017510000000000013245511145025504 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/agent/__init__.py0000666000175100017510000000000013245511145023724 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/conf/__init__.py0000666000175100017510000000000013245511145022626 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/0000775000175100017510000000000013245511554020171 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/models.py0000666000175100017510000000317413245511145022031 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron_lib.db import model_base import sqlalchemy as sa from sqlalchemy.dialects import sqlite class OVNRevisionNumbers(model_base.BASEV2): __tablename__ = 'ovn_revision_numbers' __table_args__ = ( model_base.BASEV2.__table_args__ ) standard_attr_id = sa.Column( sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), sa.ForeignKey('standardattributes.id', ondelete='SET NULL'), nullable=True) resource_uuid = sa.Column(sa.String(36), nullable=False, primary_key=True) resource_type = sa.Column(sa.String(36), nullable=False, primary_key=True) revision_number = sa.Column( sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), server_default='0', nullable=False) created_at = sa.Column( sa.DateTime().with_variant( sqlite.DATETIME(truncate_microseconds=True), 'sqlite'), default=sa.func.now()) updated_at = sa.Column(sa.TIMESTAMP, server_default=sa.func.now(), onupdate=sa.func.now()) networking-ovn-4.0.0/networking_ovn/db/revision.py0000666000175100017510000001247413245511145022407 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron.db import standard_attr from neutron_lib.db import api as db_api from oslo_db import api as oslo_db_api from oslo_log import log from sqlalchemy.orm import exc from networking_ovn.common import constants as ovn_const from networking_ovn.common import exceptions as ovn_exc from networking_ovn.common import utils from networking_ovn.db import models LOG = log.getLogger(__name__) STD_ATTR_MAP = standard_attr.get_standard_attr_resource_model_map() # 1:2 mapping for OVN, neutron router ports are simple ports, but # for OVN we handle LSP & LRP objects STD_ATTR_MAP[ovn_const.TYPE_ROUTER_PORTS] = STD_ATTR_MAP[ovn_const.TYPE_PORTS] _wrap_db_retry = oslo_db_api.wrap_db_retry( max_retries=ovn_const.DB_MAX_RETRIES, retry_interval=ovn_const.DB_INITIAL_RETRY_INTERVAL, max_retry_interval=ovn_const.DB_MAX_RETRY_INTERVAL, inc_retry_interval=True, retry_on_deadlock=True) def _get_standard_attr_id(session, resource_uuid, resource_type): try: row = session.query(STD_ATTR_MAP[resource_type]).filter_by( id=resource_uuid).one() return row.standard_attr_id except exc.NoResultFound: raise ovn_exc.StandardAttributeIDNotFound( resource_uuid=resource_uuid) @_wrap_db_retry def create_initial_revision(resource_uuid, resource_type, session, revision_number=ovn_const.INITIAL_REV_NUM): LOG.debug('create_initial_revision uuid=%s, type=%s, rev=%s', resource_uuid, resource_type, revision_number) with session.begin(subtransactions=True): std_attr_id = _get_standard_attr_id( session, resource_uuid, resource_type) row = models.OVNRevisionNumbers( resource_uuid=resource_uuid, resource_type=resource_type, standard_attr_id=std_attr_id, revision_number=revision_number) session.add(row) @_wrap_db_retry def delete_revision(resource_id, resource_type): LOG.debug('delete_revision(%s)', resource_id) session = db_api.get_writer_session() with session.begin(): row = session.query(models.OVNRevisionNumbers).filter_by( resource_uuid=resource_id, resource_type=resource_type).one_or_none() if row: session.delete(row) def _ensure_revision_row_exist(session, resource, resource_type): """Ensure the revision row exists. Ensure the revision row exist before we try to bump its revision number. This method is part of the migration plan to deal with resources that have been created prior to the database sync work getting merged. """ # TODO(lucasagomes): As the docstring says, this method was created to # deal with objects that already existed before the sync work. I believe # that we can remove this method after few development cycles. Or, # if we decide to make a migration script as well. with session.begin(subtransactions=True): try: session.query(models.OVNRevisionNumbers).filter_by( resource_uuid=resource['id'], resource_type=resource_type).one() except exc.NoResultFound: LOG.warning( 'No revision row found for %(res_uuid)s (type: ' '%(res_type)s) when bumping the revision number. ' 'Creating one.', {'res_uuid': resource['id'], 'res_type': resource_type}) create_initial_revision(resource['id'], resource_type, session) @_wrap_db_retry def bump_revision(resource, resource_type): session = db_api.get_writer_session() revision_number = utils.get_revision_number(resource, resource_type) with session.begin(): _ensure_revision_row_exist(session, resource, resource_type) std_attr_id = _get_standard_attr_id( session, resource['id'], resource_type) row = session.merge(models.OVNRevisionNumbers( standard_attr_id=std_attr_id, resource_uuid=resource['id'], resource_type=resource_type)) if revision_number < row.revision_number: LOG.debug( 'Skip bumping the revision number for %(res_uuid)s (type: ' '%(res_type)s) to %(rev_num)d. A higher version is already ' 'registered in the database (%(new_rev)d)', {'res_type': resource_type, 'res_uuid': resource['id'], 'rev_num': revision_number, 'new_rev': row.revision_number}) return row.revision_number = revision_number session.merge(row) LOG.info('Successfully bumped revision number for resource ' '%(res_uuid)s (type: %(res_type)s) to %(rev_num)d', {'res_uuid': resource['id'], 'res_type': resource_type, 'rev_num': revision_number}) networking-ovn-4.0.0/networking_ovn/db/head.py0000666000175100017510000000142113245511145021440 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 networking_ovn.db import models # noqa from neutron.db.migration.models import head def get_metadata(): return head.model_base.BASEV2.metadata networking-ovn-4.0.0/networking_ovn/db/__init__.py0000666000175100017510000000000013245511145022266 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/maintenance.py0000666000175100017510000000511613245511145023026 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron.db import standard_attr from neutron_lib.db import api as db_api import sqlalchemy as sa from networking_ovn.common import constants as ovn_const from networking_ovn.db import models def get_inconsistent_resources(): """Get a list of inconsistent resources. :returns: A list of objects which the revision number from the ovn_revision_number and standardattributes tables differs. """ sort_order = sa.case(value=models.OVNRevisionNumbers.resource_type, whens=ovn_const.MAINTENANCE_CREATE_UPDATE_TYPE_ORDER) session = db_api.get_reader_session() with session.begin(): return (session.query(models.OVNRevisionNumbers). join( standard_attr.StandardAttribute, models.OVNRevisionNumbers.standard_attr_id == standard_attr.StandardAttribute.id). filter( models.OVNRevisionNumbers.revision_number != standard_attr.StandardAttribute.revision_number). order_by(sort_order).all()) def get_deleted_resources(): """Get a list of resources that failed to be deleted in OVN. Get a list of resources that have been deleted from neutron but not in OVN. Once a resource is deleted in Neutron the ``standard_attr_id`` foreign key in the ovn_revision_numbers table will be set to NULL. Upon successfully deleting the resource in OVN the entry in the ovn_revision_number should also be deleted but if something fails the entry will be kept and returned in this list so the maintenance thread can later fix it. """ sort_order = sa.case(value=models.OVNRevisionNumbers.resource_type, whens=ovn_const.MAINTENANCE_DELETE_TYPE_ORDER) session = db_api.get_reader_session() with session.begin(): return session.query(models.OVNRevisionNumbers).filter_by( standard_attr_id=None).order_by(sort_order).all() networking-ovn-4.0.0/networking_ovn/db/migration/0000775000175100017510000000000013245511554022162 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/0000775000175100017510000000000013245511554026012 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/0000775000175100017510000000000013245511554027662 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/CONTRACT_HEAD0000666000175100017510000000001513245511145031575 0ustar zuulzuul000000000000001d271ead4eb6 networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/initial_branchpoint.py0000666000175100017510000000154313245511145034255 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. """initial branchpoint Revision ID: initial_branchpoint Revises: None Create Date: 2017-04-27 17:08:39.819577 """ # revision identifiers, used by Alembic. revision = 'initial_branchpoint' down_revision = None def upgrade(): pass networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/0000775000175100017510000000000013245511554030612 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/expand/0000775000175100017510000000000013245511554032071 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000021400000000000011212 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/expand/e229b8aad9f2_add_journal_and_maintenance_tables.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/expand/e229b8aad9f0000666000175100017510000000500213245511145033627 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. """add ovn_journal and ovn_maintenance tables Revision ID: e229b8aad9f2 Revises: ac094507b7f4 Create Date: 2017-04-28 11:41:47.487584 """ # revision identifiers, used by Alembic. revision = 'e229b8aad9f2' down_revision = 'ac094507b7f4' from alembic import op from oslo_utils import uuidutils import sqlalchemy as sa def upgrade(): op.create_table( 'ovn_journal', sa.Column('seqnum', sa.BigInteger(), primary_key=True, autoincrement=True), sa.Column('object_type', sa.String(36), nullable=False), sa.Column('object_uuid', sa.String(36), nullable=False), sa.Column('operation', sa.String(36), nullable=False), sa.Column('data', sa.PickleType, nullable=True), sa.Column('state', sa.Enum('pending', 'processing', 'failed', 'completed', name='state'), nullable=False, default='pending'), sa.Column('retry_count', sa.Integer, default=0), sa.Column('created_at', sa.DateTime, default=sa.func.now()), sa.Column('last_retried', sa.TIMESTAMP, server_default=sa.func.now(), onupdate=sa.func.now()), ) maint_table = op.create_table( 'ovn_maintenance', sa.Column('id', sa.String(36), primary_key=True), sa.Column('state', sa.Enum('pending', 'processing', name='state'), nullable=False), sa.Column('processing_operation', sa.String(70)), sa.Column('lock_updated', sa.TIMESTAMP, nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()) ) # Insert the only row here that is used to synchronize the lock between # different Neutron processes. op.bulk_insert(maint_table, [{'id': uuidutils.generate_uuid(), 'state': 'pending'}]) ././@LongLink0000000000000000000000000000016100000000000011213 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/expand/ac094507b7f4_initial.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/expand/ac094507b7f0000666000175100017510000000166713245511145033477 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. """initial networking-ovn contract branch Revision ID: ac094507b7f4 Create Date: 2017-04-27 17:10:02.788089 """ from neutron.db.migration import cli # revision identifiers, used by Alembic. revision = 'ac094507b7f4' down_revision = 'initial_branchpoint' branch_labels = (cli.EXPAND_BRANCH,) def upgrade(): pass networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/contract/0000775000175100017510000000000013245511554032427 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000016300000000000011215 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/contract/1d271ead4eb6_initial.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/pike/contract/1d271ead40000666000175100017510000000167113245511145033651 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. """initial networking-ovn contract branch Revision ID: 1d271ead4eb6 Create Date: 2017-04-27 17:10:02.788089 """ from neutron.db.migration import cli # revision identifiers, used by Alembic. revision = '1d271ead4eb6' down_revision = 'initial_branchpoint' branch_labels = (cli.CONTRACT_BRANCH,) def upgrade(): pass networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/EXPAND_HEAD0000666000175100017510000000001513245511145031337 0ustar zuulzuul000000000000005c198d2723b6 networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/0000775000175100017510000000000013245511554031162 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/0000775000175100017510000000000013245511554032441 5ustar zuulzuul00000000000000././@LongLink0000000000000000000000000000021200000000000011210 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/bc9e24bb9da2_drop_journaling_related_tables.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/bc9e24bb90000666000175100017510000000166213245511145033754 0ustar zuulzuul00000000000000# Copyright 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. # """Drop journaling related tables Revision ID: bc9e24bb9da2 Revises: e229b8aad9f2 Create Date: 2017-08-10 11:00:25.428857 """ # revision identifiers, used by Alembic. revision = 'bc9e24bb9da2' down_revision = 'e229b8aad9f2' from alembic import op def upgrade(): op.drop_table('ovn_journal') op.drop_table('ovn_maintenance') ././@LongLink0000000000000000000000000000022000000000000011207 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/5c198d2723b6_add_ovn_revision_resource_type_as_pk.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/5c198d2720000666000175100017510000000323113245511145033531 0ustar zuulzuul00000000000000# Copyright 2018 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. # """add_ovn_revision_resource_type_as_pk Revision ID: 5c198d2723b6 Revises: f48286668608 Create Date: 2018-01-17 16:10:20.232123 """ # revision identifiers, used by Alembic. revision = '5c198d2723b6' down_revision = 'f48286668608' from alembic import op from sqlalchemy.engine.reflection import Inspector as insp MYSQL_ENGINE = 'mysql' OVN_REVISION_NUMBER = 'ovn_revision_numbers' def upgrade(): bind = op.get_bind() engine = bind.engine if (engine.name == MYSQL_ENGINE): op.execute("ALTER TABLE ovn_revision_numbers DROP PRIMARY KEY," "ADD PRIMARY KEY (resource_uuid, resource_type);") else: inspector = insp.from_engine(bind) pk_constraint = inspector.get_pk_constraint(OVN_REVISION_NUMBER) op.drop_constraint(pk_constraint.get('name'), OVN_REVISION_NUMBER, type_='primary') op.create_primary_key(op.f('pk_ovn_revision_numbers'), OVN_REVISION_NUMBER, ['resource_uuid', 'resource_type']) ././@LongLink0000000000000000000000000000021200000000000011210 Lustar 00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/f48286668608_add_ovn_revision_numbers_table.pynetworking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/versions/queens/expand/f482866680000666000175100017510000000313313245511145033467 0ustar zuulzuul00000000000000# Copyright 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. # """add_ovn_revision_numbers_table Revision ID: f48286668608 Revises: 9a50bdf0c677 Create Date: 2017-08-18 09:59:20.021013 """ # revision identifiers, used by Alembic. revision = 'f48286668608' down_revision = 'bc9e24bb9da2' from alembic import op import sqlalchemy as sa def upgrade(): op.create_table( 'ovn_revision_numbers', sa.Column('standard_attr_id', sa.BigInteger, nullable=True), sa.Column('resource_uuid', sa.String(36), nullable=False, primary_key=True), sa.Column('resource_type', sa.String(36), nullable=False), sa.Column('revision_number', sa.BigInteger, nullable=False, default=0), sa.Column('created_at', sa.DateTime, nullable=False, default=sa.func.now()), sa.Column('updated_at', sa.TIMESTAMP, server_default=sa.func.now(), onupdate=sa.func.now()), sa.ForeignKeyConstraint( ['standard_attr_id'], ['standardattributes.id'], ondelete='SET NULL') ) networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/env.py0000666000175100017510000000543513245511145027161 0ustar zuulzuul00000000000000# Copyright 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. # from logging import config as logging_config from alembic import context from neutron_lib.db import model_base from oslo_config import cfg from oslo_db.sqlalchemy import session import sqlalchemy as sa from sqlalchemy import event from neutron.db.migration.alembic_migrations import external from neutron.db.migration.models import head # noqa MYSQL_ENGINE = None OVN_VERSION_TABLE = 'ovn_alembic_version' config = context.config neutron_config = config.neutron_config logging_config.fileConfig(config.config_file_name) target_metadata = model_base.BASEV2.metadata def set_mysql_engine(): try: mysql_engine = neutron_config.command.mysql_engine except cfg.NoSuchOptError: mysql_engine = None global MYSQL_ENGINE MYSQL_ENGINE = (mysql_engine or model_base.BASEV2.__table_args__['mysql_engine']) def include_object(object, name, type_, reflected, compare_to): if type_ == 'table' and name in external.TABLES: return False else: return True def run_migrations_offline(): set_mysql_engine() kwargs = dict() if neutron_config.database.connection: kwargs['url'] = neutron_config.database.connection else: kwargs['dialect_name'] = neutron_config.database.engine kwargs['include_object'] = include_object kwargs['version_table'] = OVN_VERSION_TABLE context.configure(**kwargs) with context.begin_transaction(): context.run_migrations() @event.listens_for(sa.Table, 'after_parent_attach') def set_storage_engine(target, parent): if MYSQL_ENGINE: target.kwargs['mysql_engine'] = MYSQL_ENGINE def run_migrations_online(): set_mysql_engine() engine = session.create_engine(neutron_config.database.connection) connection = engine.connect() context.configure( connection=connection, target_metadata=target_metadata, include_object=include_object, version_table=OVN_VERSION_TABLE ) try: with context.begin_transaction(): context.run_migrations() finally: connection.close() engine.dispose() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online() networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/README0000666000175100017510000000011613245511145026666 0ustar zuulzuul00000000000000This directory contains the migration scripts for the networking_ovn project. networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/__init__.py0000666000175100017510000000000013245511145030107 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/db/migration/alembic_migrations/script.py.mako0000666000175100017510000000201613245511145030613 0ustar zuulzuul00000000000000# Copyright ${create_date.year} 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. # """${message} Revision ID: ${up_revision} Revises: ${down_revision} Create Date: ${create_date} """ # revision identifiers, used by Alembic. revision = ${repr(up_revision)} down_revision = ${repr(down_revision)} % if branch_labels: branch_labels = ${repr(branch_labels)} %endif from alembic import op import sqlalchemy as sa ${imports if imports else ""} def upgrade(): ${upgrades if upgrades else "pass"} networking-ovn-4.0.0/networking_ovn/db/migration/__init__.py0000666000175100017510000000000013245511145024257 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/version.py0000666000175100017510000000125513245511145021644 0ustar zuulzuul00000000000000# Copyright 2015 VMware, 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 pbr.version version_info = pbr.version.VersionInfo('networking-ovn') networking-ovn-4.0.0/networking_ovn/common/0000775000175100017510000000000013245511554021074 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/common/config.py0000666000175100017510000002264513245511145022722 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 neutron_lib.api.definitions import portbindings from oslo_config import cfg from oslo_log import log as logging from ovsdbapp.backend.ovs_idl import vlog from networking_ovn._i18n import _ from networking_ovn import version LOG = logging.getLogger(__name__) EXTRA_LOG_LEVEL_DEFAULTS = [ ] VLOG_LEVELS = {'CRITICAL': vlog.CRITICAL, 'ERROR': vlog.ERROR, 'WARNING': vlog.WARN, 'INFO': vlog.INFO, 'DEBUG': vlog.DEBUG} ovn_opts = [ cfg.StrOpt('ovn_nb_connection', default='tcp:127.0.0.1:6641', help=_('The connection string for the OVN_Northbound OVSDB.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use ssl:IP:PORT for SSL connection. The ' 'ovn_nb_private_key, ovn_nb_certificate and ' 'ovn_nb_ca_cert are mandatory.\n' 'Use unix:FILE for unix domain socket connection.')), cfg.StrOpt('ovn_nb_private_key', default='', help=_('The PEM file with private key for SSL connection to ' 'OVN-NB-DB')), cfg.StrOpt('ovn_nb_certificate', default='', help=_('The PEM file with certificate that certifies the ' 'private key specified in ovn_nb_private_key')), cfg.StrOpt('ovn_nb_ca_cert', default='', help=_('The PEM file with CA certificate that OVN should use to' ' verify certificates presented to it by SSL peers')), cfg.StrOpt('ovn_sb_connection', default='tcp:127.0.0.1:6642', help=_('The connection string for the OVN_Southbound OVSDB.\n' 'Use tcp:IP:PORT for TCP connection.\n' 'Use ssl:IP:PORT for SSL connection. The ' 'ovn_sb_private_key, ovn_sb_certificate and ' 'ovn_sb_ca_cert are mandatory.\n' 'Use unix:FILE for unix domain socket connection.')), cfg.StrOpt('ovn_sb_private_key', default='', help=_('The PEM file with private key for SSL connection to ' 'OVN-SB-DB')), cfg.StrOpt('ovn_sb_certificate', default='', help=_('The PEM file with certificate that certifies the ' 'private key specified in ovn_sb_private_key')), cfg.StrOpt('ovn_sb_ca_cert', default='', help=_('The PEM file with CA certificate that OVN should use to' ' verify certificates presented to it by SSL peers')), cfg.IntOpt('ovsdb_connection_timeout', default=180, help=_('Timeout in seconds for the OVSDB ' 'connection transaction')), cfg.IntOpt('ovsdb_probe_interval', min=0, default=0, help=_('The probe interval in for the OVSDB session in ' 'milliseconds. If this is zero, it disables the ' 'connection keepalive feature. If non-zero the value ' 'will be forced to at least 1000 milliseconds. Probing ' 'is disabled by default.')), cfg.StrOpt('neutron_sync_mode', default='log', choices=('off', 'log', 'repair'), help=_('The synchronization mode of OVN_Northbound OVSDB ' 'with Neutron DB.\n' 'off - synchronization is off \n' 'log - during neutron-server startup, ' 'check to see if OVN is in sync with ' 'the Neutron database. ' ' Log warnings for any inconsistencies found so' ' that an admin can investigate \n' 'repair - during neutron-server startup, automatically' ' create resources found in Neutron but not in OVN.' ' Also remove resources from OVN' ' that are no longer in Neutron.')), cfg.BoolOpt('ovn_l3_mode', default=True, deprecated_for_removal=True, deprecated_reason="This option is no longer used. Native L3 " "support in OVN is always used.", help=_('Whether to use OVN native L3 support. Do not change ' 'the value for existing deployments that contain ' 'routers.')), cfg.StrOpt("ovn_l3_scheduler", default='leastloaded', choices=('leastloaded', 'chance'), help=_('The OVN L3 Scheduler type used to schedule router ' 'gateway ports on hypervisors/chassis. \n' 'leastloaded - chassis with fewest gateway ports ' 'selected \n' 'chance - chassis randomly selected')), cfg.BoolOpt('enable_distributed_floating_ip', default=False, help=_('Enable distributed floating IP support.\n' 'If True, the NAT action for floating IPs will be done ' 'locally and not in the centralized gateway. This ' 'saves the path to the external network. This requires ' 'the user to configure the physical network map ' '(i.e. ovn-bridge-mappings) on each compute node.')), cfg.StrOpt("vif_type", deprecated_for_removal=True, deprecated_reason="The port VIF type is now determined based " "on the OVN chassis information when the " "port is bound to a host.", default=portbindings.VIF_TYPE_OVS, help=_("Type of VIF to be used for ports valid values are " "(%(ovs)s, %(dpdk)s) default %(ovs)s") % { "ovs": portbindings.VIF_TYPE_OVS, "dpdk": portbindings.VIF_TYPE_VHOST_USER}, choices=[portbindings.VIF_TYPE_OVS, portbindings.VIF_TYPE_VHOST_USER]), cfg.StrOpt("vhost_sock_dir", default="/var/run/openvswitch", help=_("The directory in which vhost virtio socket " "is created by all the vswitch daemons")), cfg.IntOpt('dhcp_default_lease_time', default=(12 * 60 * 60), help=_('Default least time (in seconds) to use with ' 'OVN\'s native DHCP service.')), cfg.StrOpt("ovsdb_log_level", default="INFO", choices=list(VLOG_LEVELS.keys()), help=_("The log level used for OVSDB")), cfg.BoolOpt('ovn_metadata_enabled', default=False, help=_('Whether to use metadata service.')) ] cfg.CONF.register_opts(ovn_opts, group='ovn') def list_opts(): return [ ('ovn', ovn_opts), ] def get_ovn_nb_connection(): return cfg.CONF.ovn.ovn_nb_connection def get_ovn_nb_private_key(): return cfg.CONF.ovn.ovn_nb_private_key def get_ovn_nb_certificate(): return cfg.CONF.ovn.ovn_nb_certificate def get_ovn_nb_ca_cert(): return cfg.CONF.ovn.ovn_nb_ca_cert def get_ovn_sb_connection(): return cfg.CONF.ovn.ovn_sb_connection def get_ovn_sb_private_key(): return cfg.CONF.ovn.ovn_sb_private_key def get_ovn_sb_certificate(): return cfg.CONF.ovn.ovn_sb_certificate def get_ovn_sb_ca_cert(): return cfg.CONF.ovn.ovn_sb_ca_cert def get_ovn_ovsdb_timeout(): return cfg.CONF.ovn.ovsdb_connection_timeout def get_ovn_ovsdb_probe_interval(): return cfg.CONF.ovn.ovsdb_probe_interval def get_ovn_neutron_sync_mode(): return cfg.CONF.ovn.neutron_sync_mode def is_ovn_l3(): return cfg.CONF.ovn.ovn_l3_mode def get_ovn_l3_scheduler(): return cfg.CONF.ovn.ovn_l3_scheduler def is_ovn_distributed_floating_ip(): return cfg.CONF.ovn.enable_distributed_floating_ip def get_ovn_vhost_sock_dir(): return cfg.CONF.ovn.vhost_sock_dir def get_ovn_dhcp_default_lease_time(): return cfg.CONF.ovn.dhcp_default_lease_time def get_ovn_ovsdb_log_level(): return VLOG_LEVELS[cfg.CONF.ovn.ovsdb_log_level] def is_ovn_metadata_enabled(): return cfg.CONF.ovn.ovn_metadata_enabled def setup_logging(): """Sets up the logging options for a log with supplied name.""" product_name = "networking-ovn" # We use the oslo.log default log levels and add only the extra levels # that we need. logging.set_defaults(default_log_levels=logging.get_default_log_levels() + EXTRA_LOG_LEVEL_DEFAULTS) logging.setup(cfg.CONF, product_name) LOG.info("Logging enabled!") LOG.info("%(prog)s version %(version)s", {'prog': sys.argv[0], 'version': version.version_info.release_string()}) LOG.debug("command line: %s", " ".join(sys.argv)) networking-ovn-4.0.0/networking_ovn/common/extensions.py0000666000175100017510000000334313245511145023646 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # NOTE(russellb) This remains in its own file (vs constants.py) because we want # to be able to easily import it and export the info without any dependencies # on external imports. # NOTE(russellb) If you update these lists, please also update # doc/source/features.rst and the current release note. ML2_SUPPORTED_API_EXTENSIONS_NEUTRON_L3 = [ 'dns-integration', 'dvr', 'extraroute', 'ext-gw-mode', 'l3-ha', 'l3_agent_scheduler', 'router', 'router_availability_zone', ] ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 = [ 'router', 'extraroute', 'ext-gw-mode', 'pagination', 'sorting', 'project-id', ] ML2_SUPPORTED_API_EXTENSIONS = [ 'address-scope', 'allowed-address-pairs', 'auto-allocated-topology', 'availability_zone', 'binding', 'default-subnetpools', 'external-net', 'extra_dhcp_opt', 'multi-provider', 'net-mtu', 'network_availability_zone', 'network-ip-availability', 'port-security', 'provider', 'quotas', 'rbac-policies', 'revisions', 'security-group', 'standard-attr-description', 'subnet_allocation', 'tag', 'timestamp_core', 'trunk', ] networking-ovn-4.0.0/networking_ovn/common/exceptions.py0000666000175100017510000000224413245511145023627 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron_lib import exceptions as n_exc from networking_ovn._i18n import _ class RevisionConflict(n_exc.NeutronException): message = _('OVN revision number for %(resource_id)s (type: ' '%(resource_type)s) is equal or higher than the given ' 'resource. Skipping update') class UnknownResourceType(n_exc.NeutronException): message = _('Uknown resource type: %(resource_type)s') class StandardAttributeIDNotFound(n_exc.NeutronException): message = _('Standard attribute ID not found for %(resource_uuid)s') networking-ovn-4.0.0/networking_ovn/common/ovn_client.py0000666000175100017510000021752713245511145023622 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. import collections import copy import netaddr from neutron.plugins.common import utils as p_utils from neutron_lib.api.definitions import l3 from neutron_lib.api.definitions import port_security as psec from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet from neutron_lib import constants as const from neutron_lib import context as n_context from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.utils import helpers from neutron_lib.utils import net as n_net from oslo_config import cfg from oslo_log import log from oslo_utils import excutils from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn.agent.metadata import agent as metadata_agent from networking_ovn.common import acl as ovn_acl from networking_ovn.common import config from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils from networking_ovn.db import revision as db_rev from networking_ovn.l3 import l3_ovn_scheduler from networking_ovn.ml2 import qos_driver LOG = log.getLogger(__name__) OvnPortInfo = collections.namedtuple( 'OvnPortInfo', ['type', 'options', 'addresses', 'port_security', 'parent_name', 'tag', 'dhcpv4_options', 'dhcpv6_options', 'cidrs', 'device_owner', 'security_group_ids']) GW_INFO = collections.namedtuple('GatewayInfo', ['network_id', 'subnet_id', 'router_ip', 'gateway_ip']) class OVNClient(object): def __init__(self, nb_idl, sb_idl): self._nb_idl = nb_idl self._sb_idl = sb_idl self._plugin_property = None self._l3_plugin_property = None self._qos_driver = qos_driver.OVNQosDriver(self) self._ovn_scheduler = l3_ovn_scheduler.get_scheduler() @property def _plugin(self): if self._plugin_property is None: self._plugin_property = directory.get_plugin() return self._plugin_property @property def _l3_plugin(self): if self._l3_plugin_property is None: self._l3_plugin_property = directory.get_plugin( plugin_constants.L3) return self._l3_plugin_property def _transaction(self, commands, txn=None): """Create a new transaction or add the commands to an existing one.""" if txn is None: with self._nb_idl.transaction(check_error=True) as txn: for cmd in commands: txn.add(cmd) else: for cmd in commands: txn.add(cmd) def _get_allowed_addresses_from_port(self, port): if not port.get(psec.PORTSECURITY): return [], [] if utils.is_lsp_trusted(port): return [], [] allowed_addresses = set() new_macs = set() addresses = port['mac_address'] for ip in port.get('fixed_ips', []): addresses += ' ' + ip['ip_address'] for allowed_address in port.get('allowed_address_pairs', []): # If allowed address pair has same mac as the port mac, # append the allowed ip address to the 'addresses'. # Else we will have multiple entries for the same mac in # 'Logical_Switch_Port.port_security'. if allowed_address['mac_address'] == port['mac_address']: addresses += ' ' + allowed_address['ip_address'] else: allowed_addresses.add(allowed_address['mac_address'] + ' ' + allowed_address['ip_address']) new_macs.add(allowed_address['mac_address']) allowed_addresses.add(addresses) return list(allowed_addresses), list(new_macs) def _get_subnet_dhcp_options_for_port(self, port, ip_version): """Returns the subnet dhcp options for the port. Return the first found DHCP options belong for the port. """ subnets = [ fixed_ip['subnet_id'] for fixed_ip in port['fixed_ips'] if netaddr.IPAddress(fixed_ip['ip_address']).version == ip_version] get_opts = self._nb_idl.get_subnets_dhcp_options(subnets) if get_opts: if ip_version == const.IP_VERSION_6: # Always try to find a dhcpv6 stateful v6 subnet to return. # This ensures port can get one stateful v6 address when port # has multiple dhcpv6 stateful and stateless subnets. for opts in get_opts: # We are setting ovn_const.DHCPV6_STATELESS_OPT to "true" # in _get_ovn_dhcpv6_opts, so entries in DHCP_Options table # should have unicode type 'true' if they were defined as # dhcpv6 stateless. if opts['options'].get( ovn_const.DHCPV6_STATELESS_OPT) != 'true': return opts return get_opts[0] def _get_port_dhcp_options(self, port, ip_version): """Return dhcp options for port. In case the port is dhcp disabled, or IP addresses it has belong to dhcp disabled subnets, returns None. Otherwise, returns a dict: - with content from a existing DHCP_Options row for subnet, if the port has no extra dhcp options. - with only one item ('cmd', AddDHCPOptionsCommand(..)), if the port has extra dhcp options. The command should be processed in the same transaction with port creating or updating command to avoid orphan row issue happen. """ lsp_dhcp_disabled, lsp_dhcp_opts = utils.get_lsp_dhcp_opts( port, ip_version) if lsp_dhcp_disabled: return subnet_dhcp_options = self._get_subnet_dhcp_options_for_port( port, ip_version) if not subnet_dhcp_options: # NOTE(lizk): It's possible for Neutron to configure a port with IP # address belongs to subnet disabled dhcp. And no DHCP_Options row # will be inserted for such a subnet. So in that case, the subnet # dhcp options here will be None. return if not lsp_dhcp_opts: return subnet_dhcp_options # This port has extra DHCP options defined, so we will create a new # row in DHCP_Options table for it. subnet_dhcp_options['options'].update(lsp_dhcp_opts) subnet_dhcp_options['external_ids'].update( {'port_id': port['id']}) subnet_id = subnet_dhcp_options['external_ids']['subnet_id'] add_dhcp_opts_cmd = self._nb_idl.add_dhcp_options( subnet_id, port_id=port['id'], cidr=subnet_dhcp_options['cidr'], options=subnet_dhcp_options['options'], external_ids=subnet_dhcp_options['external_ids']) return {'cmd': add_dhcp_opts_cmd} def _get_port_options(self, port, qos_options=None): binding_prof = utils.validate_and_get_data_from_binding_profile(port) if qos_options is None: qos_options = self._qos_driver.get_qos_options(port) vtep_physical_switch = binding_prof.get('vtep-physical-switch') cidrs = '' if vtep_physical_switch: vtep_logical_switch = binding_prof.get('vtep-logical-switch') port_type = 'vtep' options = {'vtep-physical-switch': vtep_physical_switch, 'vtep-logical-switch': vtep_logical_switch} addresses = ["unknown"] parent_name = [] tag = [] port_security = [] else: options = qos_options parent_name = binding_prof.get('parent_name', []) tag = binding_prof.get('tag', []) address = port['mac_address'] for ip in port.get('fixed_ips', []): address += ' ' + ip['ip_address'] subnet = self._plugin.get_subnet(n_context.get_admin_context(), ip['subnet_id']) cidrs += ' {}/{}'.format(ip['ip_address'], subnet['cidr'].split('/')[1]) port_security, new_macs = \ self._get_allowed_addresses_from_port(port) addresses = [address] addresses.extend(new_macs) port_type = ovn_const.OVN_NEUTRON_OWNER_TO_PORT_TYPE.get( port['device_owner'], '') dhcpv4_options = self._get_port_dhcp_options(port, const.IP_VERSION_4) dhcpv6_options = self._get_port_dhcp_options(port, const.IP_VERSION_6) options.update({'requested-chassis': port.get(portbindings.HOST_ID, '')}) device_owner = port.get('device_owner', '') sg_ids = ' '.join(utils.get_lsp_security_groups(port)) return OvnPortInfo(port_type, options, addresses, port_security, parent_name, tag, dhcpv4_options, dhcpv6_options, cidrs.strip(), device_owner, sg_ids) def create_port(self, port): if utils.is_lsp_ignored(port): return port_info = self._get_port_options(port) external_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port['name'], ovn_const.OVN_DEVID_EXT_ID_KEY: port['device_id'], ovn_const.OVN_PROJID_EXT_ID_KEY: port['project_id'], ovn_const.OVN_CIDRS_EXT_ID_KEY: port_info.cidrs, ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: port_info.device_owner, ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: utils.ovn_name(port['network_id']), ovn_const.OVN_SG_IDS_EXT_ID_KEY: port_info.security_group_ids, ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( utils.get_revision_number( port, ovn_const.TYPE_PORTS))} lswitch_name = utils.ovn_name(port['network_id']) admin_context = n_context.get_admin_context() sg_cache = {} subnet_cache = {} # It's possible to have a network created on one controller and then a # port created on a different controller quickly enough that the second # controller does not yet see that network in its local cache of the # OVN northbound database. Check if the logical switch is present # or not in the idl's local copy of the database before creating # the lswitch port. self._nb_idl.check_for_row_by_value_and_retry( 'Logical_Switch', 'name', lswitch_name) with self._nb_idl.transaction(check_error=True) as txn: if not port_info.dhcpv4_options: dhcpv4_options = [] elif 'cmd' in port_info.dhcpv4_options: dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd']) else: dhcpv4_options = [port_info.dhcpv4_options['uuid']] if not port_info.dhcpv6_options: dhcpv6_options = [] elif 'cmd' in port_info.dhcpv6_options: dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd']) else: dhcpv6_options = [port_info.dhcpv6_options['uuid']] # The lport_name *must* be neutron port['id']. It must match the # iface-id set in the Interfaces table of the Open_vSwitch # database which nova sets to be the port ID. txn.add(self._nb_idl.create_lswitch_port( lport_name=port['id'], lswitch_name=lswitch_name, addresses=port_info.addresses, external_ids=external_ids, parent_name=port_info.parent_name, tag=port_info.tag, enabled=port.get('admin_state_up'), options=port_info.options, type=port_info.type, port_security=port_info.port_security, dhcpv4_options=dhcpv4_options, dhcpv6_options=dhcpv6_options)) acls_new = ovn_acl.add_acls(self._plugin, admin_context, port, sg_cache, subnet_cache, self._nb_idl) for acl in acls_new: txn.add(self._nb_idl.add_acl(**acl)) sg_ids = utils.get_lsp_security_groups(port) if port.get('fixed_ips') and sg_ids: addresses = ovn_acl.acl_port_ips(port) # NOTE(rtheis): Fail port creation if the address set doesn't # exist. This prevents ports from being created on any security # groups out-of-sync between neutron and OVN. for sg_id in sg_ids: for ip_version in addresses: if addresses[ip_version]: txn.add(self._nb_idl.update_address_set( name=utils.ovn_addrset_name(sg_id, ip_version), addrs_add=addresses[ip_version], addrs_remove=None, if_exists=False)) if self.is_dns_required_for_port(port): self.add_txns_to_sync_port_dns_records(txn, port) db_rev.bump_revision(port, ovn_const.TYPE_PORTS) # TODO(lucasagomes): Remove this helper method in the Rocky release def _get_lsp_backward_compat_sgs(self, ovn_port, port_object=None, skip_trusted_port=True): if ovn_const.OVN_SG_IDS_EXT_ID_KEY in ovn_port.external_ids: return utils.get_ovn_port_security_groups( ovn_port, skip_trusted_port=skip_trusted_port) elif port_object is not None: return utils.get_lsp_security_groups( port_object, skip_trusted_port=skip_trusted_port) return [] # TODO(lucasagomes): The ``port_object`` parameter was added to # keep things backward compatible. Remove it in the Rocky release. def update_port(self, port, qos_options=None, port_object=None): if utils.is_lsp_ignored(port): return port_info = self._get_port_options(port, qos_options) external_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port['name'], ovn_const.OVN_DEVID_EXT_ID_KEY: port['device_id'], ovn_const.OVN_PROJID_EXT_ID_KEY: port['project_id'], ovn_const.OVN_CIDRS_EXT_ID_KEY: port_info.cidrs, ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: port_info.device_owner, ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: utils.ovn_name(port['network_id']), ovn_const.OVN_SG_IDS_EXT_ID_KEY: port_info.security_group_ids, ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( utils.get_revision_number( port, ovn_const.TYPE_PORTS))} admin_context = n_context.get_admin_context() sg_cache = {} subnet_cache = {} check_rev_cmd = self._nb_idl.check_revision_number( port['id'], port, ovn_const.TYPE_PORTS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) columns_dict = {} if utils.is_lsp_router_port(port): port_info.options.update( self._nb_idl.get_router_port_options(port['id'])) else: columns_dict['type'] = port_info.type columns_dict['addresses'] = port_info.addresses if not port_info.dhcpv4_options: dhcpv4_options = [] elif 'cmd' in port_info.dhcpv4_options: dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd']) else: dhcpv4_options = [port_info.dhcpv4_options['uuid']] if not port_info.dhcpv6_options: dhcpv6_options = [] elif 'cmd' in port_info.dhcpv6_options: dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd']) else: dhcpv6_options = [port_info.dhcpv6_options['uuid']] # NOTE(lizk): Fail port updating if port doesn't exist. This # prevents any new inserted resources to be orphan, such as port # dhcp options or ACL rules for port, e.g. a port was created # without extra dhcp options and security group, while updating # includes the new attributes setting to port. txn.add(self._nb_idl.set_lswitch_port( lport_name=port['id'], external_ids=external_ids, parent_name=port_info.parent_name, tag=port_info.tag, options=port_info.options, enabled=port['admin_state_up'], port_security=port_info.port_security, dhcpv4_options=dhcpv4_options, dhcpv6_options=dhcpv6_options, if_exists=False, **columns_dict)) ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port['id']) # Determine if security groups or fixed IPs are updated. old_sg_ids = set(self._get_lsp_backward_compat_sgs( ovn_port, port_object=port_object)) new_sg_ids = set(utils.get_lsp_security_groups(port)) detached_sg_ids = old_sg_ids - new_sg_ids attached_sg_ids = new_sg_ids - old_sg_ids old_fixed_ips = utils.remove_macs_from_lsp_addresses( ovn_port.addresses) new_fixed_ips = [x['ip_address'] for x in port.get('fixed_ips', [])] old_allowed_address_pairs = ( utils.get_allowed_address_pairs_ip_addresses_from_ovn_port( ovn_port)) new_allowed_address_pairs = ( utils.get_allowed_address_pairs_ip_addresses(port)) is_fixed_ips_updated = ( sorted(old_fixed_ips) != sorted(new_fixed_ips)) is_allowed_ips_updated = (sorted(old_allowed_address_pairs) != sorted(new_allowed_address_pairs)) # Refresh ACLs for changed security groups or fixed IPs. if detached_sg_ids or attached_sg_ids or is_fixed_ips_updated: # Note that update_acls will compare the port's ACLs to # ensure only the necessary ACLs are added and deleted # on the transaction. acls_new = ovn_acl.add_acls(self._plugin, admin_context, port, sg_cache, subnet_cache, self._nb_idl) txn.add(self._nb_idl.update_acls([port['network_id']], [port], {port['id']: acls_new}, need_compare=True)) # Refresh address sets for changed security groups or fixed IPs. if len(old_fixed_ips) != 0 or len(new_fixed_ips) != 0: addresses = ovn_acl.acl_port_ips(port) addresses_old = utils.sort_ips_by_version( utils.get_ovn_port_addresses(ovn_port)) # Add current addresses to attached security groups. for sg_id in attached_sg_ids: for ip_version in addresses: if addresses[ip_version]: txn.add(self._nb_idl.update_address_set( name=utils.ovn_addrset_name(sg_id, ip_version), addrs_add=addresses[ip_version], addrs_remove=None)) # Remove old addresses from detached security groups. for sg_id in detached_sg_ids: for ip_version in addresses_old: if addresses_old[ip_version]: txn.add(self._nb_idl.update_address_set( name=utils.ovn_addrset_name(sg_id, ip_version), addrs_add=None, addrs_remove=addresses_old[ip_version])) if is_fixed_ips_updated or is_allowed_ips_updated: # We have refreshed address sets for attached and detached # security groups, so now we only need to take care of # unchanged security groups. unchanged_sg_ids = new_sg_ids & old_sg_ids for sg_id in unchanged_sg_ids: for ip_version in addresses: addr_add = (set(addresses[ip_version]) - set(addresses_old[ip_version])) or None addr_remove = (set(addresses_old[ip_version]) - set(addresses[ip_version])) or None if addr_add or addr_remove: txn.add(self._nb_idl.update_address_set( name=utils.ovn_addrset_name( sg_id, ip_version), addrs_add=addr_add, addrs_remove=addr_remove)) if self.is_dns_required_for_port(port): self.add_txns_to_sync_port_dns_records( txn, port, original_port=port_object) elif port_object and self.is_dns_required_for_port(port_object): # We need to remove the old entries self.add_txns_to_remove_port_dns_records(txn, port_object) if check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(port, ovn_const.TYPE_PORTS) def _delete_port(self, port_id, port_object=None): ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port_id) network_id = ovn_port.external_ids.get( ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY) # TODO(lucasagomes): For backward compatibility, if network_id # is not in the OVNDB, look at the port_object if not network_id and port_object: network_id = port_object['network_id'] with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.delete_lswitch_port( port_id, network_id)) txn.add(self._nb_idl.delete_acl(network_id, port_id)) addresses = utils.sort_ips_by_version( utils.get_ovn_port_addresses(ovn_port)) sec_groups = self._get_lsp_backward_compat_sgs( ovn_port, port_object=port_object, skip_trusted_port=False) for sg_id in sec_groups: for ip_version, addr_list in addresses.items(): if not addr_list: continue txn.add(self._nb_idl.update_address_set( name=utils.ovn_addrset_name(sg_id, ip_version), addrs_add=None, addrs_remove=addr_list)) if port_object and self.is_dns_required_for_port(port_object): self.add_txns_to_remove_port_dns_records(txn, port_object) # TODO(lucasagomes): The ``port_object`` parameter was added to # keep things backward compatible. Remove it in the Rocky release. def delete_port(self, port_id, port_object=None): try: self._delete_port(port_id, port_object=port_object) except idlutils.RowNotFound: pass db_rev.delete_revision(port_id, ovn_const.TYPE_PORTS) def _create_or_update_floatingip(self, floatingip, txn=None): router_id = floatingip.get('router_id') if not router_id: return commands = [] context = n_context.get_admin_context() fip_db = self._l3_plugin._get_floatingip(context, floatingip['id']) func = self._nb_idl.add_nat_rule_in_lrouter gw_lrouter_name = utils.ovn_name(router_id) nat_rule_args = (gw_lrouter_name,) # TODO(chandrav): Since the floating ip port is not # bound to any chassis, packets destined to floating ip # will be dropped. To overcome this, delete the floating # ip port. Proper fix for this would be to redirect packets # destined to floating ip to the router port. This would # require changes in ovn-northd. commands.append(self._nb_idl.delete_lswitch_port( fip_db['floating_port_id'], utils.ovn_name(floatingip['floating_network_id']))) # Get the list of nat rules and check if the external_ip # with type 'dnat_and_snat' already exists or not. # If exists, set the new value. # This happens when the port associated to a floating ip # is deleted before the disassociation. lrouter_nat_rules = self._nb_idl.get_lrouter_nat_rules( gw_lrouter_name) for nat_rule in lrouter_nat_rules: if (nat_rule['external_ip'] == floatingip['floating_ip_address'] and nat_rule['type'] == 'dnat_and_snat'): func = self._nb_idl.set_nat_rule_in_lrouter nat_rule_args = (gw_lrouter_name, nat_rule['uuid']) break ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: floatingip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number( floatingip, ovn_const.TYPE_FLOATINGIPS)), ovn_const.OVN_FIP_PORT_EXT_ID_KEY: floatingip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: gw_lrouter_name} columns = {'type': 'dnat_and_snat', 'logical_ip': floatingip['fixed_ip_address'], 'external_ip': floatingip['floating_ip_address']} # TODO(dalvarez): remove this check once the minimum OVS required # version contains the column (when OVS 2.8.2 is released). if self._nb_idl.is_col_present('NAT', 'external_ids'): columns['external_ids'] = ext_ids if config.is_ovn_distributed_floating_ip(): port = self._plugin.get_port( context, fip_db['floating_port_id']) columns['external_mac'] = port['mac_address'] columns['logical_port'] = floatingip['port_id'] commands.append(func(*nat_rule_args, **columns)) self._transaction(commands, txn=txn) def _delete_floatingip(self, fip, lrouter, txn=None): commands = [self._nb_idl.delete_nat_rule_in_lrouter( lrouter, type='dnat_and_snat', logical_ip=fip['logical_ip'], external_ip=fip['external_ip'])] self._transaction(commands, txn=txn) def update_floatingip_status(self, floatingip): # NOTE(lucasagomes): OVN doesn't care about the floating ip # status, this method just bumps the revision number check_rev_cmd = self._nb_idl.check_revision_number( floatingip['id'], floatingip, ovn_const.TYPE_FLOATINGIPS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) if check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(floatingip, ovn_const.TYPE_FLOATINGIPS) def create_floatingip(self, floatingip): try: self._create_or_update_floatingip(floatingip) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Unable to create floating ip in gateway ' 'router. Error: %s', e) db_rev.bump_revision(floatingip, ovn_const.TYPE_FLOATINGIPS) # NOTE(lucasagomes): Revise the expected status # of floating ips, setting it to ACTIVE here doesn't # see consistent with other drivers (ODL here), see: # https://bugs.launchpad.net/networking-ovn/+bug/1657693 if floatingip.get('router_id'): self._l3_plugin.update_floatingip_status( n_context.get_admin_context(), floatingip['id'], const.FLOATINGIP_STATUS_ACTIVE) # TODO(lucasagomes): The ``fip_object`` parameter was added to # keep things backward compatible since old FIPs might not have # the OVN_FIP_EXT_ID_KEY in their external_ids field. Remove it # in the Rocky release. def update_floatingip(self, floatingip, fip_object=None): fip_status = None router_id = None ovn_fip = self._nb_idl.get_floatingip(floatingip['id']) if not ovn_fip and fip_object: router_id = fip_object.get('router_id') ovn_fip = self._nb_idl.get_floatingip_by_ips( router_id, fip_object['fixed_ip_address'], fip_object['floating_ip_address']) check_rev_cmd = self._nb_idl.check_revision_number( floatingip['id'], floatingip, ovn_const.TYPE_FLOATINGIPS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) if (ovn_fip and (floatingip['fixed_ip_address'] != ovn_fip['logical_ip'] or floatingip['port_id'] != ovn_fip['external_ids'].get( ovn_const.OVN_FIP_PORT_EXT_ID_KEY))): lrouter = ovn_fip['external_ids'].get( ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY, utils.ovn_name(router_id)) self._delete_floatingip(ovn_fip, lrouter, txn=txn) fip_status = const.FLOATINGIP_STATUS_DOWN if floatingip.get('port_id'): self._create_or_update_floatingip(floatingip, txn=txn) fip_status = const.FLOATINGIP_STATUS_ACTIVE if check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(floatingip, ovn_const.TYPE_FLOATINGIPS) if fip_status: self._l3_plugin.update_floatingip_status( n_context.get_admin_context(), floatingip['id'], fip_status) # TODO(lucasagomes): The ``fip_object`` parameter was added to # keep things backward compatible since old FIPs might not have # the OVN_FIP_EXT_ID_KEY in their external_ids field. Remove it # in the Rocky release. def delete_floatingip(self, fip_id, fip_object=None): router_id = None ovn_fip = self._nb_idl.get_floatingip(fip_id) if not ovn_fip and fip_object: router_id = fip_object.get('router_id') ovn_fip = self._nb_idl.get_floatingip_by_ips( router_id, fip_object['fixed_ip_address'], fip_object['floating_ip_address']) if ovn_fip: lrouter = ovn_fip['external_ids'].get( ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY, utils.ovn_name(router_id)) try: self._delete_floatingip(ovn_fip, lrouter) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Unable to delete floating ip in gateway ' 'router. Error: %s', e) db_rev.delete_revision(fip_id, ovn_const.TYPE_FLOATINGIPS) def disassociate_floatingip(self, floatingip, router_id): lrouter = utils.ovn_name(router_id) try: self._delete_floatingip(floatingip, lrouter) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Unable to disassociate floating ip in gateway ' 'router. Error: %s', e) def _get_gw_info(self, context, router): ext_gw_info = router.get(l3.EXTERNAL_GW_INFO, {}) network_id = ext_gw_info.get('network_id', '') for ext_fixed_ip in ext_gw_info.get('external_fixed_ips', []): subnet_id = ext_fixed_ip['subnet_id'] subnet = self._plugin.get_subnet(context, subnet_id) if subnet['ip_version'] == 4: return GW_INFO( network_id, subnet_id, ext_fixed_ip['ip_address'], subnet.get('gateway_ip')) return GW_INFO('', '', '', '') def _delete_router_ext_gw(self, context, router, networks, txn): if not networks: networks = [] router_id = router['id'] gw_port_id = router['gw_port_id'] gw_lrouter_name = utils.ovn_name(router_id) gw_info = self._get_gw_info(context, router) txn.add(self._nb_idl.delete_static_route(gw_lrouter_name, ip_prefix='0.0.0.0/0', nexthop=gw_info.gateway_ip)) txn.add(self._nb_idl.delete_lrouter_port( utils.ovn_lrouter_port_name(gw_port_id), gw_lrouter_name)) for network in networks: txn.add(self._nb_idl.delete_nat_rule_in_lrouter( gw_lrouter_name, type='snat', logical_ip=network, external_ip=gw_info.router_ip)) def _get_nets_and_ipv6_ra_confs_for_router_port( self, port_fixed_ips): context = n_context.get_admin_context() networks = set() ipv6_ra_configs = {} ipv6_ra_configs_supported = self._nb_idl.is_col_present( 'Logical_Router_Port', 'ipv6_ra_configs') for fixed_ip in port_fixed_ips: subnet_id = fixed_ip['subnet_id'] subnet = self._plugin.get_subnet(context, subnet_id) cidr = netaddr.IPNetwork(subnet['cidr']) networks.add("%s/%s" % (fixed_ip['ip_address'], str(cidr.prefixlen))) if subnet.get('ipv6_address_mode') and not ipv6_ra_configs and ( ipv6_ra_configs_supported): ipv6_ra_configs['address_mode'] = ( utils.get_ovn_ipv6_address_mode( subnet['ipv6_address_mode'])) ipv6_ra_configs['send_periodic'] = 'true' net = self._plugin.get_network(context, subnet['network_id']) ipv6_ra_configs['mtu'] = str(net['mtu']) return list(networks), ipv6_ra_configs def _add_router_ext_gw(self, context, router, networks, txn): router_id = router['id'] # 1. Add the external gateway router port. gw_info = self._get_gw_info(context, router) gw_port_id = router['gw_port_id'] port = self._plugin.get_port(context, gw_port_id) self.create_router_port(router_id, port, txn=txn) # TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged # (prior to that, the external_ids column didn't exist in this # table). columns = {} if self._nb_idl.is_col_present('Logical_Router_Static_Route', 'external_ids'): columns['external_ids'] = { ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: gw_info.subnet_id} # 2. Add default route with nexthop as gateway ip lrouter_name = utils.ovn_name(router_id) txn.add(self._nb_idl.add_static_route( lrouter_name, ip_prefix='0.0.0.0/0', nexthop=gw_info.gateway_ip, **columns)) # 3. Add snat rules for tenant networks in lrouter if snat is enabled if utils.is_snat_enabled(router) and networks: self.update_nat_rules(router, networks, enable_snat=True, txn=txn) return port def _check_external_ips_changed(self, context, ovn_snats, ovn_static_route, router): gw_info = self._get_gw_info(context, router) if self._nb_idl.is_col_present('Logical_Router_Static_Route', 'external_ids'): ext_ids = getattr(ovn_static_route, 'external_ids', {}) if (ext_ids.get(ovn_const.OVN_SUBNET_EXT_ID_KEY) != gw_info.subnet_id): return True for snat in ovn_snats: if snat.external_ip != gw_info.router_ip: return True return False def update_router_routes(self, context, router_id, add, remove, txn=None): if not any([add, remove]): return lrouter_name = utils.ovn_name(router_id) commands = [] for route in add: commands.append( self._nb_idl.add_static_route( lrouter_name, ip_prefix=route['destination'], nexthop=route['nexthop'])) for route in remove: commands.append( self._nb_idl.delete_static_route( lrouter_name, ip_prefix=route['destination'], nexthop=route['nexthop'])) self._transaction(commands, txn=txn) def _get_router_ports(self, context, router_id, get_gw_port=False): router_db = self._l3_plugin._get_router(context, router_id) if get_gw_port: return [p.port for p in router_db.attached_ports] else: # When the existing deployment is migrated to OVN # we may need to consider other port types - DVR_INTERFACE/HA_INTF. return [p.port for p in router_db.attached_ports if p.port_type in [const.DEVICE_OWNER_ROUTER_INTF, const.DEVICE_OWNER_DVR_INTERFACE, const.DEVICE_OWNER_HA_REPLICATED_INT, const.DEVICE_OWNER_ROUTER_HA_INTF]] def _get_v4_network_for_router_port(self, context, port): cidr = None for fixed_ip in port['fixed_ips']: subnet_id = fixed_ip['subnet_id'] subnet = self._plugin.get_subnet(context, subnet_id) if subnet['ip_version'] != 4: continue cidr = subnet['cidr'] return cidr def _get_v4_network_of_all_router_ports(self, context, router_id, ports=None): networks = [] ports = ports or self._get_router_ports(context, router_id) for port in ports: network = self._get_v4_network_for_router_port(context, port) if network: networks.append(network) return networks def _gen_router_ext_ids(self, router): return { ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: router.get('name', 'no_router_name'), ovn_const.OVN_GW_PORT_EXT_ID_KEY: router.get('gw_port_id') or '', ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number( router, ovn_const.TYPE_ROUTERS))} def create_router(self, router, add_external_gateway=True): """Create a logical router.""" context = n_context.get_admin_context() external_ids = self._gen_router_ext_ids(router) enabled = router.get('admin_state_up') lrouter_name = utils.ovn_name(router['id']) added_gw_port = None with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.create_lrouter(lrouter_name, external_ids=external_ids, enabled=enabled, options={})) # TODO(lucasagomes): add_external_gateway is being only used # by the ovn_db_sync.py script, remove it after the database # synchronization work if add_external_gateway: networks = self._get_v4_network_of_all_router_ports( context, router['id']) if router.get(l3.EXTERNAL_GW_INFO) and networks is not None: added_gw_port = self._add_router_ext_gw(context, router, networks, txn) if added_gw_port: db_rev.bump_revision(added_gw_port, ovn_const.TYPE_ROUTER_PORTS) db_rev.bump_revision(router, ovn_const.TYPE_ROUTERS) # TODO(lucasagomes): The ``router_object`` parameter was added to # keep things backward compatible with old routers created prior to # the database sync work. Remove it in the Rocky release. def update_router(self, new_router, router_object=None): """Update a logical router.""" context = n_context.get_admin_context() router_id = new_router['id'] router_name = utils.ovn_name(router_id) ovn_router = self._nb_idl.get_lrouter(router_name) gateway_new = new_router.get(l3.EXTERNAL_GW_INFO) gateway_old = utils.get_lrouter_ext_gw_static_route(ovn_router) added_gw_port = None deleted_gw_port_id = None if router_object: gateway_old = gateway_old or router_object.get(l3.EXTERNAL_GW_INFO) ovn_snats = utils.get_lrouter_snats(ovn_router) networks = self._get_v4_network_of_all_router_ports(context, router_id) try: check_rev_cmd = self._nb_idl.check_revision_number( router_name, new_router, ovn_const.TYPE_ROUTERS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) if gateway_new and not gateway_old: # Route gateway is set added_gw_port = self._add_router_ext_gw( context, new_router, networks, txn) elif gateway_old and not gateway_new: # router gateway is removed txn.add(self._nb_idl.delete_lrouter_ext_gw(router_name)) if router_object: self._delete_router_ext_gw(context, router_object, networks, txn) deleted_gw_port_id = router_object['gw_port_id'] elif gateway_new and gateway_old: # Check if external gateway has changed, if yes, delete # the old gateway and add the new gateway if self._check_external_ips_changed( context, ovn_snats, gateway_old, new_router): txn.add(self._nb_idl.delete_lrouter_ext_gw( router_name)) if router_object: self._delete_router_ext_gw(context, router_object, networks, txn) deleted_gw_port_id = router_object['gw_port_id'] added_gw_port = self._add_router_ext_gw( context, new_router, networks, txn) else: # Check if snat has been enabled/disabled and update new_snat_state = gateway_new.get('enable_snat', True) if bool(ovn_snats) != new_snat_state: if utils.is_snat_enabled(new_router) and networks: self.update_nat_rules( new_router, networks, enable_snat=new_snat_state, txn=txn) update = {'external_ids': self._gen_router_ext_ids(new_router)} update['enabled'] = new_router.get('admin_state_up') or False txn.add(self._nb_idl.update_lrouter(router_name, **update)) # Check for route updates routes = new_router.get('routes') if routes: old_routes = utils.get_lrouter_non_gw_routes(ovn_router) added, removed = helpers.diff_list_of_dict( old_routes, routes) self.update_router_routes( context, router_id, added, removed, txn=txn) if check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(new_router, ovn_const.TYPE_ROUTERS) if added_gw_port: db_rev.bump_revision(added_gw_port, ovn_const.TYPE_ROUTER_PORTS) if deleted_gw_port_id: db_rev.delete_revision(deleted_gw_port_id, ovn_const.TYPE_ROUTER_PORTS) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Unable to update router %(router)s. ' 'Error: %(error)s', {'router': router_id, 'error': e}) def delete_router(self, router_id): """Delete a logical router.""" lrouter_name = utils.ovn_name(router_id) with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.delete_lrouter(lrouter_name)) db_rev.delete_revision(router_id, ovn_const.TYPE_ROUTERS) def get_candidates_for_scheduling(self, extnet): if extnet.get(pnet.NETWORK_TYPE) in [const.TYPE_FLAT, const.TYPE_VLAN]: physnet = extnet.get(pnet.PHYSICAL_NETWORK) if not physnet: return [] chassis_physnets = self._sb_idl.get_chassis_and_physnets() return [chassis for chassis, physnets in chassis_physnets.items() if physnet in physnets] return [] def _gen_router_port_ext_ids(self, port): return { ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number( port, ovn_const.TYPE_ROUTER_PORTS))} def create_router_port(self, router_id, port, txn=None): """Create a logical router port.""" lrouter = utils.ovn_name(router_id) networks, ipv6_ra_configs = ( self._get_nets_and_ipv6_ra_confs_for_router_port( port['fixed_ips'])) lrouter_port_name = utils.ovn_lrouter_port_name(port['id']) is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get( 'device_owner') columns = {} if is_gw_port: context = n_context.get_admin_context() candidates = self.get_candidates_for_scheduling( self._plugin.get_network(context, port['network_id'])) selected_chassis = self._ovn_scheduler.select( self._nb_idl, self._sb_idl, lrouter_port_name, candidates=candidates) if selected_chassis: columns['gateway_chassis'] = selected_chassis if ipv6_ra_configs: columns['ipv6_ra_configs'] = ipv6_ra_configs commands = [ self._nb_idl.add_lrouter_port( name=lrouter_port_name, lrouter=lrouter, mac=port['mac_address'], networks=networks, may_exist=True, external_ids=self._gen_router_port_ext_ids(port), **columns), self._nb_idl.set_lrouter_port_in_lswitch_port( port['id'], lrouter_port_name, is_gw_port=is_gw_port)] self._transaction(commands, txn=txn) # NOTE(mangelajo): we don't bump the revision here, but we do # in the higher level add_router_interface function, because # we can't consider it done until the Nat rules are updated def update_router_port(self, port, bump_db_rev=True, if_exists=False): """Update a logical router port.""" networks, ipv6_ra_configs = ( self._get_nets_and_ipv6_ra_confs_for_router_port( port['fixed_ips'])) lrouter_port_name = utils.ovn_lrouter_port_name(port['id']) update = {'networks': networks, 'ipv6_ra_configs': ipv6_ra_configs} is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get( 'device_owner') check_rev_cmd = self._nb_idl.check_revision_number( lrouter_port_name, port, ovn_const.TYPE_ROUTER_PORTS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) txn.add(self._nb_idl.update_lrouter_port( name=lrouter_port_name, external_ids=self._gen_router_port_ext_ids(port), if_exists=if_exists, **update)) txn.add(self._nb_idl.set_lrouter_port_in_lswitch_port( port['id'], lrouter_port_name, is_gw_port=is_gw_port)) if bump_db_rev and check_rev_cmd.result == ovn_const.TXN_COMMITTED: db_rev.bump_revision(port, ovn_const.TYPE_ROUTER_PORTS) else: # NOTE(mangelajo): we don't bump the revision here, but we do # in the higher level add_router_interface function, because # we can't consider it done until the Nat rules are updated pass def delete_router_port(self, port_id, router_id=None): """Delete a logical router port.""" with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.lrp_del( utils.ovn_lrouter_port_name(port_id), utils.ovn_name(router_id) if router_id else None, if_exists=True)) db_rev.delete_revision(port_id, ovn_const.TYPE_ROUTER_PORTS) def update_nat_rules(self, router, networks, enable_snat, txn=None): """Update the NAT rules in a logical router.""" context = n_context.get_admin_context() func = (self._nb_idl.add_nat_rule_in_lrouter if enable_snat else self._nb_idl.delete_nat_rule_in_lrouter) gw_lrouter_name = utils.ovn_name(router['id']) gw_info = self._get_gw_info(context, router) commands = [] for network in networks: commands.append( func(gw_lrouter_name, type='snat', logical_ip=network, external_ip=gw_info.router_ip)) self._transaction(commands, txn=txn) def _create_provnet_port(self, txn, network, physnet, tag): txn.add(self._nb_idl.create_lswitch_port( lport_name=utils.ovn_provnet_port_name(network['id']), lswitch_name=utils.ovn_name(network['id']), addresses=['unknown'], external_ids={}, type='localnet', tag=tag if tag else [], options={'network_name': physnet})) def _gen_network_external_ids(self, network): ext_ids = { ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network['name'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( utils.get_revision_number(network, ovn_const.TYPE_NETWORKS))} # NOTE(lucasagomes): There's a difference between the # "qos_policy_id" key existing and it being None, the latter is a # valid value. Since we can't save None in OVSDB, we are converting # it to "null" as a placeholder. if 'qos_policy_id' in network: ext_ids[ovn_const.OVN_QOS_POLICY_EXT_ID_KEY] = ( network['qos_policy_id'] or 'null') return ext_ids def create_network(self, network): # Create a logical switch with a name equal to the Neutron network # UUID. This provides an easy way to refer to the logical switch # without having to track what UUID OVN assigned to it. ext_ids = self._gen_network_external_ids(network) lswitch_name = utils.ovn_name(network['id']) with self._nb_idl.transaction(check_error=True) as txn: txn.add(self._nb_idl.ls_add(lswitch_name, external_ids=ext_ids, may_exist=True)) physnet = network.get(pnet.PHYSICAL_NETWORK) if physnet: self._create_provnet_port(txn, network, physnet, network.get(pnet.SEGMENTATION_ID)) db_rev.bump_revision(network, ovn_const.TYPE_NETWORKS) self.create_metadata_port(n_context.get_admin_context(), network) return network def delete_network(self, network_id): with self._nb_idl.transaction(check_error=True) as txn: ls, ls_dns_record = self._nb_idl.get_ls_and_dns_record( utils.ovn_name(network_id)) txn.add(self._nb_idl.ls_del(utils.ovn_name(network_id), if_exists=True)) if ls_dns_record: txn.add(self._nb_idl.dns_del(ls_dns_record.uuid)) db_rev.delete_revision(network_id, ovn_const.TYPE_NETWORKS) def _is_qos_update_required(self, network): # Is qos service enabled if 'qos_policy_id' not in network: return False # Check if qos service wasn't enabled before ovn_net = self._nb_idl.get_lswitch(utils.ovn_name(network['id'])) if ovn_const.OVN_QOS_POLICY_EXT_ID_KEY not in ovn_net.external_ids: return True # Check if the policy_id has changed new_qos_id = network['qos_policy_id'] or 'null' return new_qos_id != ovn_net.external_ids[ ovn_const.OVN_QOS_POLICY_EXT_ID_KEY] def update_network(self, network): lswitch_name = utils.ovn_name(network['id']) # Check if QoS needs to be update, before updating OVNDB qos_update_required = self._is_qos_update_required(network) check_rev_cmd = self._nb_idl.check_revision_number( lswitch_name, network, ovn_const.TYPE_NETWORKS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) ext_ids = self._gen_network_external_ids(network) txn.add(self._nb_idl.set_lswitch_ext_ids(lswitch_name, ext_ids)) if check_rev_cmd.result == ovn_const.TXN_COMMITTED: if qos_update_required: self._qos_driver.update_network(network) db_rev.bump_revision(network, ovn_const.TYPE_NETWORKS) def _add_subnet_dhcp_options(self, subnet, network, ovn_dhcp_options=None): if utils.is_dhcp_options_ignored(subnet): return if not ovn_dhcp_options: ovn_dhcp_options = self._get_ovn_dhcp_options(subnet, network) with self._nb_idl.transaction(check_error=True) as txn: rev_num = {ovn_const.OVN_REV_NUM_EXT_ID_KEY: str( utils.get_revision_number(subnet, ovn_const.TYPE_SUBNETS))} ovn_dhcp_options['external_ids'].update(rev_num) txn.add(self._nb_idl.add_dhcp_options(subnet['id'], **ovn_dhcp_options)) def _get_ovn_dhcp_options(self, subnet, network, server_mac=None): external_ids = { 'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number( subnet, ovn_const.TYPE_SUBNETS))} dhcp_options = {'cidr': subnet['cidr'], 'options': {}, 'external_ids': external_ids} if subnet['enable_dhcp']: if subnet['ip_version'] == const.IP_VERSION_4: dhcp_options['options'] = self._get_ovn_dhcpv4_opts( subnet, network, server_mac=server_mac) else: dhcp_options['options'] = self._get_ovn_dhcpv6_opts( subnet, server_id=server_mac) return dhcp_options def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None): metadata_port_ip = self._find_metadata_port_ip( n_context.get_admin_context(), subnet) # TODO(dongj): Currently the metadata port is created only when # ovn_metadata_enabled is true, therefore this is a restriction for # supporting DHCP of subnet without gateway IP. # We will remove this restriction later. service_id = subnet['gateway_ip'] or metadata_port_ip if not service_id: return {} default_lease_time = str(config.get_ovn_dhcp_default_lease_time()) mtu = network['mtu'] options = { 'server_id': service_id, 'lease_time': default_lease_time, 'mtu': str(mtu), } if subnet['gateway_ip']: options['router'] = subnet['gateway_ip'] if server_mac: options['server_mac'] = server_mac else: options['server_mac'] = n_net.get_random_mac( cfg.CONF.base_mac.split(':')) if subnet['dns_nameservers']: dns_servers = '{%s}' % ', '.join(subnet['dns_nameservers']) options['dns_server'] = dns_servers routes = [] if metadata_port_ip: routes.append('%s/32,%s' % ( metadata_agent.METADATA_DEFAULT_IP, metadata_port_ip)) # Add subnet host_routes to 'classless_static_route' dhcp option routes.extend(['%s,%s' % (route['destination'], route['nexthop']) for route in subnet['host_routes']]) if routes: # if there are static routes, then we need to add the # default route in this option. As per RFC 3442 dhcp clients # should ignore 'router' dhcp option (option 3) # if option 121 is present. if subnet['gateway_ip']: routes.append('0.0.0.0/0,%s' % subnet['gateway_ip']) options['classless_static_route'] = '{' + ', '.join(routes) + '}' return options def _get_ovn_dhcpv6_opts(self, subnet, server_id=None): """Returns the DHCPv6 options""" dhcpv6_opts = { 'server_id': server_id or n_net.get_random_mac( cfg.CONF.base_mac.split(':')) } if subnet['dns_nameservers']: dns_servers = '{%s}' % ', '.join(subnet['dns_nameservers']) dhcpv6_opts['dns_server'] = dns_servers if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS: dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true' return dhcpv6_opts def _remove_subnet_dhcp_options(self, subnet_id, txn): dhcp_options = self._nb_idl.get_subnet_dhcp_options( subnet_id, with_ports=True) if dhcp_options['subnet'] is not None: txn.add(self._nb_idl.delete_dhcp_options( dhcp_options['subnet']['uuid'])) # Remove subnet and port DHCP_Options rows, the DHCP options in # lsp rows will be removed by related UUID for opt in dhcp_options['ports']: txn.add(self._nb_idl.delete_dhcp_options(opt['uuid'])) def _enable_subnet_dhcp_options(self, subnet, network, txn): if utils.is_dhcp_options_ignored(subnet): return filters = {'fixed_ips': {'subnet_id': [subnet['id']]}} all_ports = self._plugin.get_ports(n_context.get_admin_context(), filters=filters) ports = [p for p in all_ports if not utils.is_network_device_port(p)] dhcp_options = self._get_ovn_dhcp_options(subnet, network) subnet_dhcp_cmd = self._nb_idl.add_dhcp_options(subnet['id'], **dhcp_options) subnet_dhcp_option = txn.add(subnet_dhcp_cmd) # Traverse ports to add port DHCP_Options rows for port in ports: lsp_dhcp_disabled, lsp_dhcp_opts = utils.get_lsp_dhcp_opts( port, subnet['ip_version']) if lsp_dhcp_disabled: continue elif not lsp_dhcp_opts: lsp_dhcp_options = subnet_dhcp_option else: port_dhcp_options = copy.deepcopy(dhcp_options) port_dhcp_options['options'].update(lsp_dhcp_opts) port_dhcp_options['external_ids'].update( {'port_id': port['id']}) lsp_dhcp_options = txn.add(self._nb_idl.add_dhcp_options( subnet['id'], port_id=port['id'], **port_dhcp_options)) columns = {'dhcpv6_options': lsp_dhcp_options} if \ subnet['ip_version'] == const.IP_VERSION_6 else { 'dhcpv4_options': lsp_dhcp_options} # Set lsp DHCP options txn.add(self._nb_idl.set_lswitch_port( lport_name=port['id'], **columns)) def _update_subnet_dhcp_options(self, subnet, network, txn): if utils.is_dhcp_options_ignored(subnet): return original_options = self._nb_idl.get_subnet_dhcp_options( subnet['id'])['subnet'] mac = None if original_options: if subnet['ip_version'] == const.IP_VERSION_6: mac = original_options['options'].get('server_id') else: mac = original_options['options'].get('server_mac') new_options = self._get_ovn_dhcp_options(subnet, network, mac) # Check whether DHCP changed if (original_options and original_options['cidr'] == new_options['cidr'] and original_options['options'] == new_options['options']): return txn.add(self._nb_idl.add_dhcp_options(subnet['id'], **new_options)) dhcp_options = self._nb_idl.get_subnet_dhcp_options( subnet['id'], with_ports=True) for opt in dhcp_options['ports']: if not new_options.get('options'): continue options = dict(new_options['options']) options.update(opt['options']) port_id = opt['external_ids']['port_id'] txn.add(self._nb_idl.add_dhcp_options( subnet['id'], port_id=port_id, options=options)) def create_subnet(self, subnet, network): if subnet['enable_dhcp']: if subnet['ip_version'] == 4: context = n_context.get_admin_context() self.update_metadata_port(context, network['id']) self._add_subnet_dhcp_options(subnet, network) db_rev.bump_revision(subnet, ovn_const.TYPE_SUBNETS) def update_subnet(self, subnet, network): ovn_subnet = self._nb_idl.get_subnet_dhcp_options( subnet['id'])['subnet'] if subnet['enable_dhcp'] or ovn_subnet: context = n_context.get_admin_context() self.update_metadata_port(context, network['id']) check_rev_cmd = self._nb_idl.check_revision_number( subnet['id'], subnet, ovn_const.TYPE_SUBNETS) with self._nb_idl.transaction(check_error=True) as txn: txn.add(check_rev_cmd) if subnet['enable_dhcp'] and not ovn_subnet: self._enable_subnet_dhcp_options(subnet, network, txn) elif not subnet['enable_dhcp'] and ovn_subnet: self._remove_subnet_dhcp_options(subnet['id'], txn) elif subnet['enable_dhcp'] and ovn_subnet: self._update_subnet_dhcp_options(subnet, network, txn) db_rev.bump_revision(subnet, ovn_const.TYPE_SUBNETS) def delete_subnet(self, subnet_id): with self._nb_idl.transaction(check_error=True) as txn: self._remove_subnet_dhcp_options(subnet_id, txn) db_rev.delete_revision(subnet_id, ovn_const.TYPE_FLOATINGIPS) def create_security_group(self, security_group): with self._nb_idl.transaction(check_error=True) as txn: for ip_version in ('ip4', 'ip6'): name = utils.ovn_addrset_name(security_group['id'], ip_version) ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: security_group['id']} txn.add(self._nb_idl.create_address_set( name=name, external_ids=ext_ids)) db_rev.bump_revision(security_group, ovn_const.TYPE_SECURITY_GROUPS) def delete_security_group(self, security_group_id): with self._nb_idl.transaction(check_error=True) as txn: for ip_version in ('ip4', 'ip6'): name = utils.ovn_addrset_name(security_group_id, ip_version) txn.add(self._nb_idl.delete_address_set(name=name)) db_rev.delete_revision(security_group_id, ovn_const.TYPE_SECURITY_GROUPS) def _process_security_group_rule(self, rule, is_add_acl=True): admin_context = n_context.get_admin_context() ovn_acl.update_acls_for_security_group( self._plugin, admin_context, self._nb_idl, rule['security_group_id'], rule, is_add_acl=is_add_acl) def create_security_group_rule(self, rule): self._process_security_group_rule(rule) db_rev.bump_revision(rule, ovn_const.TYPE_SECURITY_GROUP_RULES) def delete_security_group_rule(self, rule): self._process_security_group_rule(rule, is_add_acl=False) db_rev.delete_revision(rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES) def _find_metadata_port(self, context, network_id): if not config.is_ovn_metadata_enabled(): return ports = self._plugin.get_ports(context, filters=dict( network_id=[network_id], device_owner=[const.DEVICE_OWNER_DHCP])) # There should be only one metadata port per network if len(ports) == 1: return ports[0] def _find_metadata_port_ip(self, context, subnet): metadata_port = self._find_metadata_port(context, subnet['network_id']) if metadata_port: for fixed_ip in metadata_port['fixed_ips']: if fixed_ip['subnet_id'] == subnet['id']: return fixed_ip['ip_address'] def _get_metadata_ports(self, context, network_id): if not config.is_ovn_metadata_enabled(): return return self._plugin.get_ports(context, filters=dict( network_id=[network_id], device_owner=[const.DEVICE_OWNER_DHCP])) def create_metadata_port(self, context, network): if config.is_ovn_metadata_enabled(): metadata_ports = self._get_metadata_ports(context, network['id']) if not metadata_ports: # Create a neutron port for DHCP/metadata services port = {'port': {'network_id': network['id'], 'tenant_id': network['project_id'], 'device_owner': const.DEVICE_OWNER_DHCP}} p_utils.create_port(self._plugin, context, port) elif len(metadata_ports) > 1: LOG.error("More than one metadata ports found for network %s. " "Please run the neutron-ovn-db-sync-util to fix it.", network['id']) def update_metadata_port(self, context, network_id): """Update metadata port. This function will allocate an IP address for the metadata port of the given network in all its IPv4 subnets. """ if not config.is_ovn_metadata_enabled(): return # Retrieve the metadata port of this network metadata_port = self._find_metadata_port(context, network_id) if not metadata_port: LOG.error("Metadata port couldn't be found for network %s", network_id) return # Retrieve all subnets in this network subnets = self._plugin.get_subnets(context, filters=dict( network_id=[network_id], ip_version=[4])) subnet_ids = set(s['id'] for s in subnets) port_subnet_ids = set(ip['subnet_id'] for ip in metadata_port['fixed_ips']) # Find all subnets where metadata port doesn't have an IP in and # allocate one. if subnet_ids != port_subnet_ids: wanted_fixed_ips = [] for fixed_ip in metadata_port['fixed_ips']: wanted_fixed_ips.append( {'subnet_id': fixed_ip['subnet_id'], 'ip_address': fixed_ip['ip_address']}) wanted_fixed_ips.extend( dict(subnet_id=s) for s in subnet_ids - port_subnet_ids) port = {'id': metadata_port['id'], 'port': {'network_id': network_id, 'fixed_ips': wanted_fixed_ips}} self._plugin.update_port(n_context.get_admin_context(), metadata_port['id'], port) def get_parent_port(self, port_id): return self._nb_idl.get_parent_port(port_id) def is_dns_required_for_port(self, port): try: if not all([port['dns_name'], port['dns_assignment'], port['device_id']]): return False except KeyError: # Possible that dns extension is not enabled. return False if not self._nb_idl.is_table_present('DNS'): return False return True def get_port_dns_records(self, port): port_dns_records = {} for dns_assignment in port.get('dns_assignment', []): hostname = dns_assignment['hostname'] fqdn = dns_assignment['fqdn'] if hostname not in port_dns_records: port_dns_records[hostname] = dns_assignment['ip_address'] else: port_dns_records[hostname] += " " + ( dns_assignment['ip_address']) if fqdn not in port_dns_records: port_dns_records[fqdn] = dns_assignment['ip_address'] else: port_dns_records[fqdn] += " " + dns_assignment['ip_address'] return port_dns_records def add_txns_to_sync_port_dns_records(self, txn, port, original_port=None): # NOTE(numans): - This implementation has certain known limitations # and that will be addressed in the future patches # https://bugs.launchpad.net/networking-ovn/+bug/1739257. # Please see the bug report for more information, but just to sum up # here # - We will have issues if two ports have same dns name # - If a port is deleted with dns name 'd1' and a new port is # added with the same dns name 'd1'. records_to_add = self.get_port_dns_records(port) lswitch_name = utils.ovn_name(port['network_id']) ls, ls_dns_record = self._nb_idl.get_ls_and_dns_record(lswitch_name) # If ls_dns_record is None, then we need to create a DNS row for the # logical switch. if ls_dns_record is None: dns_add_txn = txn.add(self._nb_idl.dns_add( external_ids={'ls_name': ls.name}, records=records_to_add)) txn.add(self._nb_idl.ls_set_dns_records(ls.uuid, dns_add_txn)) return if original_port: old_records = self.get_port_dns_records(original_port) for old_hostname, old_ips in old_records.items(): if records_to_add.get(old_hostname) != old_ips: txn.add(self._nb_idl.dns_remove_record( ls_dns_record.uuid, old_hostname)) for hostname, ips in records_to_add.items(): if ls_dns_record.records.get(hostname) != ips: txn.add(self._nb_idl.dns_add_record( ls_dns_record.uuid, hostname, ips)) def add_txns_to_remove_port_dns_records(self, txn, port): lswitch_name = utils.ovn_name(port['network_id']) ls, ls_dns_record = self._nb_idl.get_ls_and_dns_record(lswitch_name) if ls_dns_record is None: return hostnames = [] for dns_assignment in port['dns_assignment']: if dns_assignment['hostname'] not in hostnames: hostnames.append(dns_assignment['hostname']) if dns_assignment['fqdn'] not in hostnames: hostnames.append(dns_assignment['fqdn']) for hostname in hostnames: if ls_dns_record.records.get(hostname): txn.add(self._nb_idl.dns_remove_record( ls_dns_record.uuid, hostname)) networking-ovn-4.0.0/networking_ovn/common/__init__.py0000666000175100017510000000000013245511145023171 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/common/maintenance.py0000666000175100017510000002523513245511164023736 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. import inspect import threading from futurist import periodics from neutron.common import config as n_conf from neutron_lib import context as n_context from neutron_lib import worker from oslo_log import log from networking_ovn.common import constants as ovn_const from networking_ovn.db import maintenance as db_maint from networking_ovn.db import revision as db_rev LOG = log.getLogger(__name__) DB_CONSISTENCY_CHECK_INTERVAL = 300 # 5 minutes class MaintenanceWorker(worker.BaseWorker): def start(self): super(MaintenanceWorker, self).start() # NOTE(twilson) The super class will trigger the post_fork_initialize # in the driver, which starts the connection/IDL notify loop which # keeps the process from exiting def stop(self): """Stop service.""" super(MaintenanceWorker, self).stop() def wait(self): """Wait for service to complete.""" super(MaintenanceWorker, self).wait() @staticmethod def reset(): n_conf.reset_service() class MaintenanceThread(object): def __init__(self): self._callables = [] self._thread = None self._worker = None def add_periodics(self, obj): for name, member in inspect.getmembers(obj): if periodics.is_periodic(member): LOG.debug('Periodic task found: %(owner)s.%(member)s', {'owner': obj.__class__.__name__, 'member': name}) self._callables.append((member, (), {})) def start(self): if self._thread is None: self._worker = periodics.PeriodicWorker(self._callables) self._thread = threading.Thread(target=self._worker.start) self._thread.daemon = True self._thread.start() def stop(self): self._worker.stop() self._worker.wait() self._thread.join() self._worker = self._thread = None class DBInconsistenciesPeriodics(object): def __init__(self, ovn_client): self._ovn_client = ovn_client # FIXME(lucasagomes): We should not be accessing private # attributes like that, perhaps we should extend the OVNClient # class and create an interface for the locks ? self._nb_idl = self._ovn_client._nb_idl self._idl = self._nb_idl.idl self._idl.set_lock('ovn_db_inconsistencies_periodics') self._resources_func_map = { ovn_const.TYPE_NETWORKS: { 'neutron_get': self._ovn_client._plugin.get_network, 'ovn_get': self._nb_idl.get_lswitch, 'ovn_create': self._ovn_client.create_network, 'ovn_update': self._ovn_client.update_network, 'ovn_delete': self._ovn_client.delete_network, }, ovn_const.TYPE_PORTS: { 'neutron_get': self._ovn_client._plugin.get_port, 'ovn_get': self._nb_idl.get_lswitch_port, 'ovn_create': self._ovn_client.create_port, 'ovn_update': self._ovn_client.update_port, 'ovn_delete': self._ovn_client.delete_port, }, ovn_const.TYPE_FLOATINGIPS: { 'neutron_get': self._ovn_client._l3_plugin.get_floatingip, 'ovn_get': self._nb_idl.get_floatingip, 'ovn_create': self._ovn_client.create_floatingip, 'ovn_update': self._ovn_client.update_floatingip, 'ovn_delete': self._ovn_client.delete_floatingip, }, ovn_const.TYPE_ROUTERS: { 'neutron_get': self._ovn_client._l3_plugin.get_router, 'ovn_get': self._nb_idl.get_lrouter, 'ovn_create': self._ovn_client.create_router, 'ovn_update': self._ovn_client.update_router, 'ovn_delete': self._ovn_client.delete_router, }, ovn_const.TYPE_SECURITY_GROUPS: { 'neutron_get': self._ovn_client._plugin.get_security_group, 'ovn_get': self._nb_idl.get_address_set, 'ovn_create': self._ovn_client.create_security_group, 'ovn_delete': self._ovn_client.delete_security_group, }, ovn_const.TYPE_SECURITY_GROUP_RULES: { 'neutron_get': self._ovn_client._plugin.get_security_group_rule, 'ovn_get': self._nb_idl.get_acl_by_id, 'ovn_create': self._ovn_client.create_security_group_rule, 'ovn_delete': self._ovn_client.delete_security_group_rule, }, ovn_const.TYPE_ROUTER_PORTS: { 'neutron_get': self._ovn_client._plugin.get_port, 'ovn_get': self._nb_idl.get_lrouter_port, 'ovn_create': self._create_lrouter_port, 'ovn_update': self._update_lrouter_port, 'ovn_delete': self._ovn_client.delete_router_port, }, } @property def has_lock(self): return not self._idl.is_lock_contended def _fix_create_update(self, row): res_map = self._resources_func_map[row.resource_type] admin_context = n_context.get_admin_context() # Get the latest version of the resource in Neutron DB n_obj = res_map['neutron_get'](admin_context, row.resource_uuid) ovn_obj = res_map['ovn_get'](row.resource_uuid) if not ovn_obj: res_map['ovn_create'](n_obj) else: if row.resource_type == ovn_const.TYPE_SECURITY_GROUP_RULES: LOG.error("SG rule %s found with a revision number while " "this resource doesn't support updates", row.resource_uuid) elif row.resource_type == ovn_const.TYPE_SECURITY_GROUPS: # In OVN, we don't care about updates to security groups, # so just bump the revision number to whatever it's # supposed to be. db_rev.bump_revision(n_obj, row.resource_type) else: ext_ids = getattr(ovn_obj, 'external_ids', {}) ovn_revision = int(ext_ids.get( ovn_const.OVN_REV_NUM_EXT_ID_KEY, -1)) # If the resource exist in the OVN DB but the revision # number is different from Neutron DB, updated it. if ovn_revision != n_obj['revision_number']: res_map['ovn_update'](n_obj) else: # If the resource exist and the revision number # is equal on both databases just bump the revision on # the cache table. db_rev.bump_revision(n_obj, row.resource_type) def _fix_delete(self, row): res_map = self._resources_func_map[row.resource_type] ovn_obj = res_map['ovn_get'](row.resource_uuid) if not ovn_obj: db_rev.delete_revision(row.resource_uuid, row.resource_type) else: res_map['ovn_delete'](row.resource_uuid) def _fix_create_update_subnet(self, row): # Get the lasted version of the port in Neutron DB admin_context = n_context.get_admin_context() sn_db_obj = self._ovn_client._plugin.get_subnet( admin_context, row.resource_uuid) n_db_obj = self._ovn_client._plugin.get_network( admin_context, sn_db_obj['network_id']) if row.revision_number == ovn_const.INITIAL_REV_NUM: self._ovn_client.create_subnet(sn_db_obj, n_db_obj) else: self._ovn_client.update_subnet(sn_db_obj, n_db_obj) @periodics.periodic(spacing=DB_CONSISTENCY_CHECK_INTERVAL, run_immediately=True) def check_for_inconsistencies(self): # Only the worker holding a valid lock within OVSDB will run # this periodic if not self.has_lock: return create_update_inconsistencies = db_maint.get_inconsistent_resources() delete_inconsistencies = db_maint.get_deleted_resources() if not any([create_update_inconsistencies, delete_inconsistencies]): return LOG.warning('Inconsistencies found in the database!') # Fix the create/update resources inconsistencies for row in create_update_inconsistencies: try: # NOTE(lucasagomes): The way to fix subnets is bit # different than other resources. A subnet in OVN language # is just a DHCP rule but, this rule only exist if the # subnet in Neutron has the "enable_dhcp" attribute set # to True. So, it's possible to have a consistent subnet # resource even when it does not exist in the OVN database. if row.resource_type == ovn_const.TYPE_SUBNETS: self._fix_create_update_subnet(row) else: self._fix_create_update(row) except Exception: LOG.exception('Failed to fix resource %(res_uuid)s ' '(type: %(res_type)s)', {'res_uuid': row.resource_uuid, 'res_type': row.resource_type}) # Fix the deleted resources inconsistencies for row in delete_inconsistencies: try: if row.resource_type == ovn_const.TYPE_SUBNETS: self._ovn_client.delete_subnet(row.resource_uuid) else: self._fix_delete(row) except Exception: LOG.exception('Failed to fix deleted resource %(res_uuid)s ' '(type: %(res_type)s)', {'res_uuid': row.resource_uuid, 'res_type': row.resource_type}) def _create_lrouter_port(self, port): admin_context = n_context.get_admin_context() self._ovn_client._l3_plugin.add_router_interface( admin_context, port['device_owner'], {'port_id': port['id']}) def _update_lrouter_port(self, port): self._ovn_client._l3_plugin.update_router_port( port['device_owner'], port) networking-ovn-4.0.0/networking_ovn/common/utils.py0000666000175100017510000002721013245511145022606 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 netaddr from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext from neutron_lib.api.definitions import l3 from neutron_lib.api import validators from neutron_lib import constants as const from neutron_lib import context as n_context from neutron_lib import exceptions as n_exc from neutron_lib.plugins import directory from neutron_lib.utils import net as n_utils from oslo_utils import netutils from oslo_utils import strutils from networking_ovn._i18n import _ from networking_ovn.common import constants from networking_ovn.common import exceptions as ovn_exc def ovn_name(id): # The name of the OVN entry will be neutron- # This is due to the fact that the OVN application checks if the name # is a UUID. If so then there will be no matches. # We prefix the UUID to enable us to use the Neutron UUID when # updating, deleting etc. return 'neutron-%s' % id def ovn_lrouter_port_name(id): # The name of the OVN lrouter port entry will be lrp- # This is to distinguish with the name of the connected lswitch patch port, # which is named with neutron port uuid, so that OVS patch ports are # generated properly. The pairing patch port names will be: # - patch-lrp--to- # - patch--to-lrp- # lrp stands for Logical Router Port return 'lrp-%s' % id def ovn_provnet_port_name(network_id): # The name of OVN lswitch provider network port entry will be # provnet-. The port is created for network having # provider:physical_network attribute. return constants.OVN_PROVNET_PORT_NAME_PREFIX + '%s' % network_id def ovn_vhu_sockpath(sock_dir, port_id): # Frame the socket path of a virtio socket return os.path.join( sock_dir, # this parameter will become the virtio port name, # so it should not exceed IFNAMSIZ(16). (const.VHOST_USER_DEVICE_PREFIX + port_id)[:14]) def ovn_addrset_name(sg_id, ip_version): # The name of the address set for the given security group id and ip # version. The format is: # as-- # with all '-' replaced with '_'. This replacement is necessary # because OVN doesn't support '-' in an address set name. return ('as-%s-%s' % (ip_version, sg_id)).replace('-', '_') def is_network_device_port(port): return port.get('device_owner', '').startswith( const.DEVICE_OWNER_PREFIXES) def get_lsp_dhcp_opts(port, ip_version): # Get dhcp options from Neutron port, for setting DHCP_Options row # in OVN. lsp_dhcp_disabled = False lsp_dhcp_opts = {} if is_network_device_port(port): lsp_dhcp_disabled = True else: for edo in port.get(edo_ext.EXTRADHCPOPTS, []): if edo['ip_version'] != ip_version: continue if edo['opt_name'] == 'dhcp_disabled' and ( edo['opt_value'] in ['True', 'true']): # OVN native DHCP is disabled on this port lsp_dhcp_disabled = True # Make sure return value behavior not depends on the order and # content of the extra DHCP options for the port lsp_dhcp_opts.clear() break if edo['opt_name'] not in ( constants.SUPPORTED_DHCP_OPTS[ip_version]): continue opt = edo['opt_name'].replace('-', '_') lsp_dhcp_opts[opt] = edo['opt_value'] return (lsp_dhcp_disabled, lsp_dhcp_opts) def is_lsp_trusted(port): return n_utils.is_port_trusted(port) if port.get('device_owner') else False def is_lsp_ignored(port): # Since the floating IP port is not bound to any chassis, packets from vm # destined to floating IP will be dropped. To overcome this, we do not # create/update floating IP port in OVN. return port.get('device_owner') in [const.DEVICE_OWNER_FLOATINGIP] def get_lsp_security_groups(port, skip_trusted_port=True): # In other agent link OVS, skipping trusted port is processed in security # groups RPC. We haven't that step, so we do it here. return [] if (skip_trusted_port and is_lsp_trusted(port) ) else port.get('security_groups', []) def is_snat_enabled(router): return router.get(l3.EXTERNAL_GW_INFO, {}).get('enable_snat', True) def validate_and_get_data_from_binding_profile(port): if (constants.OVN_PORT_BINDING_PROFILE not in port or not validators.is_attr_set( port[constants.OVN_PORT_BINDING_PROFILE])): return {} param_set = {} param_dict = {} for param_set in constants.OVN_PORT_BINDING_PROFILE_PARAMS: param_keys = param_set.keys() for param_key in param_keys: try: param_dict[param_key] = (port[ constants.OVN_PORT_BINDING_PROFILE][param_key]) except KeyError: pass if len(param_dict) == 0: continue if len(param_dict) != len(param_keys): msg = _('Invalid binding:profile. %s are all ' 'required.') % param_keys raise n_exc.InvalidInput(error_message=msg) if (len(port[constants.OVN_PORT_BINDING_PROFILE]) != len( param_keys)): msg = _('Invalid binding:profile. too many parameters') raise n_exc.InvalidInput(error_message=msg) break if not param_dict: return {} for param_key, param_type in param_set.items(): if param_type is None: continue param_value = param_dict[param_key] if not isinstance(param_value, param_type): msg = _('Invalid binding:profile. %(key)s %(value)s ' 'value invalid type') % {'key': param_key, 'value': param_value} raise n_exc.InvalidInput(error_message=msg) # Make sure we can successfully look up the port indicated by # parent_name. Just let it raise the right exception if there is a # problem. if 'parent_name' in param_set: plugin = directory.get_plugin() plugin.get_port(n_context.get_admin_context(), param_dict['parent_name']) if 'tag' in param_set: tag = int(param_dict['tag']) if tag < 0 or tag > 4095: msg = _('Invalid binding:profile. tag "%s" must be ' 'an integer between 0 and 4095, inclusive') % tag raise n_exc.InvalidInput(error_message=msg) return param_dict def is_dhcp_options_ignored(subnet): # Don't insert DHCP_Options entry for v6 subnet with 'SLAAC' as # 'ipv6_address_mode', since DHCPv6 shouldn't work for this mode. return (subnet['ip_version'] == const.IP_VERSION_6 and subnet.get('ipv6_address_mode') == const.IPV6_SLAAC) def get_ovn_ipv6_address_mode(address_mode): return constants.OVN_IPV6_ADDRESS_MODES[address_mode] def get_revision_number(resource, resource_type): """Get the resource's revision number based on its type.""" if resource_type in (constants.TYPE_NETWORKS, constants.TYPE_PORTS, constants.TYPE_SECURITY_GROUP_RULES, constants.TYPE_ROUTERS, constants.TYPE_ROUTER_PORTS, constants.TYPE_SECURITY_GROUPS, constants.TYPE_FLOATINGIPS, constants.TYPE_SUBNETS): return resource['revision_number'] else: raise ovn_exc.UnknownResourceType(resource_type=resource_type) def remove_macs_from_lsp_addresses(addresses): """Remove the mac addreses from the Logical_Switch_Port addresses column. :param addresses: The list of addresses from the Logical_Switch_Port. Example: ["80:fa:5b:06:72:b7 158.36.44.22", "ff:ff:ff:ff:ff:ff 10.0.0.2"] :returns: A list of IP addesses (v4 and v6) """ ip_list = [] for addr in addresses: ip_list.extend([x for x in addr.split() if (netutils.is_valid_ipv4(x) or netutils.is_valid_ipv6(x))]) return ip_list def get_allowed_address_pairs_ip_addresses(port): """Return a list of IP addresses from port's allowed_address_pairs. :param port: A neutron port :returns: A list of IP addesses (v4 and v6) """ return [x['ip_address'] for x in port.get('allowed_address_pairs', []) if 'ip_address' in x] def get_allowed_address_pairs_ip_addresses_from_ovn_port(ovn_port): """Return a list of IP addresses from ovn port. Return a list of IP addresses equivalent of Neutron's port allowed_address_pairs column using the data in the OVN port. :param ovn_port: A OVN port :returns: A list of IP addesses (v4 and v6) """ addresses = remove_macs_from_lsp_addresses(ovn_port.addresses) port_security = remove_macs_from_lsp_addresses(ovn_port.port_security) return [x for x in port_security if x not in addresses] def get_ovn_port_security_groups(ovn_port, skip_trusted_port=True): info = {'security_groups': ovn_port.external_ids.get( constants.OVN_SG_IDS_EXT_ID_KEY, '').split(), 'device_owner': ovn_port.external_ids.get( constants.OVN_DEVICE_OWNER_EXT_ID_KEY, '')} return get_lsp_security_groups(info, skip_trusted_port=skip_trusted_port) def get_ovn_port_addresses(ovn_port): addresses = remove_macs_from_lsp_addresses(ovn_port.addresses) port_security = remove_macs_from_lsp_addresses(ovn_port.port_security) return list(set(addresses + port_security)) def sort_ips_by_version(addresses): ip_map = {'ip4': [], 'ip6': []} for addr in addresses: ip_version = netaddr.IPNetwork(addr).version ip_map['ip%d' % ip_version].append(addr) return ip_map def is_lsp_router_port(port): return port.get('device_owner') in [const.DEVICE_OWNER_ROUTER_INTF, const.DEVICE_OWNER_ROUTER_GW] def get_lrouter_ext_gw_static_route(ovn_router): # TODO(lucasagomes): Remove the try...except block after OVS 2.8.2 # is tagged. try: for route in getattr(ovn_router, 'static_routes', []): external_ids = getattr(route, 'external_ids', {}) if strutils.bool_from_string( external_ids.get(constants.OVN_ROUTER_IS_EXT_GW, 'false')): return route except KeyError: pass def get_lrouter_snats(ovn_router): return [n for n in getattr(ovn_router, 'nat', []) if n.type == 'snat'] def get_lrouter_non_gw_routes(ovn_router): routes = [] # TODO(lucasagomes): Remove the try...except block after OVS 2.8.2 # is tagged. try: for route in getattr(ovn_router, 'static_routes', []): external_ids = getattr(route, 'external_ids', {}) if strutils.bool_from_string( external_ids.get(constants.OVN_ROUTER_IS_EXT_GW, 'false')): continue routes.append({'destination': route.ip_prefix, 'nexthop': route.nexthop}) except KeyError: pass return routes def is_ovn_l3(l3_plugin): return hasattr(l3_plugin, '_ovn_client_inst') networking-ovn-4.0.0/networking_ovn/common/acl.py0000666000175100017510000003334513245511145022213 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 netaddr from neutron_lib import constants as const from neutron_lib import exceptions as n_exceptions from oslo_config import cfg from networking_ovn._i18n import _ from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils # Convert the protocol number from integer to strings because that's # how Neutron will pass it to us PROTOCOL_NAME_TO_NUM_MAP = {k: str(v) for k, v in const.IP_PROTOCOL_MAP.items()} # Create a map from protocol numbers to names PROTOCOL_NUM_TO_NAME_MAP = {v: k for k, v in PROTOCOL_NAME_TO_NUM_MAP.items()} # Group of transport protocols supported TRANSPORT_PROTOCOLS = (const.PROTO_NAME_TCP, const.PROTO_NAME_UDP, const.PROTO_NAME_SCTP, PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_TCP], PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_UDP], PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_SCTP]) # Group of versions of the ICMP protocol supported ICMP_PROTOCOLS = (const.PROTO_NAME_ICMP, const.PROTO_NAME_IPV6_ICMP, const.PROTO_NAME_IPV6_ICMP_LEGACY, PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_ICMP], PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_IPV6_ICMP], PROTOCOL_NAME_TO_NUM_MAP[const.PROTO_NAME_IPV6_ICMP_LEGACY]) class ProtocolNotSupported(n_exceptions.NeutronException): message = _('The protocol "%(protocol)s" is not supported. Valid ' 'protocols are: %(valid_protocols); or protocol ' 'numbers ranging from 0 to 255.') def is_sg_enabled(): return cfg.CONF.SECURITYGROUP.enable_security_group def acl_direction(r, port): if r['direction'] == 'ingress': portdir = 'outport' else: portdir = 'inport' return '%s == "%s"' % (portdir, port['id']) def acl_ethertype(r): match = '' ip_version = None icmp = None if r['ethertype'] == 'IPv4': match = ' && ip4' ip_version = 'ip4' icmp = 'icmp4' elif r['ethertype'] == 'IPv6': match = ' && ip6' ip_version = 'ip6' icmp = 'icmp6' return match, ip_version, icmp def acl_remote_ip_prefix(r, ip_version): if not r['remote_ip_prefix']: return '' src_or_dst = 'src' if r['direction'] == 'ingress' else 'dst' return ' && %s.%s == %s' % (ip_version, src_or_dst, r['remote_ip_prefix']) def _get_protocol_number(protocol): if protocol is None: return try: protocol = int(protocol) if protocol >= 0 and protocol <= 255: return str(protocol) except (ValueError, TypeError): protocol = PROTOCOL_NAME_TO_NUM_MAP.get(protocol) if protocol is not None: return protocol raise ProtocolNotSupported( protocol=protocol, valid_protocols=', '.join(PROTOCOL_NAME_TO_NUM_MAP)) def acl_protocol_and_ports(r, icmp): match = '' protocol = _get_protocol_number(r.get('protocol')) if protocol is None: return match min_port = r.get('port_range_min') max_port = r.get('port_range_max') if protocol in TRANSPORT_PROTOCOLS: protocol = PROTOCOL_NUM_TO_NAME_MAP[protocol] match += ' && %s' % protocol if min_port is not None and min_port == max_port: match += ' && %s.dst == %d' % (protocol, min_port) else: if min_port is not None: match += ' && %s.dst >= %d' % (protocol, min_port) if max_port is not None: match += ' && %s.dst <= %d' % (protocol, max_port) elif protocol in ICMP_PROTOCOLS: protocol = icmp match += ' && %s' % protocol if min_port is not None: match += ' && %s.type == %d' % (protocol, min_port) if max_port is not None: match += ' && %s.code == %d' % (protocol, max_port) else: match += ' && ip.proto == %s' % protocol return match def drop_all_ip_traffic_for_port(port): acl_list = [] for direction, p in (('from-lport', 'inport'), ('to-lport', 'outport')): lswitch = utils.ovn_name(port['network_id']) lport = port['id'] acl = {"lswitch": lswitch, "lport": lport, "priority": ovn_const.ACL_PRIORITY_DROP, "action": ovn_const.ACL_ACTION_DROP, "log": False, "name": [], "severity": [], "direction": direction, "match": '%s == "%s" && ip' % (p, port['id']), "external_ids": {'neutron:lport': port['id']}} acl_list.append(acl) return acl_list def add_sg_rule_acl_for_port(port, r, match): dir_map = { 'ingress': 'to-lport', 'egress': 'from-lport', } acl = {"lswitch": utils.ovn_name(port['network_id']), "lport": port['id'], "priority": ovn_const.ACL_PRIORITY_ALLOW, "action": ovn_const.ACL_ACTION_ALLOW_RELATED, "log": False, "name": [], "severity": [], "direction": dir_map[r['direction']], "match": match, "external_ids": {'neutron:lport': port['id'], ovn_const.OVN_SG_RULE_EXT_ID_KEY: r['id']}} return acl def add_acl_dhcp(port, subnet, ovn_dhcp=True): # Allow DHCP requests for OVN native DHCP service, while responses are # allowed in ovn-northd. # Allow both DHCP requests and responses to pass for other DHCP services. # We do this even if DHCP isn't enabled for the subnet acl_list = [] if not ovn_dhcp: acl = {"lswitch": utils.ovn_name(port['network_id']), "lport": port['id'], "priority": ovn_const.ACL_PRIORITY_ALLOW, "action": ovn_const.ACL_ACTION_ALLOW, "log": False, "name": [], "severity": [], "direction": 'to-lport', "match": ('outport == "%s" && ip4 && ip4.src == %s && ' 'udp && udp.src == 67 && udp.dst == 68' ) % (port['id'], subnet['cidr']), "external_ids": {'neutron:lport': port['id']}} acl_list.append(acl) acl = {"lswitch": utils.ovn_name(port['network_id']), "lport": port['id'], "priority": ovn_const.ACL_PRIORITY_ALLOW, "action": ovn_const.ACL_ACTION_ALLOW, "log": False, "name": [], "severity": [], "direction": 'from-lport', "match": ('inport == "%s" && ip4 && ' 'ip4.dst == {255.255.255.255, %s} && ' 'udp && udp.src == 68 && udp.dst == 67' ) % (port['id'], subnet['cidr']), "external_ids": {'neutron:lport': port['id']}} acl_list.append(acl) return acl_list def _get_subnet_from_cache(plugin, admin_context, subnet_cache, subnet_id): if subnet_id in subnet_cache: return subnet_cache[subnet_id] else: subnet = plugin.get_subnet(admin_context, subnet_id) if subnet: subnet_cache[subnet_id] = subnet return subnet def _get_sg_ports_from_cache(plugin, admin_context, sg_ports_cache, sg_id): if sg_id in sg_ports_cache: return sg_ports_cache[sg_id] else: filters = {'security_group_id': [sg_id]} sg_ports = plugin._get_port_security_group_bindings( admin_context, filters) if sg_ports: sg_ports_cache[sg_id] = sg_ports return sg_ports def _get_sg_from_cache(plugin, admin_context, sg_cache, sg_id): if sg_id in sg_cache: return sg_cache[sg_id] else: sg = plugin.get_security_group(admin_context, sg_id) if sg: sg_cache[sg_id] = sg return sg def acl_remote_group_id(r, ip_version): if not r['remote_group_id']: return '' src_or_dst = 'src' if r['direction'] == 'ingress' else 'dst' addrset_name = utils.ovn_addrset_name(r['remote_group_id'], ip_version) return ' && %s.%s == $%s' % (ip_version, src_or_dst, addrset_name) def _add_sg_rule_acl_for_port(port, r): # Update the match based on which direction this rule is for (ingress # or egress). match = acl_direction(r, port) # Update the match for IPv4 vs IPv6. ip_match, ip_version, icmp = acl_ethertype(r) match += ip_match # Update the match if an IPv4 or IPv6 prefix was specified. match += acl_remote_ip_prefix(r, ip_version) # Update the match if remote group id was specified. match += acl_remote_group_id(r, ip_version) # Update the match for the protocol (tcp, udp, icmp) and port/type # range if specified. match += acl_protocol_and_ports(r, icmp) # Finally, create the ACL entry for the direction specified. return add_sg_rule_acl_for_port(port, r, match) def _acl_columns_name_severity_supported(nb_idl): columns = list(nb_idl._tables['ACL'].columns) return ('name' in columns) and ('severity' in columns) def update_acls_for_security_group(plugin, admin_context, ovn, security_group_id, security_group_rule, sg_ports_cache=None, is_add_acl=True): # Skip ACLs if security groups aren't enabled if not is_sg_enabled(): return # Get the security group ports. sg_ports_cache = sg_ports_cache or {} sg_ports = _get_sg_ports_from_cache(plugin, admin_context, sg_ports_cache, security_group_id) # ACLs associated with a security group may span logical switches sg_port_ids = [binding['port_id'] for binding in sg_ports] sg_port_ids = list(set(sg_port_ids)) port_list = plugin.get_ports(admin_context, filters={'id': sg_port_ids}) if not port_list: return acl_new_values_dict = {} update_port_list = [] # Check if ACL log name and severity supported or not keep_name_severity = _acl_columns_name_severity_supported(ovn) # NOTE(lizk): We can directly locate the affected acl records, # so no need to compare new acl values with existing acl objects. for port in port_list: # Skip trusted port if utils.is_lsp_trusted(port): continue update_port_list.append(port) acl = _add_sg_rule_acl_for_port(port, security_group_rule) # Remove lport and lswitch since we don't need them acl.pop('lport') acl.pop('lswitch') # Remove ACL log name and severity if not supported, if not keep_name_severity: acl.pop('name') acl.pop('severity') acl_new_values_dict[port['id']] = acl if not update_port_list: return lswitch_names = set([p['network_id'] for p in update_port_list]) ovn.update_acls(list(lswitch_names), iter(update_port_list), acl_new_values_dict, need_compare=False, is_add_acl=is_add_acl).execute(check_error=True) def add_acls(plugin, admin_context, port, sg_cache, subnet_cache, ovn): acl_list = [] # Skip ACLs if security groups aren't enabled if not is_sg_enabled(): return acl_list sec_groups = utils.get_lsp_security_groups(port) if not sec_groups: return acl_list # Drop all IP traffic to and from the logical port by default. acl_list += drop_all_ip_traffic_for_port(port) # Add DHCP ACLs. port_subnet_ids = set() for ip in port['fixed_ips']: if netaddr.IPNetwork(ip['ip_address']).version != 4: continue subnet = _get_subnet_from_cache(plugin, admin_context, subnet_cache, ip['subnet_id']) # Ignore duplicate DHCP ACLs for the subnet. if subnet['id'] not in port_subnet_ids: acl_list += add_acl_dhcp(port, subnet, True) port_subnet_ids.add(subnet['id']) # We create an ACL entry for each rule on each security group applied # to this port. for sg_id in sec_groups: sg = _get_sg_from_cache(plugin, admin_context, sg_cache, sg_id) for r in sg['security_group_rules']: acl = _add_sg_rule_acl_for_port(port, r) acl_list.append(acl) # Remove ACL log name and severity if not supported, if not _acl_columns_name_severity_supported(ovn): for acl in acl_list: acl.pop('name') acl.pop('severity') return acl_list def acl_port_ips(port): # Skip ACLs if security groups aren't enabled if not is_sg_enabled(): return {'ip4': [], 'ip6': []} ip_list = [x['ip_address'] for x in port.get('fixed_ips', [])] ip_list.extend(utils.get_allowed_address_pairs_ip_addresses(port)) return utils.sort_ips_by_version(ip_list) networking-ovn-4.0.0/networking_ovn/common/constants.py0000666000175100017510000001164313245511145023465 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 neutron_lib.api.definitions import portbindings from neutron_lib import constants as const import six # TODO(lucasagomes): Remove OVN_SG_NAME_EXT_ID_KEY in the Rocky release OVN_SG_NAME_EXT_ID_KEY = 'neutron:security_group_name' OVN_SG_EXT_ID_KEY = 'neutron:security_group_id' OVN_SG_RULE_EXT_ID_KEY = 'neutron:security_group_rule_id' OVN_ML2_MECH_DRIVER_NAME = 'ovn' OVN_NETWORK_NAME_EXT_ID_KEY = 'neutron:network_name' OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name' OVN_ROUTER_NAME_EXT_ID_KEY = 'neutron:router_name' OVN_ROUTER_IS_EXT_GW = 'neutron:is_ext_gw' OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id' OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_id' OVN_PHYSNET_EXT_ID_KEY = 'neutron:provnet-physical-network' OVN_NETTYPE_EXT_ID_KEY = 'neutron:provnet-network-type' OVN_SEGID_EXT_ID_KEY = 'neutron:provnet-segmentation-id' OVN_PROJID_EXT_ID_KEY = 'neutron:project_id' OVN_DEVID_EXT_ID_KEY = 'neutron:device_id' OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs' OVN_FIP_EXT_ID_KEY = 'neutron:fip_id' OVN_FIP_PORT_EXT_ID_KEY = 'neutron:fip_port_id' OVN_REV_NUM_EXT_ID_KEY = 'neutron:revision_number' OVN_QOS_POLICY_EXT_ID_KEY = 'neutron:qos_policy_id' OVN_SG_IDS_EXT_ID_KEY = 'neutron:security_group_ids' OVN_DEVICE_OWNER_EXT_ID_KEY = 'neutron:device_owner' OVN_PORT_BINDING_PROFILE = portbindings.PROFILE OVN_PORT_BINDING_PROFILE_PARAMS = [{'parent_name': six.string_types, 'tag': six.integer_types}, {'vtep-physical-switch': six.string_types, 'vtep-logical-switch': six.string_types}] OVN_ROUTER_PORT_OPTION_KEYS = ['router-port', 'nat-addresses'] OVN_GATEWAY_CHASSIS_KEY = 'redirect-chassis' OVN_GATEWAY_NAT_ADDRESSES_KEY = 'nat-addresses' OVN_PROVNET_PORT_NAME_PREFIX = 'provnet-' OVN_NEUTRON_OWNER_TO_PORT_TYPE = {const.DEVICE_OWNER_DHCP: 'localport'} # OVN ACLs have priorities. The highest priority ACL that matches is the one # that takes effect. Our choice of priority numbers is arbitrary, but it # leaves room above and below the ACLs we create. We only need two priorities. # The first is for all the things we allow. The second is for dropping traffic # by default. ACL_PRIORITY_ALLOW = 1002 ACL_PRIORITY_DROP = 1001 ACL_ACTION_DROP = 'drop' ACL_ACTION_ALLOW_RELATED = 'allow-related' ACL_ACTION_ALLOW = 'allow' # When a OVN L3 gateway is created, it needs to be bound to a chassis. In # case a chassis is not found OVN_GATEWAY_INVALID_CHASSIS will be set in # the options column of the Logical Router. This value is used to detect # unhosted router gateways to schedule. OVN_GATEWAY_INVALID_CHASSIS = 'neutron-ovn-invalid-chassis' SUPPORTED_DHCP_OPTS = { 4: ['netmask', 'router', 'dns-server', 'log-server', 'lpr-server', 'swap-server', 'ip-forward-enable', 'policy-filter', 'default-ttl', 'mtu', 'router-discovery', 'router-solicitation', 'arp-timeout', 'ethernet-encap', 'tcp-ttl', 'tcp-keepalive', 'nis-server', 'ntp-server', 'tftp-server'], 6: ['server-id', 'dns-server', 'domain-search']} DHCPV6_STATELESS_OPT = 'dhcpv6_stateless' CHASSIS_DATAPATH_NETDEV = 'netdev' CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser' OVN_IPV6_ADDRESS_MODES = { const.IPV6_SLAAC: const.IPV6_SLAAC, const.DHCPV6_STATEFUL: const.DHCPV6_STATEFUL.replace('-', '_'), const.DHCPV6_STATELESS: const.DHCPV6_STATELESS.replace('-', '_') } DB_MAX_RETRIES = 60 DB_INITIAL_RETRY_INTERVAL = 0.5 DB_MAX_RETRY_INTERVAL = 1 TXN_COMMITTED = 'committed' INITIAL_REV_NUM = -1 # Resource types TYPE_NETWORKS = 'networks' TYPE_PORTS = 'ports' TYPE_SECURITY_GROUP_RULES = 'security_group_rules' TYPE_ROUTERS = 'routers' TYPE_ROUTER_PORTS = 'router_ports' TYPE_SECURITY_GROUPS = 'security_groups' TYPE_FLOATINGIPS = 'floatingips' TYPE_SUBNETS = 'subnets' _TYPES_PRIORITY_ORDER = ( TYPE_NETWORKS, TYPE_SECURITY_GROUPS, TYPE_SUBNETS, TYPE_ROUTERS, TYPE_PORTS, TYPE_ROUTER_PORTS, TYPE_FLOATINGIPS, TYPE_SECURITY_GROUP_RULES) # The order in which the resources should be created or updated by the # maintenance task: Root ones first and leafs at the end. MAINTENANCE_CREATE_UPDATE_TYPE_ORDER = { t: n for n, t in enumerate(_TYPES_PRIORITY_ORDER, 1)} # The order in which the resources should be deleted by the maintenance # task: Leaf ones first and roots at the end. MAINTENANCE_DELETE_TYPE_ORDER = { t: n for n, t in enumerate(reversed(_TYPES_PRIORITY_ORDER), 1)} networking-ovn-4.0.0/networking_ovn/agent/0000775000175100017510000000000013245511554020702 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/agent/metadata/0000775000175100017510000000000013245511554022462 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/agent/metadata/agent.py0000666000175100017510000003476713245511145024151 0ustar zuulzuul00000000000000# Copyright 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 re from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib from neutron.common import utils from neutron_lib import constants as n_const from oslo_concurrency import lockutils from oslo_log import log from ovsdbapp.backend.ovs_idl import event as row_event from ovsdbapp.backend.ovs_idl import vlog import six from networking_ovn.agent.metadata import driver as metadata_driver from networking_ovn.agent.metadata import ovsdb from networking_ovn.agent.metadata import server as metadata_server from networking_ovn.common import config from networking_ovn.common import constants as ovn_const LOG = log.getLogger(__name__) _SYNC_STATE_LOCK = lockutils.ReaderWriterLock() NS_PREFIX = 'ovnmeta-' METADATA_DEFAULT_PREFIX = 16 METADATA_DEFAULT_IP = '169.254.169.254' METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP, METADATA_DEFAULT_PREFIX) METADATA_PORT = 80 MAC_PATTERN = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I) MetadataPortInfo = collections.namedtuple('MetadataPortInfo', ['mac', 'ip_addresses']) def _sync_lock(f): """Decorator to block all operations for a global sync call.""" @six.wraps(f) def wrapped(*args, **kwargs): with _SYNC_STATE_LOCK.write_lock(): return f(*args, **kwargs) return wrapped def _wait_if_syncing(f): """Decorator to wait if any sync operations are in progress.""" @six.wraps(f) def wrapped(*args, **kwargs): with _SYNC_STATE_LOCK.read_lock(): return f(*args, **kwargs) return wrapped class PortBindingChassisEvent(row_event.RowEvent): def __init__(self, metadata_agent): self.agent = metadata_agent table = 'Port_Binding' events = (self.ROW_UPDATE) super(PortBindingChassisEvent, self).__init__( events, table, None) self.event_name = 'PortBindingChassisEvent' @_wait_if_syncing def run(self, event, row, old): # Check if the port has been bound/unbound to our chassis and update # the metadata namespace accordingly. # Type must be empty to make sure it's a VIF. if row.type != "": return new_chassis = getattr(row, 'chassis', []) old_chassis = getattr(old, 'chassis', []) if new_chassis and new_chassis[0].name == self.agent.chassis: LOG.info("Port %s in datapath %s bound to our chassis", row.logical_port, str(row.datapath.uuid)) self.agent.update_datapath(str(row.datapath.uuid)) elif old_chassis and old_chassis[0].name == self.agent.chassis: LOG.info("Port %s in datapath %s unbound from our chassis", row.logical_port, str(row.datapath.uuid)) self.agent.update_datapath(str(row.datapath.uuid)) class ChassisCreateEvent(row_event.RowEvent): """Row create event - Chassis name == our_chassis. On connection, we get a dump of all chassis so if we catch a creation of our own chassis it has to be a reconnection. In this case, we need to do a full sync to make sure that we capture all changes while the connection to OVSDB was down. """ def __init__(self, metadata_agent): self.agent = metadata_agent self.first_time = True table = 'Chassis' events = (self.ROW_CREATE) super(ChassisCreateEvent, self).__init__( events, table, (('name', '=', self.agent.chassis),)) self.event_name = 'ChassisCreateEvent' def run(self, event, row, old): if self.first_time: self.first_time = False else: LOG.info("Connection to OVSDB established, doing a full sync") self.agent.sync() class MetadataAgent(object): def __init__(self, conf): self.conf = conf vlog.use_python_logger(max_level=config.get_ovn_ovsdb_log_level()) self._process_monitor = external_process.ProcessMonitor( config=self.conf, resource_type='metadata') def start(self): # Launch the server that will act as a proxy between the VM's and Nova. proxy = metadata_server.UnixDomainMetadataProxy(self.conf) proxy.run() # Open the connection to OVS database self.ovs_idl = ovsdb.MetadataAgentOvsIdl().start() self.chassis = self._get_own_chassis_name() # Open the connection to OVN SB database. self.sb_idl = ovsdb.MetadataAgentOvnSbIdl( [PortBindingChassisEvent(self), ChassisCreateEvent(self)]).start() # Do the initial sync. self.sync() proxy.wait() def _get_own_chassis_name(self): """Return the external_ids:system-id value of the Open_vSwitch table. As long as ovn-controller is running on this node, the key is guaranteed to exist and will include the chassis name. """ ext_ids = self.ovs_idl.db_get( 'Open_vSwitch', '.', 'external_ids').execute() return ext_ids['system-id'] @_sync_lock def sync(self): """Agent sync. This function will make sure that all networks with ports in our chassis are serving metadata. Also, it will tear down those namespaces which were serving metadata but are no longer needed. """ metadata_namespaces = self.ensure_all_networks_provisioned() system_namespaces = ip_lib.IPWrapper().get_namespaces() unused_namespaces = [ns for ns in system_namespaces if ns.startswith(NS_PREFIX) and ns not in metadata_namespaces] for ns in unused_namespaces: self.teardown_datapath(self._get_datapath_name(ns)) @staticmethod def _get_veth_name(datapath): return ['{}{}{}'.format(n_const.TAP_DEVICE_PREFIX, datapath[:10], i) for i in [0, 1]] @staticmethod def _get_datapath_name(namespace): return namespace[len(NS_PREFIX):] @staticmethod def _get_namespace_name(datapath): return NS_PREFIX + datapath def teardown_datapath(self, datapath): """Unprovision this datapath to stop serving metadata. This function will shutdown metadata proxy if it's running and delete the VETH pair, the OVS port and the namespace. """ self.update_chassis_metadata_networks(datapath, remove=True) namespace = self._get_namespace_name(datapath) ip = ip_lib.IPWrapper(namespace) # If the namespace doesn't exist, return if not ip.netns.exists(namespace): return LOG.info("Cleaning up %s namespace which is not needed anymore", namespace) metadata_driver.MetadataDriver.destroy_monitored_metadata_proxy( self._process_monitor, datapath, self.conf, namespace) veth_name = self._get_veth_name(datapath) self.ovs_idl.del_port( veth_name[0], bridge=self.conf.ovs_integration_bridge).execute() if ip_lib.device_exists(veth_name[0]): ip_lib.IPWrapper().del_veth(veth_name[0]) ip.garbage_collect_namespace() def update_datapath(self, datapath): """Update the metadata service for this datapath. This function will: * Provision the namespace if it wasn't already in place. * Update the namespace if it was already serving metadata (for example, after binding/unbinding the first/last port of a subnet in our chassis). * Tear down the namespace if there are no more ports in our chassis for this datapath. """ ports = self.sb_idl.get_ports_on_chassis(self.chassis) datapath_ports = [p for p in ports if p.type == '' and str(p.datapath.uuid) == datapath] if datapath_ports: self.provision_datapath(datapath) else: self.teardown_datapath(datapath) def provision_datapath(self, datapath): """Provision the datapath so that it can serve metadata. This function will create the namespace and VETH pair if needed and assign the IP addresses to the interface corresponding to the metadata port of the network. It will also remove existing IP addresses that are no longer needed. :return: The metadata namespace name of this datapath """ LOG.debug("Provisioning datapath %s", datapath) port = self.sb_idl.get_metadata_port_network(datapath) # If there's no metadata port or it doesn't have a MAC or IP # addresses, then tear the namespace down if needed. This might happen # when there are no subnets yet created so metadata port doesn't have # an IP address. if not (port and port.mac and port.external_ids.get(ovn_const.OVN_CIDRS_EXT_ID_KEY, None)): LOG.debug("There is no metadata port for datapath %s or it has no " "MAC or IP addresses configured, tearing the namespace " "down if needed", datapath) self.teardown_datapath(datapath) return # First entry of the mac field must be the MAC address. match = MAC_PATTERN.match(port.mac[0].split(' ')[0]) # If it is not, we can't provision the namespace. Tear it down if # needed and log the error. if not match: LOG.error("Metadata port for datapath %s doesn't have a MAC " "address, tearing the namespace down if needed", datapath) self.teardown_datapath(datapath) return mac = match.group() ip_addresses = set( port.external_ids[ovn_const.OVN_CIDRS_EXT_ID_KEY].split(' ')) ip_addresses.add(METADATA_DEFAULT_CIDR) metadata_port = MetadataPortInfo(mac, ip_addresses) # Create the VETH pair if it's not created. Also the add_veth function # will create the namespace for us. namespace = self._get_namespace_name(datapath) veth_name = self._get_veth_name(datapath) ip1 = ip_lib.IPDevice(veth_name[0]) if ip_lib.device_exists(veth_name[1], namespace): ip2 = ip_lib.IPDevice(veth_name[1], namespace) else: LOG.debug("Creating VETH %s in %s namespace", veth_name[1], namespace) # Might happen that the end in the root namespace exists even # though the other end doesn't. Make sure we delete it first if # that's the case. if ip1.exists(): ip1.link.delete() ip1, ip2 = ip_lib.IPWrapper().add_veth( veth_name[0], veth_name[1], namespace) # Make sure both ends of the VETH are up ip1.link.set_up() ip2.link.set_up() # Configure the MAC address. ip2.link.set_address(metadata_port.mac) dev_info = ip2.addr.list() # Configure the IP addresses on the VETH pair and remove those # that we no longer need. current_cidrs = {dev['cidr'] for dev in dev_info} for ipaddr in current_cidrs - metadata_port.ip_addresses: ip2.addr.delete(ipaddr) for ipaddr in metadata_port.ip_addresses - current_cidrs: # NOTE(dalvarez): metadata only works on IPv4. We're doing this # extra check here because it could be that the metadata port has # an IPv6 address if there's an IPv6 subnet with SLAAC in its # network. Neutron IPAM will autoallocate an IPv6 address for every # port in the network. if utils.get_ip_version(ipaddr) == 4: ip2.addr.add(ipaddr) # Configure the OVS port and add external_ids:iface-id so that it # can be tracked by OVN. self.ovs_idl.add_port(self.conf.ovs_integration_bridge, veth_name[0]).execute() self.ovs_idl.db_set( 'Interface', veth_name[0], ('external_ids', {'iface-id': port.logical_port})).execute() # Spawn metadata proxy if it's not already running. metadata_driver.MetadataDriver.spawn_monitored_metadata_proxy( self._process_monitor, namespace, METADATA_PORT, self.conf, network_id=datapath) self.update_chassis_metadata_networks(datapath) return namespace def ensure_all_networks_provisioned(self): """Ensure that all datapaths are provisioned. This function will make sure that all datapaths with ports bound to our chassis have its namespace, VETH pair and OVS port created and metadata proxy is up and running. :return: A list with the namespaces that are currently serving metadata """ # Retrieve all ports in our Chassis with type == '' ports = self.sb_idl.get_ports_on_chassis(self.chassis) datapaths = {str(p.datapath.uuid) for p in ports if p.type == ''} namespaces = [] # Make sure that all those datapaths are serving metadata for datapath in datapaths: netns = self.provision_datapath(datapath) if netns: namespaces.append(netns) return namespaces def update_chassis_metadata_networks(self, datapath, remove=False): """Update metadata networks hosted in this chassis. Add or remove a datapath from the list of current datapaths that we're currently serving metadata. """ current_dps = self.sb_idl.get_chassis_metadata_networks(self.chassis) updated = False if remove: if datapath in current_dps: current_dps.remove(datapath) updated = True else: if datapath not in current_dps: current_dps.append(datapath) updated = True if updated: with self.sb_idl.create_transaction(check_error=True) as txn: txn.add(self.sb_idl.set_chassis_metadata_networks( self.chassis, current_dps)) networking-ovn-4.0.0/networking_ovn/agent/metadata/ovsdb.py0000666000175100017510000000470013245511145024150 0ustar zuulzuul00000000000000# Copyright 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. from ovs.db import idl from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.schema.open_vswitch import impl_idl as idl_ovs import tenacity from networking_ovn.common import config from networking_ovn.ovsdb import impl_idl_ovn as idl_ovn from networking_ovn.ovsdb import ovsdb_monitor class MetadataAgentOvnSbIdl(ovsdb_monitor.OvnIdl): SCHEMA = 'OVN_Southbound' def __init__(self, events=None): connection_string = config.get_ovn_sb_connection() helper = self._get_ovsdb_helper(connection_string) tables = ('Chassis', 'Encap', 'Port_Binding', 'Datapath_Binding') for table in tables: helper.register_table(table) super(MetadataAgentOvnSbIdl, self).__init__( None, connection_string, helper) if events: self.notify_handler.watch_events(events) @tenacity.retry( wait=tenacity.wait_exponential(max=180), reraise=True) def _get_ovsdb_helper(self, connection_string): return idlutils.get_schema_helper(connection_string, self.SCHEMA) def start(self): ovsdb_monitor._check_and_set_ssl_files(self.SCHEMA) conn = connection.Connection( self, timeout=config.get_ovn_ovsdb_timeout()) return idl_ovn.OvsdbSbOvnIdl(conn) class MetadataAgentOvsIdl(object): def start(self): connection_string = config.cfg.CONF.ovs.ovsdb_connection helper = idlutils.get_schema_helper(connection_string, 'Open_vSwitch') tables = ('Open_vSwitch', 'Bridge', 'Port', 'Interface') for table in tables: helper.register_table(table) ovs_idl = idl.Idl(connection_string, helper) conn = connection.Connection( ovs_idl, timeout=config.cfg.CONF.ovs.ovsdb_connection_timeout) return idl_ovs.OvsdbIdl(conn) networking-ovn-4.0.0/networking_ovn/agent/metadata/driver.py0000666000175100017510000001661713245511145024340 0ustar zuulzuul00000000000000# Copyright 2017 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. import errno import grp import os import pwd from oslo_config import cfg from oslo_log import log as logging from neutron.agent.linux import external_process from neutron.common import exceptions from networking_ovn._i18n import _ LOG = logging.getLogger(__name__) METADATA_SERVICE_NAME = 'metadata-proxy' PROXY_CONFIG_DIR = "ovn-metadata-proxy" _HAPROXY_CONFIG_TEMPLATE = """ global log /dev/log local0 %(log_level)s user %(user)s group %(group)s maxconn 1024 pidfile %(pidfile)s daemon defaults log global mode http option httplog option dontlognull option http-server-close option forwardfor retries 3 timeout http-request 30s timeout connect 30s timeout client 32s timeout server 32s timeout http-keep-alive 30s listen listener bind 0.0.0.0:%(port)s server metadata %(unix_socket_path)s http-request add-header X-OVN-%(res_type)s-ID %(res_id)s """ class InvalidUserOrGroupException(Exception): pass class HaproxyConfigurator(object): def __init__(self, network_id, router_id, unix_socket_path, port, user, group, state_path, pid_file): self.network_id = network_id self.router_id = router_id if network_id is None and router_id is None: raise exceptions.NetworkIdOrRouterIdRequiredError() self.port = port self.user = user self.group = group self.state_path = state_path self.unix_socket_path = unix_socket_path self.pidfile = pid_file self.log_level = ( 'debug' if logging.is_debug_enabled(cfg.CONF) else 'info') def create_config_file(self): """Create the config file for haproxy.""" # Need to convert uid/gid into username/group try: username = pwd.getpwuid(int(self.user)).pw_name except (ValueError, KeyError): try: username = pwd.getpwnam(self.user).pw_name except KeyError: raise InvalidUserOrGroupException( _("Invalid user/uid: '%s'") % self.user) try: groupname = grp.getgrgid(int(self.group)).gr_name except (ValueError, KeyError): try: groupname = grp.getgrnam(self.group).gr_name except KeyError: raise InvalidUserOrGroupException( _("Invalid group/gid: '%s'") % self.group) cfg_info = { 'port': self.port, 'unix_socket_path': self.unix_socket_path, 'user': username, 'group': groupname, 'pidfile': self.pidfile, 'log_level': self.log_level } if self.network_id: cfg_info['res_type'] = 'Network' cfg_info['res_id'] = self.network_id else: cfg_info['res_type'] = 'Router' cfg_info['res_id'] = self.router_id haproxy_cfg = _HAPROXY_CONFIG_TEMPLATE % cfg_info LOG.debug("haproxy_cfg = %s", haproxy_cfg) cfg_dir = self.get_config_path(self.state_path) # uuid has to be included somewhere in the command line so that it can # be tracked by process_monitor. self.cfg_path = os.path.join(cfg_dir, "%s.conf" % cfg_info['res_id']) if not os.path.exists(cfg_dir): os.makedirs(cfg_dir) with open(self.cfg_path, "w") as cfg_file: cfg_file.write(haproxy_cfg) @staticmethod def get_config_path(state_path): return os.path.join(state_path or cfg.CONF.state_path, PROXY_CONFIG_DIR) @staticmethod def cleanup_config_file(uuid, state_path): """Delete config file created when metadata proxy was spawned.""" # Delete config file if it exists cfg_path = os.path.join( HaproxyConfigurator.get_config_path(state_path), "%s.conf" % uuid) try: os.unlink(cfg_path) except OSError as ex: # It can happen that this function is called but metadata proxy # was never spawned so its config file won't exist if ex.errno != errno.ENOENT: raise class MetadataDriver(object): monitors = {} @classmethod def _get_metadata_proxy_user_group(cls, conf): user = conf.metadata_proxy_user or str(os.geteuid()) group = conf.metadata_proxy_group or str(os.getegid()) return user, group @classmethod def _get_metadata_proxy_callback(cls, port, conf, network_id=None, router_id=None): def callback(pid_file): metadata_proxy_socket = conf.metadata_proxy_socket user, group = ( cls._get_metadata_proxy_user_group(conf)) haproxy = HaproxyConfigurator(network_id, router_id, metadata_proxy_socket, port, user, group, conf.state_path, pid_file) haproxy.create_config_file() proxy_cmd = ['haproxy', '-f', haproxy.cfg_path] return proxy_cmd return callback @classmethod def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf, network_id=None, router_id=None): uuid = network_id or router_id callback = cls._get_metadata_proxy_callback( port, conf, network_id=network_id, router_id=router_id) pm = cls._get_metadata_proxy_process_manager(uuid, conf, ns_name=ns_name, callback=callback) pm.enable() monitor.register(uuid, METADATA_SERVICE_NAME, pm) cls.monitors[router_id] = pm @classmethod def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf, ns_name): monitor.unregister(uuid, METADATA_SERVICE_NAME) pm = cls._get_metadata_proxy_process_manager(uuid, conf, ns_name=ns_name) pm.disable() # Delete metadata proxy config file HaproxyConfigurator.cleanup_config_file(uuid, cfg.CONF.state_path) cls.monitors.pop(uuid, None) @classmethod def _get_metadata_proxy_process_manager(cls, router_id, conf, ns_name=None, callback=None): return external_process.ProcessManager( conf=conf, uuid=router_id, namespace=ns_name, default_cmd_callback=callback) networking-ovn-4.0.0/networking_ovn/agent/metadata/__init__.py0000666000175100017510000000000013245511145024557 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/agent/metadata/server.py0000666000175100017510000001600613245511145024343 0ustar zuulzuul00000000000000# Copyright 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 hashlib import hmac import httplib2 from neutron.agent.linux import utils as agent_utils from neutron.conf.agent.metadata import config from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from oslo_config import cfg from oslo_log import log as logging from oslo_utils import encodeutils import six import six.moves.urllib.parse as urlparse import webob from networking_ovn._i18n import _ from networking_ovn.agent.metadata import ovsdb from networking_ovn.common import constants as ovn_const LOG = logging.getLogger(__name__) MODE_MAP = { config.USER_MODE: 0o644, config.GROUP_MODE: 0o664, config.ALL_MODE: 0o666, } class MetadataProxyHandler(object): def __init__(self, conf): self.conf = conf self.subscribe() def subscribe(self): registry.subscribe(self.post_fork_initialize, resources.PROCESS, events.AFTER_INIT) def post_fork_initialize(self, resource, event, trigger, payload=None): # We need to open a connection to OVN SouthBound database for # each worker so that we can process the metadata requests. self.sb_idl = ovsdb.MetadataAgentOvnSbIdl().start() @webob.dec.wsgify(RequestClass=webob.Request) def __call__(self, req): try: LOG.debug("Request: %s", req) instance_id, project_id = self._get_instance_and_project_id(req) if instance_id: return self._proxy_request(instance_id, project_id, req) else: return webob.exc.HTTPNotFound() except Exception: LOG.exception("Unexpected error.") msg = _('An unknown error has occurred. ' 'Please try your request again.') explanation = six.text_type(msg) return webob.exc.HTTPInternalServerError(explanation=explanation) def _get_instance_and_project_id(self, req): remote_address = req.headers.get('X-Forwarded-For') network_id = req.headers.get('X-OVN-Network-ID') ports = self.sb_idl.get_network_port_bindings_by_ip(network_id, remote_address) if len(ports) == 1: external_ids = ports[0].external_ids return (external_ids[ovn_const.OVN_DEVID_EXT_ID_KEY], external_ids[ovn_const.OVN_PROJID_EXT_ID_KEY]) return None, None def _proxy_request(self, instance_id, tenant_id, req): headers = { 'X-Forwarded-For': req.headers.get('X-Forwarded-For'), 'X-Instance-ID': str(instance_id), 'X-Tenant-ID': str(tenant_id), 'X-Instance-ID-Signature': self._sign_instance_id(instance_id) } nova_host_port = '%s:%s' % (self.conf.nova_metadata_host, self.conf.nova_metadata_port) LOG.debug('Request to Nova at %s', nova_host_port) LOG.debug(headers) url = urlparse.urlunsplit(( self.conf.nova_metadata_protocol, nova_host_port, req.path_info, req.query_string, '')) h = httplib2.Http( ca_certs=self.conf.auth_ca_cert, disable_ssl_certificate_validation=self.conf.nova_metadata_insecure ) if self.conf.nova_client_cert and self.conf.nova_client_priv_key: h.add_certificate(self.conf.nova_client_priv_key, self.conf.nova_client_cert, nova_host_port) resp, content = h.request(url, method=req.method, headers=headers, body=req.body) if resp.status == 200: req.response.content_type = resp['content-type'] req.response.body = content LOG.debug(str(resp)) return req.response elif resp.status == 403: LOG.warning( 'The remote metadata server responded with Forbidden. This ' 'response usually occurs when shared secrets do not match.' ) return webob.exc.HTTPForbidden() elif resp.status == 400: return webob.exc.HTTPBadRequest() elif resp.status == 404: return webob.exc.HTTPNotFound() elif resp.status == 409: return webob.exc.HTTPConflict() elif resp.status == 500: msg = _( 'Remote metadata server experienced an internal server error.' ) LOG.debug(msg) explanation = six.text_type(msg) return webob.exc.HTTPInternalServerError(explanation=explanation) else: raise Exception(_('Unexpected response code: %s') % resp.status) def _sign_instance_id(self, instance_id): secret = self.conf.metadata_proxy_shared_secret secret = encodeutils.to_utf8(secret) instance_id = encodeutils.to_utf8(instance_id) return hmac.new(secret, instance_id, hashlib.sha256).hexdigest() class UnixDomainMetadataProxy(object): def __init__(self, conf): self.conf = conf agent_utils.ensure_directory_exists_without_file( cfg.CONF.metadata_proxy_socket) def _get_socket_mode(self): mode = self.conf.metadata_proxy_socket_mode if mode == config.DEDUCE_MODE: user = self.conf.metadata_proxy_user if (not user or user == '0' or user == 'root' or agent_utils.is_effective_user(user)): # user is agent effective user or root => USER_MODE mode = config.USER_MODE else: group = self.conf.metadata_proxy_group if not group or agent_utils.is_effective_group(group): # group is agent effective group => GROUP_MODE mode = config.GROUP_MODE else: # otherwise => ALL_MODE mode = config.ALL_MODE return MODE_MAP[mode] def run(self): self.server = agent_utils.UnixDomainWSGIServer( 'networking-ovn-metadata-agent') self.server.start(MetadataProxyHandler(self.conf), self.conf.metadata_proxy_socket, workers=self.conf.metadata_workers, backlog=self.conf.metadata_backlog, mode=self._get_socket_mode()) def wait(self): self.server.wait() networking-ovn-4.0.0/networking_ovn/agent/metadata_agent.py0000666000175100017510000000246713245511145024221 0ustar zuulzuul00000000000000# Copyright 2017 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 sys from neutron.common import config from neutron.common import utils from oslo_config import cfg from oslo_log import log as logging from networking_ovn.agent.metadata import agent from networking_ovn.conf.agent.metadata import config as meta LOG = logging.getLogger(__name__) def main(): meta.register_meta_conf_opts(meta.SHARED_OPTS) meta.register_meta_conf_opts(meta.UNIX_DOMAIN_METADATA_PROXY_OPTS) meta.register_meta_conf_opts(meta.METADATA_PROXY_HANDLER_OPTS) meta.register_meta_conf_opts(meta.OVS_OPTS, group='ovs') config.init(sys.argv[1:]) config.setup_logging() meta.setup_privsep() utils.log_opt_values(LOG) agt = agent.MetadataAgent(cfg.CONF) agt.start() networking-ovn-4.0.0/networking_ovn/agent/__init__.py0000666000175100017510000000000013245511145022777 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/l3/0000775000175100017510000000000013245511554020122 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/l3/l3_ovn_scheduler.py0000666000175100017510000001001713245511145023727 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import random from oslo_log import log import six from networking_ovn.common import config as ovn_config from networking_ovn.common import constants as ovn_const LOG = log.getLogger(__name__) OVN_SCHEDULER_CHANCE = 'chance' OVN_SCHEDULER_LEAST_LOADED = 'leastloaded' MAX_GW_CHASSIS = 5 @six.add_metaclass(abc.ABCMeta) class OVNGatewayScheduler(object): def __init__(self): pass @abc.abstractmethod def select(self, nb_idl, sb_idl, gateway_name, candidates=None): """Schedule the gateway port of a router to an OVN chassis. Schedule the gateway router port only if it is not already scheduled. """ pass def _schedule_gateway(self, nb_idl, sb_idl, gateway_name, candidates): existing_chassis = nb_idl.get_gateway_chassis_binding(gateway_name) candidates = candidates or self._get_chassis_candidates(sb_idl) # if no candidates or all chassis in existing_chassis also present # in candidates, then return existing_chassis # TODO(anilvenkata): If more candidates avaialable, then schedule # on them also? if existing_chassis and (not candidates or not (set(existing_chassis) - set(candidates))): return existing_chassis if not candidates: return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] # The actual binding of the gateway to a chassis via the options # column or gateway_chassis column in the OVN_Northbound is done # by the caller chassis = self._select_gateway_chassis( nb_idl, candidates)[:MAX_GW_CHASSIS] LOG.debug("Gateway %s scheduled on chassis %s", gateway_name, chassis) return chassis @abc.abstractmethod def _select_gateway_chassis(self, nb_idl, candidates): """Choose a chassis from candidates based on a specific policy.""" pass def _get_chassis_candidates(self, sb_idl): # TODO(azbiswas): Allow selection of a specific type of chassis when # the upstream code merges. # return sb_idl.get_all_chassis('gateway_router') or \ # sb_idl.get_all_chassis() return sb_idl.get_all_chassis() class OVNGatewayChanceScheduler(OVNGatewayScheduler): """Randomly select an chassis for a gateway port of a router""" def select(self, nb_idl, sb_idl, gateway_name, candidates=None): return self._schedule_gateway(nb_idl, sb_idl, gateway_name, candidates) def _select_gateway_chassis(self, nb_idl, candidates): candidates = copy.deepcopy(candidates) random.shuffle(candidates) return candidates class OVNGatewayLeastLoadedScheduler(OVNGatewayScheduler): """Select the least loaded chassis for a gateway port of a router""" def select(self, nb_idl, sb_idl, gateway_name, candidates=None): return self._schedule_gateway(nb_idl, sb_idl, gateway_name, candidates) def _select_gateway_chassis(self, nb_idl, candidates): chassis_bindings = nb_idl.get_all_chassis_gateway_bindings(candidates) # Sort on the length of the values in the returned dictionary return [k for k, v in sorted(chassis_bindings.items(), key=lambda x: len(x[1]))] OVN_SCHEDULER_STR_TO_CLASS = { OVN_SCHEDULER_CHANCE: OVNGatewayChanceScheduler, OVN_SCHEDULER_LEAST_LOADED: OVNGatewayLeastLoadedScheduler } def get_scheduler(): return OVN_SCHEDULER_STR_TO_CLASS[ovn_config.get_ovn_l3_scheduler()]() networking-ovn-4.0.0/networking_ovn/l3/__init__.py0000666000175100017510000000000013245511145022217 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/l3/l3_ovn.py0000666000175100017510000004367413245511145021710 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import l3 from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as n_const from neutron_lib import context as n_context from neutron_lib import exceptions as n_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.services import base as service_base from oslo_log import log from oslo_utils import excutils from neutron.db import common_db_mixin from neutron.db import dns_db from neutron.db import extraroute_db from neutron.db import l3_gwmode_db from neutron.db.models import l3 as l3_models from neutron.quota import resource_registry from networking_ovn.common import constants as ovn_const from networking_ovn.common import extensions from networking_ovn.common import ovn_client from networking_ovn.common import utils from networking_ovn.db import revision as db_rev from networking_ovn.l3 import l3_ovn_scheduler from networking_ovn.ovsdb import impl_idl_ovn LOG = log.getLogger(__name__) @registry.has_registry_receivers class OVNL3RouterPlugin(service_base.ServicePluginBase, common_db_mixin.CommonDbMixin, extraroute_db.ExtraRoute_dbonly_mixin, l3_gwmode_db.L3_NAT_db_mixin, dns_db.DNSDbMixin): """Implementation of the OVN L3 Router Service Plugin. This class implements a L3 service plugin that provides router and floatingip resources and manages associated request/response. """ supported_extension_aliases = \ extensions.ML2_SUPPORTED_API_EXTENSIONS_OVN_L3 @resource_registry.tracked_resources(router=l3_models.Router, floatingip=l3_models.FloatingIP) def __init__(self): LOG.info("Starting OVNL3RouterPlugin") super(OVNL3RouterPlugin, self).__init__() self._nb_ovn_idl = None self._sb_ovn_idl = None self._plugin_property = None self._ovn_client_inst = None self.scheduler = l3_ovn_scheduler.get_scheduler() self._register_precommit_callbacks() def _register_precommit_callbacks(self): registry.subscribe( self.create_router_precommit, resources.ROUTER, events.PRECOMMIT_CREATE) registry.subscribe( self.create_floatingip_precommit, resources.FLOATING_IP, events.PRECOMMIT_CREATE) @property def _ovn_client(self): if self._ovn_client_inst is None: self._ovn_client_inst = ovn_client.OVNClient(self._ovn, self._sb_ovn) return self._ovn_client_inst @property def _ovn(self): if self._nb_ovn_idl is None: LOG.info("Getting OvsdbNbOvnIdl") conn = impl_idl_ovn.get_connection(impl_idl_ovn.OvsdbNbOvnIdl) self._nb_ovn_idl = impl_idl_ovn.OvsdbNbOvnIdl(conn) return self._nb_ovn_idl @property def _sb_ovn(self): if self._sb_ovn_idl is None: LOG.info("Getting OvsdbSbOvnIdl") conn = impl_idl_ovn.get_connection(impl_idl_ovn.OvsdbSbOvnIdl) self._sb_ovn_idl = impl_idl_ovn.OvsdbSbOvnIdl(conn) return self._sb_ovn_idl @property def _plugin(self): if self._plugin_property is None: self._plugin_property = directory.get_plugin() return self._plugin_property def get_plugin_type(self): return plugin_constants.L3 def get_plugin_description(self): """returns string description of the plugin.""" return ("L3 Router Service Plugin for basic L3 forwarding" " using OVN") def create_router_precommit(self, resource, event, trigger, context, router, router_id, router_db): db_rev.create_initial_revision( router_id, ovn_const.TYPE_ROUTERS, context.session) def create_router(self, context, router): router = super(OVNL3RouterPlugin, self).create_router(context, router) try: self._ovn_client.create_router(router) except Exception: with excutils.save_and_reraise_exception(): # Delete the logical router LOG.error('Unable to create lrouter for %s', router['id']) super(OVNL3RouterPlugin, self).delete_router(context, router['id']) return router def update_router(self, context, id, router): original_router = self.get_router(context, id) result = super(OVNL3RouterPlugin, self).update_router(context, id, router) try: self._ovn_client.update_router(result, original_router) except Exception: with excutils.save_and_reraise_exception(): LOG.exception('Unable to update lrouter for %s', id) revert_router = {'router': original_router} super(OVNL3RouterPlugin, self).update_router(context, id, revert_router) return result def delete_router(self, context, id): original_router = self.get_router(context, id) super(OVNL3RouterPlugin, self).delete_router(context, id) try: self._ovn_client.delete_router(id) except Exception: with excutils.save_and_reraise_exception(): super(OVNL3RouterPlugin, self).create_router( context, {'router': original_router}) def add_router_interface(self, context, router_id, interface_info): router_interface_info = \ super(OVNL3RouterPlugin, self).add_router_interface( context, router_id, interface_info) port = self._plugin.get_port(context, router_interface_info['port_id']) multi_prefix = False if (len(router_interface_info['subnet_ids']) == 1 and len(port['fixed_ips']) > 1): # NOTE(lizk) It's adding a subnet onto an already existing router # interface port, try to update lrouter port 'networks' column. self._ovn_client.update_router_port(port, bump_db_rev=False) multi_prefix = True else: self._ovn_client.create_router_port(router_id, port) router = self.get_router(context, router_id) if not router.get(l3.EXTERNAL_GW_INFO): db_rev.bump_revision(port, ovn_const.TYPE_ROUTER_PORTS) return router_interface_info cidr = None for fixed_ip in port['fixed_ips']: subnet = self._plugin.get_subnet(context, fixed_ip['subnet_id']) if multi_prefix: if 'subnet_id' in interface_info: if subnet['id'] is not interface_info['subnet_id']: continue if subnet['ip_version'] == 4: cidr = subnet['cidr'] if utils.is_snat_enabled(router) and cidr: try: self._ovn_client.update_nat_rules(router, networks=[cidr], enable_snat=True) except Exception: with excutils.save_and_reraise_exception(): self._ovn.delete_lrouter_port( utils.ovn_lrouter_port_name(port['id']), utils.ovn_name(router_id)).execute(check_error=True) super(OVNL3RouterPlugin, self).remove_router_interface( context, router_id, router_interface_info) LOG.error('Error updating snat for subnet %(subnet)s in ' 'router %(router)s', {'subnet': router_interface_info['subnet_id'], 'router': router_id}) db_rev.bump_revision(port, ovn_const.TYPE_ROUTER_PORTS) return router_interface_info def remove_router_interface(self, context, router_id, interface_info): router_interface_info = \ super(OVNL3RouterPlugin, self).remove_router_interface( context, router_id, interface_info) router = self.get_router(context, router_id) port_id = router_interface_info['port_id'] multi_prefix = False port_removed = False try: port = self._plugin.get_port(context, port_id) # The router interface port still exists, call ovn to update it. self._ovn_client.update_router_port(port, bump_db_rev=False) multi_prefix = True except n_exc.PortNotFound: # The router interface port doesn't exist any more, # we will call ovn to delete it once we remove the snat # rules in the router itself if we have to port_removed = True if not router.get(l3.EXTERNAL_GW_INFO): if port_removed: self._ovn_client.delete_router_port(port_id, router_id) return router_interface_info try: cidr = None if multi_prefix: subnet = self._plugin.get_subnet(context, interface_info['subnet_id']) if subnet['ip_version'] == 4: cidr = subnet['cidr'] else: subnet_ids = router_interface_info.get('subnet_ids') for subnet_id in subnet_ids: subnet = self._plugin.get_subnet(context, subnet_id) if subnet['ip_version'] == 4: cidr = subnet['cidr'] break if utils.is_snat_enabled(router) and cidr: self._ovn_client.update_nat_rules( router, networks=[cidr], enable_snat=False) except Exception: with excutils.save_and_reraise_exception(): super(OVNL3RouterPlugin, self).add_router_interface( context, router_id, interface_info) LOG.error('Error is deleting snat') # NOTE(mangelajo): If the port doesn't exist anymore, we delete the # router port as the last operation and update the revision database # to ensure consistency if port_removed: self._ovn_client.delete_router_port(port_id, router_id) else: # otherwise, we just update the revision database db_rev.bump_revision(port, ovn_const.TYPE_ROUTER_PORTS) return router_interface_info def create_floatingip_precommit(self, resource, event, trigger, context, floatingip, floatingip_id, floatingip_db): db_rev.create_initial_revision( floatingip_id, ovn_const.TYPE_FLOATINGIPS, context.session) def create_floatingip(self, context, floatingip, initial_status=n_const.FLOATINGIP_STATUS_DOWN): fip = super(OVNL3RouterPlugin, self).create_floatingip( context, floatingip, initial_status) self._ovn_client.create_floatingip(fip) return fip def delete_floatingip(self, context, id): # TODO(lucasagomes): Passing ``original_fip`` object as a # parameter to the OVNClient's delete_floatingip() method is done # for backward-compatible reasons. Remove it in the Rocky release # of OpenStack. original_fip = self.get_floatingip(context, id) super(OVNL3RouterPlugin, self).delete_floatingip(context, id) self._ovn_client.delete_floatingip(id, fip_object=original_fip) def update_floatingip(self, context, id, floatingip): # TODO(lucasagomes): Passing ``original_fip`` object as a # parameter to the OVNClient's update_floatingip() method is done # for backward-compatible reasons. Remove it in the Rocky release # of OpenStack. original_fip = self.get_floatingip(context, id) fip = super(OVNL3RouterPlugin, self).update_floatingip(context, id, floatingip) self._ovn_client.update_floatingip(fip, fip_object=original_fip) return fip def update_floatingip_status(self, context, floatingip_id, status): fip = super(OVNL3RouterPlugin, self).update_floatingip_status( context, floatingip_id, status) self._ovn_client.update_floatingip_status(fip) return fip def disassociate_floatingips(self, context, port_id, do_notify=True): fips = self.get_floatingips(context.elevated(), filters={'port_id': [port_id]}) router_ids = super(OVNL3RouterPlugin, self).disassociate_floatingips( context, port_id, do_notify) for fip in fips: router_id = fip.get('router_id') fixed_ip_address = fip.get('fixed_ip_address') if router_id and fixed_ip_address: update_fip = {'logical_ip': fixed_ip_address, 'external_ip': fip['floating_ip_address']} try: self._ovn_client.disassociate_floatingip(update_fip, router_id) self.update_floatingip_status( context, fip['id'], n_const.FLOATINGIP_STATUS_DOWN) except Exception as e: LOG.error('Error in disassociating floatingip %(id)s: ' '%(error)s', {'id': fip['id'], 'error': e}) return router_ids def _get_gateway_port_physnet_mapping(self): # This function returns all gateway ports with corresponding # external network's physnet net_physnet_dict = {} port_physnet_dict = {} l3plugin = directory.get_plugin(plugin_constants.L3) if not l3plugin: return port_physnet_dict context = n_context.get_admin_context() for net in l3plugin._plugin.get_networks( context, {external_net.EXTERNAL: [True]}): if net.get(pnet.NETWORK_TYPE) in [n_const.TYPE_FLAT, n_const.TYPE_VLAN]: net_physnet_dict[net['id']] = net.get(pnet.PHYSICAL_NETWORK) for port in l3plugin._plugin.get_ports(context, filters={ 'device_owner': [n_const.DEVICE_OWNER_ROUTER_GW]}): port_physnet_dict[port['id']] = net_physnet_dict.get( port['network_id']) return port_physnet_dict def schedule_unhosted_gateways(self): port_physnet_dict = self._get_gateway_port_physnet_mapping() chassis_physnets = self._sb_ovn.get_chassis_and_physnets() unhosted_gateways = self._ovn.get_unhosted_gateways( port_physnet_dict, chassis_physnets) with self._ovn.transaction(check_error=True) as txn: for g_name in unhosted_gateways: physnet = port_physnet_dict.get(g_name[len('lrp-'):]) candidates = [chassis for chassis, physnets in chassis_physnets.items() if physnet and physnet in physnets] chassis = self.scheduler.select( self._ovn, self._sb_ovn, g_name, candidates=candidates) txn.add(self._ovn.update_lrouter_port( g_name, gateway_chassis=chassis)) @staticmethod @registry.receives(resources.SUBNET, [events.AFTER_UPDATE]) def _subnet_update(resource, event, trigger, **kwargs): l3plugin = directory.get_plugin(plugin_constants.L3) if not l3plugin: return context = kwargs['context'] orig = kwargs['original_subnet'] current = kwargs['subnet'] orig_gw_ip = orig['gateway_ip'] current_gw_ip = current['gateway_ip'] if orig_gw_ip == current_gw_ip: return gw_ports = l3plugin._plugin.get_ports(context, filters={ 'network_id': [orig['network_id']], 'device_owner': [n_const.DEVICE_OWNER_ROUTER_GW], 'fixed_ips': {'subnet_id': [orig['id']]}, }) router_ids = set([port['device_id'] for port in gw_ports]) remove = [{'destination': '0.0.0.0/0', 'nexthop': orig_gw_ip} ] if orig_gw_ip else [] add = [{'destination': '0.0.0.0/0', 'nexthop': current_gw_ip} ] if current_gw_ip else [] with l3plugin._ovn.transaction(check_error=True) as txn: for router_id in router_ids: l3plugin._ovn_client.update_router_routes( context, router_id, add, remove, txn=txn) @staticmethod @registry.receives(resources.PORT, [events.AFTER_UPDATE]) def _port_update(resource, event, trigger, **kwargs): l3plugin = directory.get_plugin(plugin_constants.L3) if not l3plugin: return current = kwargs['port'] if utils.is_lsp_router_port(current): # We call the update_router port with if_exists, because neutron, # internally creates the port, and then calls update, which will # trigger this callback even before we had the chance to create # the OVN NB DB side l3plugin._ovn_client.update_router_port(current, if_exists=True) networking-ovn-4.0.0/networking_ovn/cmd/0000775000175100017510000000000013245511554020347 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/cmd/neutron_ovn_db_sync_util.py0000666000175100017510000001706613245511145026043 0ustar zuulzuul00000000000000# 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. from neutron_lib.plugins import directory from oslo_config import cfg from oslo_db import options as db_options from oslo_log import log as logging from neutron.common import topics from neutron.conf.agent import securitygroups_rpc from neutron import manager from neutron import opts as neutron_options from neutron.plugins.ml2 import plugin as ml2_plugin from networking_ovn.common import config as ovn_config from networking_ovn.common import ovn_client from networking_ovn.ml2 import mech_driver from networking_ovn import ovn_db_sync from networking_ovn.ovsdb import impl_idl_ovn LOG = logging.getLogger(__name__) class Ml2Plugin(ml2_plugin.Ml2Plugin): def _setup_dhcp(self): pass def _start_rpc_notifiers(self): # Override the notifier so that when calling the ML2 plugin to create # resources, it doesn't crash trying to notify subscribers. self.notifier = AgentNotifierApi(topics.AGENT) class OVNMechanismDriver(mech_driver.OVNMechanismDriver): def subscribe(self): pass def post_fork_initialize(self, resource, event, trigger, **kwargs): pass @property def ovn_client(self): if not self._ovn_client: self._ovn_client = ovn_client.OVNClient(self._nb_ovn, self._sb_ovn) return self._ovn_client # Since we are not using the networking_ovn mechanism driver while syncing, # we override the post and pre commit methods so that original ones are # not called. def create_port_precommit(self, context): pass def create_port_postcommit(self, context): port = context.current self.ovn_client.create_port(port) def update_port_precommit(self, context): pass def update_port_postcommit(self, context): port = context.current original_port = context.original self.ovn_client.update_port(port, original_port) def delete_port_precommit(self, context): pass def delete_port_postcommit(self, context): port = context.current self.ovn_client.delete_port(port) class AgentNotifierApi(object): """Default Agent Notifier class for ovn-db-sync-util. This class implements empty methods so that when creating resources in the core plugin, the original ones don't get called and don't interfere with the syncing process. """ def __init__(self, topic): self.topic = topic self.topic_network_delete = topics.get_topic_name(topic, topics.NETWORK, topics.DELETE) self.topic_port_update = topics.get_topic_name(topic, topics.PORT, topics.UPDATE) self.topic_port_delete = topics.get_topic_name(topic, topics.PORT, topics.DELETE) self.topic_network_update = topics.get_topic_name(topic, topics.NETWORK, topics.UPDATE) def network_delete(self, context, network_id): pass def port_update(self, context, port, network_type, segmentation_id, physical_network): pass def port_delete(self, context, port_id): pass def network_update(self, context, network): pass def security_groups_provider_updated(self, context, devices_to_udpate=None): pass def setup_conf(): conf = cfg.CONF ml2_group, ml2_opts = neutron_options.list_ml2_conf_opts()[0] cfg.CONF.register_cli_opts(ml2_opts, ml2_group) cfg.CONF.register_cli_opts(securitygroups_rpc.security_group_opts, 'SECURITYGROUP') ovn_group, ovn_opts = ovn_config.list_opts()[0] cfg.CONF.register_cli_opts(ovn_opts, group=ovn_group) db_group, neutron_db_opts = db_options.list_opts()[0] cfg.CONF.register_cli_opts(neutron_db_opts, db_group) return conf def main(): """Main method for syncing neutron networks and ports with ovn nb db. The utility syncs neutron db with ovn nb db. """ conf = setup_conf() # if no config file is passed or no configuration options are passed # then load configuration from /etc/neutron/neutron.conf try: conf(project='neutron') except TypeError: LOG.error('Error parsing the configuration values. Please verify.') return logging.setup(conf, 'neutron_ovn_db_sync_util') LOG.info('Started Neutron OVN db sync') mode = ovn_config.get_ovn_neutron_sync_mode() if mode not in [ovn_db_sync.SYNC_MODE_LOG, ovn_db_sync.SYNC_MODE_REPAIR]: LOG.error( 'Invalid sync mode : ["%s"]. Should be "log" or "repair"', mode) return # Validate and modify core plugin and ML2 mechanism drivers for syncing. if (cfg.CONF.core_plugin.endswith('.Ml2Plugin') or cfg.CONF.core_plugin == 'ml2'): cfg.CONF.core_plugin = ( 'networking_ovn.cmd.neutron_ovn_db_sync_util.Ml2Plugin') if not cfg.CONF.ml2.mechanism_drivers: LOG.error('please use --config-file to specify ' 'neutron and ml2 configuration file.') return if 'ovn' not in cfg.CONF.ml2.mechanism_drivers: LOG.error('No "ovn" mechanism driver found : "%s".', cfg.CONF.ml2.mechanism_drivers) return cfg.CONF.set_override('mechanism_drivers', ['ovn-sync'], 'ml2') conf.service_plugins = ['networking_ovn.l3.l3_ovn.OVNL3RouterPlugin'] else: LOG.error('Invalid core plugin : ["%s"].', cfg.CONF.core_plugin) return try: conn = impl_idl_ovn.get_connection(impl_idl_ovn.OvsdbNbOvnIdl) ovn_api = impl_idl_ovn.OvsdbNbOvnIdl(conn) except RuntimeError: LOG.error('Invalid --ovn-ovn_nb_connection parameter provided.') return try: sb_conn = impl_idl_ovn.get_connection(impl_idl_ovn.OvsdbSbOvnIdl) ovn_sb_api = impl_idl_ovn.OvsdbSbOvnIdl(sb_conn) except RuntimeError: LOG.error('Invalid --ovn-ovn_sb_connection parameter provided.') return manager.init() core_plugin = directory.get_plugin() ovn_driver = core_plugin.mechanism_manager.mech_drivers['ovn-sync'].obj ovn_driver._nb_ovn = ovn_api ovn_driver._sb_ovn = ovn_sb_api synchronizer = ovn_db_sync.OvnNbSynchronizer( core_plugin, ovn_api, ovn_sb_api, mode, ovn_driver) LOG.info('Sync for Northbound db started with mode : %s', mode) synchronizer.do_sync() LOG.info('Sync completed for Northbound db') sb_synchronizer = ovn_db_sync.OvnSbSynchronizer( core_plugin, ovn_sb_api, ovn_driver) LOG.info('Sync for Southbound db started with mode : %s', mode) sb_synchronizer.do_sync() LOG.info('Sync completed for Southbound db') networking-ovn-4.0.0/networking_ovn/cmd/eventlet/0000775000175100017510000000000013245511554022175 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/cmd/eventlet/__init__.py0000666000175100017510000000120613245511145024303 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 neutron.common import eventlet_utils eventlet_utils.monkey_patch() networking-ovn-4.0.0/networking_ovn/cmd/eventlet/agents/0000775000175100017510000000000013245511554023456 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/cmd/eventlet/agents/__init__.py0000666000175100017510000000000013245511145025553 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/cmd/eventlet/agents/metadata.py0000666000175100017510000000122513245511145025606 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 networking_ovn.agent import metadata_agent def main(): metadata_agent.main() networking-ovn-4.0.0/networking_ovn/cmd/__init__.py0000666000175100017510000000000013245511145022444 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/ovsdb/0000775000175100017510000000000013245511554020721 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/ovsdb/ovsdb_monitor.py0000666000175100017510000002642513245511145024166 0ustar zuulzuul00000000000000# 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. from neutron.common import config from neutron_lib.plugins import constants from neutron_lib.plugins import directory from neutron_lib.utils import helpers from neutron_lib import worker from oslo_log import log from ovs.stream import Stream from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import event as row_event from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp import event from networking_ovn.common import config as ovn_config from networking_ovn.common import utils LOG = log.getLogger(__name__) class ChassisEvent(row_event.RowEvent): """Chassis create update delete event.""" def __init__(self, driver): self.driver = driver self.l3_plugin = directory.get_plugin(constants.L3) table = 'Chassis' events = (self.ROW_CREATE, self.ROW_UPDATE, self.ROW_DELETE) super(ChassisEvent, self).__init__(events, table, None) self.event_name = 'ChassisEvent' def run(self, event, row, old): host = row.hostname phy_nets = [] if event != self.ROW_DELETE: bridge_mappings = row.external_ids.get('ovn-bridge-mappings', '') mapping_dict = helpers.parse_mappings(bridge_mappings.split(','), unique_values=False) phy_nets = list(mapping_dict) self.driver.update_segment_host_mapping(host, phy_nets) if utils.is_ovn_l3(self.l3_plugin): self.l3_plugin.schedule_unhosted_gateways() class LogicalSwitchPortCreateUpEvent(row_event.RowEvent): """Row create event - Logical_Switch_Port 'up' = True. On connection, we get a dump of all ports, so if there is a neutron port that is down that has since been activated, we'll catch it here. This event will not be generated for new ports getting created. """ def __init__(self, driver): self.driver = driver table = 'Logical_Switch_Port' events = (self.ROW_CREATE) super(LogicalSwitchPortCreateUpEvent, self).__init__( events, table, (('up', '=', True),)) self.event_name = 'LogicalSwitchPortCreateUpEvent' def run(self, event, row, old): self.driver.set_port_status_up(row.name) class LogicalSwitchPortCreateDownEvent(row_event.RowEvent): """Row create event - Logical_Switch_Port 'up' = False On connection, we get a dump of all ports, so if there is a neutron port that is up that has since been deactivated, we'll catch it here. This event will not be generated for new ports getting created. """ def __init__(self, driver): self.driver = driver table = 'Logical_Switch_Port' events = (self.ROW_CREATE) super(LogicalSwitchPortCreateDownEvent, self).__init__( events, table, (('up', '=', False),)) self.event_name = 'LogicalSwitchPortCreateDownEvent' def run(self, event, row, old): self.driver.set_port_status_down(row.name) class LogicalSwitchPortUpdateUpEvent(row_event.RowEvent): """Row update event - Logical_Switch_Port 'up' going from False to True This happens when the VM goes up. New value of Logical_Switch_Port 'up' will be True and the old value will be False. """ def __init__(self, driver): self.driver = driver table = 'Logical_Switch_Port' events = (self.ROW_UPDATE) super(LogicalSwitchPortUpdateUpEvent, self).__init__( events, table, (('up', '=', True),), old_conditions=(('up', '=', False),)) self.event_name = 'LogicalSwitchPortUpdateUpEvent' def run(self, event, row, old): self.driver.set_port_status_up(row.name) class LogicalSwitchPortUpdateDownEvent(row_event.RowEvent): """Row update event - Logical_Switch_Port 'up' going from True to False This happens when the VM goes down. New value of Logical_Switch_Port 'up' will be False and the old value will be True. """ def __init__(self, driver): self.driver = driver table = 'Logical_Switch_Port' events = (self.ROW_UPDATE) super(LogicalSwitchPortUpdateDownEvent, self).__init__( events, table, (('up', '=', False),), old_conditions=(('up', '=', True),)) self.event_name = 'LogicalSwitchPortUpdateDownEvent' def run(self, event, row, old): self.driver.set_port_status_down(row.name) class OvnDbNotifyHandler(event.RowEventHandler): def __init__(self, driver): super(OvnDbNotifyHandler, self).__init__() self.driver = driver class BaseOvnIdl(connection.OvsdbIdl): @classmethod def from_server(cls, connection_string, schema_name): _check_and_set_ssl_files(schema_name) helper = idlutils.get_schema_helper(connection_string, schema_name) helper.register_all() return cls(connection_string, helper) class BaseOvnSbIdl(connection.OvsdbIdl): @classmethod def from_server(cls, connection_string, schema_name): _check_and_set_ssl_files(schema_name) helper = idlutils.get_schema_helper(connection_string, schema_name) helper.register_table('Chassis') helper.register_table('Encap') if ovn_config.is_ovn_metadata_enabled(): helper.register_table('Port_Binding') helper.register_table('Datapath_Binding') return cls(connection_string, helper) class OvnIdl(BaseOvnIdl): def __init__(self, driver, remote, schema): super(OvnIdl, self).__init__(remote, schema) self.driver = driver self.notify_handler = OvnDbNotifyHandler(driver) # ovsdb lock name to acquire. # This event lock is used to handle the notify events sent by idl.Idl # idl.Idl will call notify function for the "update" rpc method it # receives from the ovsdb-server. # This event lock is required for the following reasons # - If there are multiple neutron servers running, OvnWorkers of # these neutron servers would receive the notify events from # idl.Idl # # - we do not want all the neutron servers to handle these events # # - only the neutron server which has the lock will handle the # notify events. # # - In case the neutron server which owns this lock goes down, # ovsdb server would assign the lock to one of the other neutron # servers. self.event_lock_name = "neutron_ovn_event_lock" def notify(self, event, row, updates=None): # Do not handle the notification if the event lock is requested, # but not granted by the ovsdb-server. if self.is_lock_contended: return self.notify_handler.notify(event, row, updates) def post_connect(self): """Should be called after the idl has been initialized""" pass class OvnNbIdl(OvnIdl): def __init__(self, driver, remote, schema): super(OvnNbIdl, self).__init__(driver, remote, schema) self._lsp_update_up_event = LogicalSwitchPortUpdateUpEvent(driver) self._lsp_update_down_event = LogicalSwitchPortUpdateDownEvent(driver) self._lsp_create_up_event = LogicalSwitchPortCreateUpEvent(driver) self._lsp_create_down_event = LogicalSwitchPortCreateDownEvent(driver) self.notify_handler.watch_events([self._lsp_create_up_event, self._lsp_create_down_event, self._lsp_update_up_event, self._lsp_update_down_event]) @classmethod def from_server(cls, connection_string, schema_name, driver): _check_and_set_ssl_files(schema_name) helper = idlutils.get_schema_helper(connection_string, schema_name) helper.register_all() _idl = cls(driver, connection_string, helper) _idl.set_lock(_idl.event_lock_name) return _idl def unwatch_logical_switch_port_create_events(self): """Unwatch the logical switch port create events. When the ovs idl client connects to the ovsdb-server, it gets a dump of all logical switch ports as events and we need to process them at start up. After the startup, there is no need to watch these events. So unwatch these events. """ self.notify_handler.unwatch_events([self._lsp_create_up_event, self._lsp_create_down_event]) self._lsp_create_up_event = None self._lsp_create_down_event = None def post_connect(self): self.unwatch_logical_switch_port_create_events() class OvnSbIdl(OvnIdl): @classmethod def from_server(cls, connection_string, schema_name, driver): _check_and_set_ssl_files(schema_name) helper = idlutils.get_schema_helper(connection_string, schema_name) helper.register_table('Chassis') helper.register_table('Encap') if ovn_config.is_ovn_metadata_enabled(): helper.register_table('Port_Binding') helper.register_table('Datapath_Binding') _idl = cls(driver, connection_string, helper) _idl.set_lock(_idl.event_lock_name) return _idl def post_connect(self): """Watch Chassis events. When the ovs idl client connects to the ovsdb-server, it gets a dump of all Chassis create event. We don't need to process them because there will be sync up at startup. After that, we will watch the events to make notify work. """ self._chassis_event = ChassisEvent(self.driver) self.notify_handler.watch_events([self._chassis_event]) def _check_and_set_ssl_files(schema_name): if schema_name == 'OVN_Southbound': priv_key_file = ovn_config.get_ovn_sb_private_key() cert_file = ovn_config.get_ovn_sb_certificate() ca_cert_file = ovn_config.get_ovn_sb_ca_cert() else: priv_key_file = ovn_config.get_ovn_nb_private_key() cert_file = ovn_config.get_ovn_nb_certificate() ca_cert_file = ovn_config.get_ovn_nb_ca_cert() if priv_key_file: Stream.ssl_set_private_key_file(priv_key_file) if cert_file: Stream.ssl_set_certificate_file(cert_file) if ca_cert_file: Stream.ssl_set_ca_cert_file(ca_cert_file) class OvnWorker(worker.BaseWorker): def start(self): super(OvnWorker, self).start() # NOTE(twilson) The super class will trigger the post_fork_initialize # in the driver, which starts the connection/IDL notify loop which # keeps the process from exiting def stop(self): """Stop service.""" # TODO(numans) def wait(self): """Wait for service to complete.""" # TODO(numans) @staticmethod def reset(): config.reset_service() networking-ovn-4.0.0/networking_ovn/ovsdb/impl_idl_ovn.py0000666000175100017510000010032113245511164023742 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 contextlib import uuid from neutron_lib import exceptions as n_exc from oslo_log import log import tenacity from neutron_lib.utils import helpers from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from ovsdbapp.backend.ovs_idl import transaction as idl_trans from ovsdbapp.backend.ovs_idl import vlog from ovsdbapp.schema.ovn_northbound import impl_idl as nb_impl_idl from ovsdbapp.schema.ovn_southbound import impl_idl as sb_impl_idl from networking_ovn._i18n import _ from networking_ovn.common import config as cfg from networking_ovn.common import constants as ovn_const from networking_ovn.common import exceptions as ovn_exc from networking_ovn.common import utils from networking_ovn.ovsdb import commands as cmd from networking_ovn.ovsdb import ovsdb_monitor LOG = log.getLogger(__name__) from ovsdbapp.backend import ovs_idl # This version of Backend doesn't use a class variable for ovsdb_connection # and therefor allows networking-ovn to manage connection scope on its own class Backend(ovs_idl.Backend): lookup_table = {} def __init__(self, connection): self.ovsdb_connection = connection super(Backend, self).__init__(connection) def start_connection(self, connection): try: self.ovsdb_connection.start() except Exception as e: connection_exception = OvsdbConnectionUnavailable( db_schema=self.schema, error=e) LOG.exception(connection_exception) raise connection_exception @property def idl(self): return self.ovsdb_connection.idl @property def tables(self): return self.idl.tables _tables = tables def is_table_present(self, table_name): return table_name in self._tables def is_col_present(self, table_name, col_name): return self.is_table_present(table_name) and ( col_name in self._tables[table_name].columns) def create_transaction(self, check_error=False, log_errors=True): return idl_trans.Transaction( self, self.ovsdb_connection, self.ovsdb_connection.timeout, check_error, log_errors) class OvsdbConnectionUnavailable(n_exc.ServiceUnavailable): message = _("OVS database connection to %(db_schema)s failed with error: " "'%(error)s'. Verify that the OVS and OVN services are " "available and that the 'ovn_nb_connection' and " "'ovn_sb_connection' configuration options are correct.") # Retry forever to get the OVN NB and SB IDLs. Wait 2^x * 1 seconds between # each retry, up to 180 seconds, then 180 seconds afterwards. def get_ovn_idls(driver, trigger): @tenacity.retry( wait=tenacity.wait_exponential(max=180), reraise=True) def get_ovn_idl_retry(cls): LOG.info('Getting %(cls)s for %(trigger)s with retry', {'cls': cls.__name__, 'trigger': trigger.im_class.__name__}) return cls(get_connection(cls, trigger, driver)) vlog.use_python_logger(max_level=cfg.get_ovn_ovsdb_log_level()) return tuple(get_ovn_idl_retry(c) for c in (OvsdbNbOvnIdl, OvsdbSbOvnIdl)) def get_connection(db_class, trigger=None, driver=None): # The trigger is the start() method of the worker class if db_class == OvsdbNbOvnIdl: args = (cfg.get_ovn_nb_connection(), 'OVN_Northbound') cls = ovsdb_monitor.OvnNbIdl elif db_class == OvsdbSbOvnIdl: args = (cfg.get_ovn_sb_connection(), 'OVN_Southbound') cls = ovsdb_monitor.OvnSbIdl if trigger and trigger.im_class == ovsdb_monitor.OvnWorker: idl_ = cls.from_server(*args, driver=driver) else: if db_class == OvsdbSbOvnIdl: idl_ = ovsdb_monitor.BaseOvnSbIdl.from_server(*args) else: idl_ = ovsdb_monitor.BaseOvnIdl.from_server(*args) return connection.Connection(idl_, timeout=cfg.get_ovn_ovsdb_timeout()) class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend): def __init__(self, connection): super(OvsdbNbOvnIdl, self).__init__(connection) self.idl._session.reconnect.set_probe_interval( cfg.get_ovn_ovsdb_probe_interval()) @contextlib.contextmanager def transaction(self, *args, **kwargs): """A wrapper on the ovsdbapp transaction to work with revisions. This method is just a wrapper around the ovsdbapp transaction to handle revision conflicts correctly. """ try: with super(OvsdbNbOvnIdl, self).transaction(*args, **kwargs) as t: yield t except ovn_exc.RevisionConflict as e: LOG.info('Transaction aborted. Reason: %s', e) def set_lswitch_ext_ids(self, lswitch_id, ext_ids, if_exists=True): return cmd.LSwitchSetExternalIdsCommand(self, lswitch_id, ext_ids, if_exists) def create_lswitch_port(self, lport_name, lswitch_name, may_exist=True, **columns): return cmd.AddLSwitchPortCommand(self, lport_name, lswitch_name, may_exist, **columns) def set_lswitch_port(self, lport_name, if_exists=True, **columns): return cmd.SetLSwitchPortCommand(self, lport_name, if_exists, **columns) def delete_lswitch_port(self, lport_name=None, lswitch_name=None, ext_id=None, if_exists=True): if lport_name is not None: return cmd.DelLSwitchPortCommand(self, lport_name, lswitch_name, if_exists) else: raise RuntimeError(_("Currently only supports " "delete by lport-name")) def get_all_logical_switches_with_ports(self): result = [] for lswitch in self._tables['Logical_Switch'].rows.values(): if ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY not in ( lswitch.external_ids): continue ports = [] provnet_port = None for lport in getattr(lswitch, 'ports', []): if ovn_const.OVN_PORT_NAME_EXT_ID_KEY in lport.external_ids: ports.append(lport.name) # Handle provider network port elif lport.name.startswith( ovn_const.OVN_PROVNET_PORT_NAME_PREFIX): provnet_port = lport.name result.append({'name': lswitch.name, 'ports': ports, 'provnet_port': provnet_port}) return result def get_all_logical_routers_with_rports(self): """Get logical Router ports associated with all logical Routers @return: list of dict, each dict has key-value: - 'name': string router_id in neutron. - 'static_routes': list of static routes dict. - 'ports': dict of port_id in neutron (key) and networks on port (value). - 'snats': list of snats dict - 'dnat_and_snats': list of dnat_and_snats dict """ result = [] for lrouter in self._tables['Logical_Router'].rows.values(): if ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY not in ( lrouter.external_ids): continue lrports = {lrport.name.replace('lrp-', ''): lrport.networks for lrport in getattr(lrouter, 'ports', [])} sroutes = [{'destination': sroute.ip_prefix, 'nexthop': sroute.nexthop} for sroute in getattr(lrouter, 'static_routes', [])] dnat_and_snats = [] snat = [] for nat in getattr(lrouter, 'nat', []): columns = {'logical_ip': nat.logical_ip, 'external_ip': nat.external_ip, 'type': nat.type} if nat.type == 'dnat_and_snat': if nat.external_mac: columns['external_mac'] = nat.external_mac[0] if nat.logical_port: columns['logical_port'] = nat.logical_port[0] dnat_and_snats.append(columns) elif nat.type == 'snat': snat.append(columns) result.append({'name': lrouter.name.replace('neutron-', ''), 'static_routes': sroutes, 'ports': lrports, 'snats': snat, 'dnat_and_snats': dnat_and_snats}) return result def get_acl_by_id(self, acl_id): try: return self.lookup('ACL', uuid.UUID(acl_id)) except idlutils.RowNotFound: return def get_acls_for_lswitches(self, lswitch_names): """Get the existing set of acls that belong to the logical switches @param lswitch_names: List of logical switch names @type lswitch_names: [] @var acl_values_dict: A dictionary indexed by port_id containing the list of acl values in string format that belong to that port @var acl_obj_dict: A dictionary indexed by acl value containing the corresponding acl idl object. @var lswitch_ovsdb_dict: A dictionary mapping from logical switch name to lswitch idl object @return: (acl_values_dict, acl_obj_dict, lswitch_ovsdb_dict) """ acl_values_dict = {} acl_obj_dict = {} lswitch_ovsdb_dict = {} for lswitch_name in lswitch_names: try: lswitch = idlutils.row_by_value(self.idl, 'Logical_Switch', 'name', utils.ovn_name(lswitch_name)) except idlutils.RowNotFound: # It is possible for the logical switch to be deleted # while we are searching for it by name in idl. continue lswitch_ovsdb_dict[lswitch_name] = lswitch acls = getattr(lswitch, 'acls', []) # Iterate over each acl in a lswitch and store the acl in # a key:value representation for e.g. acl_string. This # key:value representation can invoke the code - # self._ovn.add_acl(**acl_string) for acl in acls: ext_ids = getattr(acl, 'external_ids', {}) port_id = ext_ids.get('neutron:lport') acl_list = acl_values_dict.setdefault(port_id, []) acl_string = {'lport': port_id, 'lswitch': utils.ovn_name(lswitch_name)} for acl_key in getattr(acl, "_data", {}): try: acl_string[acl_key] = getattr(acl, acl_key) except AttributeError: pass acl_obj_dict[str(acl_string)] = acl acl_list.append(acl_string) return acl_values_dict, acl_obj_dict, lswitch_ovsdb_dict def create_lrouter(self, name, may_exist=True, **columns): return cmd.AddLRouterCommand(self, name, may_exist, **columns) def update_lrouter(self, name, if_exists=True, **columns): return cmd.UpdateLRouterCommand(self, name, if_exists, **columns) def delete_lrouter(self, name, if_exists=True): return cmd.DelLRouterCommand(self, name, if_exists) def add_lrouter_port(self, name, lrouter, may_exist=False, **columns): return cmd.AddLRouterPortCommand(self, name, lrouter, may_exist, **columns) def update_lrouter_port(self, name, if_exists=True, **columns): return cmd.UpdateLRouterPortCommand(self, name, if_exists, **columns) def delete_lrouter_port(self, name, lrouter, if_exists=True): return cmd.DelLRouterPortCommand(self, name, lrouter, if_exists) def set_lrouter_port_in_lswitch_port(self, lswitch_port, lrouter_port, is_gw_port=False, if_exists=True): return cmd.SetLRouterPortInLSwitchPortCommand(self, lswitch_port, lrouter_port, is_gw_port, if_exists) def add_acl(self, lswitch, lport, **columns): return cmd.AddACLCommand(self, lswitch, lport, **columns) def delete_acl(self, lswitch, lport, if_exists=True): return cmd.DelACLCommand(self, lswitch, lport, if_exists) def update_acls(self, lswitch_names, port_list, acl_new_values_dict, need_compare=True, is_add_acl=True): return cmd.UpdateACLsCommand(self, lswitch_names, port_list, acl_new_values_dict, need_compare=need_compare, is_add_acl=is_add_acl) def add_static_route(self, lrouter, **columns): return cmd.AddStaticRouteCommand(self, lrouter, **columns) def delete_static_route(self, lrouter, ip_prefix, nexthop, if_exists=True): return cmd.DelStaticRouteCommand(self, lrouter, ip_prefix, nexthop, if_exists) def create_address_set(self, name, may_exist=True, **columns): return cmd.AddAddrSetCommand(self, name, may_exist, **columns) def delete_address_set(self, name, if_exists=True, **columns): return cmd.DelAddrSetCommand(self, name, if_exists) def update_address_set(self, name, addrs_add, addrs_remove, if_exists=True): return cmd.UpdateAddrSetCommand(self, name, addrs_add, addrs_remove, if_exists) def update_address_set_ext_ids(self, name, external_ids, if_exists=True): return cmd.UpdateAddrSetExtIdsCommand(self, name, external_ids, if_exists) def _get_logical_router_port_gateway_chassis(self, lrp): # Try retrieving gateway_chassis with new schema. If new schema is not # supported or user is using old schema, then use old schema for # getting gateway_chassis chassis = [] if self._tables.get('Gateway_Chassis'): for gwc in lrp.gateway_chassis: # TODO(anilvenkata): Add to the list based on priority. # Otherwise, if lrp1 is scheduled on c1 with priority 1 and c2 # with priority 2. When new port lrp2 is scheduled, it is also # scheduled on c1 with priority 1 and c2 with priority 2. # If we add to the list based on priority then it will be # scheduled on c1 with priority 2 and on c2 with priority 1. chassis.append(gwc.chassis_name) else: rc = lrp.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY) if rc: chassis.append(rc) return chassis def get_all_chassis_gateway_bindings(self, chassis_candidate_list=None): chassis_bindings = {} for chassis_name in chassis_candidate_list or []: chassis_bindings.setdefault(chassis_name, []) for lrp in self._tables['Logical_Router_Port'].rows.values(): if not lrp.name.startswith('lrp-'): continue chassis = self._get_logical_router_port_gateway_chassis(lrp) for chassis_name in chassis: if (not chassis_candidate_list or chassis_name in chassis_candidate_list): routers_hosted = chassis_bindings.setdefault(chassis_name, []) routers_hosted.append(lrp.name) return chassis_bindings def get_gateway_chassis_binding(self, gateway_name): try: lrp = idlutils.row_by_value( self.idl, 'Logical_Router_Port', 'name', gateway_name) return self._get_logical_router_port_gateway_chassis(lrp) except idlutils.RowNotFound: return [] def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets): unhosted_gateways = [] valid_chassis_list = list(chassis_physnets) for lrp in self._tables['Logical_Router_Port'].rows.values(): if not lrp.name.startswith('lrp-'): continue physnet = port_physnet_dict.get(lrp.name[len('lrp-'):]) for chassis_name in self._get_logical_router_port_gateway_chassis( lrp): # TODO(azbiswas): Handle the case when a chassis is no # longer valid. This may involve moving conntrack states, # so it needs to discussed in the OVN community first. if (chassis_name == ovn_const.OVN_GATEWAY_INVALID_CHASSIS or chassis_name not in valid_chassis_list or (physnet and physnet not in chassis_physnets.get(chassis_name))): unhosted_gateways.append(lrp.name) return unhosted_gateways def add_dhcp_options(self, subnet_id, port_id=None, may_exist=True, **columns): return cmd.AddDHCPOptionsCommand(self, subnet_id, port_id=port_id, may_exist=may_exist, **columns) def delete_dhcp_options(self, row_uuid, if_exists=True): return cmd.DelDHCPOptionsCommand(self, row_uuid, if_exists=if_exists) def _format_dhcp_row(self, row): ext_ids = dict(getattr(row, 'external_ids', {})) return {'cidr': row.cidr, 'options': dict(row.options), 'external_ids': ext_ids, 'uuid': row.uuid} def get_subnet_dhcp_options(self, subnet_id, with_ports=False): subnet = None ports = [] for row in self._tables['DHCP_Options'].rows.values(): external_ids = getattr(row, 'external_ids', {}) if subnet_id == external_ids.get('subnet_id'): port_id = external_ids.get('port_id') if with_ports and port_id: ports.append(self._format_dhcp_row(row)) elif not port_id: subnet = self._format_dhcp_row(row) if not with_ports: break return {'subnet': subnet, 'ports': ports} def get_subnets_dhcp_options(self, subnet_ids): ret_opts = [] for row in self._tables['DHCP_Options'].rows.values(): external_ids = getattr(row, 'external_ids', {}) if (external_ids.get('subnet_id') in subnet_ids and not external_ids.get('port_id')): ret_opts.append(self._format_dhcp_row(row)) if len(ret_opts) == len(subnet_ids): break return ret_opts def get_all_dhcp_options(self): dhcp_options = {'subnets': {}, 'ports_v4': {}, 'ports_v6': {}} for row in self._tables['DHCP_Options'].rows.values(): external_ids = getattr(row, 'external_ids', {}) if not external_ids.get('subnet_id'): # This row is not created by OVN ML2 driver. Ignore it. continue if not external_ids.get('port_id'): dhcp_options['subnets'][external_ids['subnet_id']] = ( self._format_dhcp_row(row)) else: port_dict = 'ports_v6' if ':' in row.cidr else 'ports_v4' dhcp_options[port_dict][external_ids['port_id']] = ( self._format_dhcp_row(row)) return dhcp_options def get_address_sets(self): address_sets = {} for row in self._tables['Address_Set'].rows.values(): # TODO(lucasagomes): Remove OVN_SG_NAME_EXT_ID_KEY in the # Rocky release if not (ovn_const.OVN_SG_EXT_ID_KEY in row.external_ids or ovn_const.OVN_SG_NAME_EXT_ID_KEY in row.external_ids): continue name = getattr(row, 'name') data = {} for row_key in getattr(row, "_data", {}): data[row_key] = getattr(row, row_key) address_sets[name] = data return address_sets def get_router_port_options(self, lsp_name): try: lsp = idlutils.row_by_value(self.idl, 'Logical_Switch_Port', 'name', lsp_name) options = getattr(lsp, 'options') for key in list(options.keys()): if key not in ovn_const.OVN_ROUTER_PORT_OPTION_KEYS: del(options[key]) return options except idlutils.RowNotFound: return {} def add_nat_rule_in_lrouter(self, lrouter, **columns): return cmd.AddNATRuleInLRouterCommand(self, lrouter, **columns) def delete_nat_rule_in_lrouter(self, lrouter, type, logical_ip, external_ip, if_exists=True): return cmd.DeleteNATRuleInLRouterCommand(self, lrouter, type, logical_ip, external_ip, if_exists) def get_lrouter_nat_rules(self, lrouter_name): try: lrouter = idlutils.row_by_value(self.idl, 'Logical_Router', 'name', lrouter_name) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % lrouter_name raise RuntimeError(msg) nat_rules = [] for nat_rule in getattr(lrouter, 'nat', []): ext_ids = {} # TODO(dalvarez): remove this check once the minimum OVS required # version contains the column (when OVS 2.8.2 is released). if self.is_col_present('NAT', 'external_ids'): ext_ids = dict(getattr(nat_rule, 'external_ids', {})) nat_rules.append({'external_ip': nat_rule.external_ip, 'logical_ip': nat_rule.logical_ip, 'type': nat_rule.type, 'uuid': nat_rule.uuid, 'external_ids': ext_ids}) return nat_rules def set_nat_rule_in_lrouter(self, lrouter, nat_rule_uuid, **columns): return cmd.SetNATRuleInLRouterCommand(self, lrouter, nat_rule_uuid, **columns) def add_nat_ip_to_lrport_peer_options(self, lport, nat_ip): return cmd.AddNatIpToLRPortPeerOptionsCommand(self, lport, nat_ip) def delete_nat_ip_from_lrport_peer_options(self, lport, nat_ip): return cmd.DeleteNatIpFromLRPortPeerOptionsCommand(self, lport, nat_ip) def get_lswitch_port(self, lsp_name): try: return self.lookup('Logical_Switch_Port', lsp_name) except idlutils.RowNotFound: return None def get_parent_port(self, lsp_name): lsp = self.get_lswitch_port(lsp_name) if not lsp: return '' return lsp.parent_name def get_lswitch(self, lswitch_name): # FIXME(lucasagomes): We should refactor those get_*() # methods. Some of 'em require the name, others IDs etc... It can # be confusing. if uuidutils.is_uuid_like(lswitch_name): lswitch_name = utils.ovn_name(lswitch_name) try: return self.lookup('Logical_Switch', lswitch_name) except idlutils.RowNotFound: return None def get_ls_and_dns_record(self, lswitch_name): ls = self.get_lswitch(lswitch_name) if not ls: return (None, None) if not hasattr(ls, 'dns_records'): return (ls, None) for dns_row in ls.dns_records: if dns_row.external_ids.get('ls_name') == lswitch_name: return (ls, dns_row) return (ls, None) # Check for a column match in the table. If not found do a retry with # a stop delay of 10 secs. This function would be useful if the caller # wants to verify for the presence of a particular row in the table # with the column match before doing any transaction. # Eg. We can check if Logical_Switch row is present before adding a # logical switch port to it. @tenacity.retry(retry=tenacity.retry_if_exception_type(RuntimeError), wait=tenacity.wait_exponential(), stop=tenacity.stop_after_delay(10), reraise=True) def check_for_row_by_value_and_retry(self, table, column, match): try: idlutils.row_by_value(self.idl, table, column, match) except idlutils.RowNotFound: msg = (_("%(match)s does not exist in %(column)s of %(table)s") % {'match': match, 'column': column, 'table': table}) raise RuntimeError(msg) def get_floatingip(self, fip_id): # TODO(dalvarez): remove this check once the minimum OVS required # version contains the column (when OVS 2.8.2 is released). if not self.is_col_present('NAT', 'external_ids'): return fip = self.db_find('NAT', ('external_ids', '=', {ovn_const.OVN_FIP_EXT_ID_KEY: fip_id})) result = fip.execute(check_error=True) return result[0] if result else None def get_floatingip_by_ips(self, router_id, logical_ip, external_ip): if not all([router_id, logical_ip, external_ip]): return for nat in self.get_lrouter_nat_rules(utils.ovn_name(router_id)): if (nat['type'] == 'dnat_and_snat' and nat['logical_ip'] == logical_ip and nat['external_ip'] == external_ip): return nat def get_address_set(self, addrset_id, ip_version='ip4'): addr_name = utils.ovn_addrset_name(addrset_id, ip_version) try: return idlutils.row_by_value(self.idl, 'Address_Set', 'name', addr_name) except idlutils.RowNotFound: return None def check_revision_number(self, name, resource, resource_type, if_exists=True): return cmd.CheckRevisionNumberCommand( self, name, resource, resource_type, if_exists) def get_lrouter(self, lrouter_name): if uuidutils.is_uuid_like(lrouter_name): lrouter_name = utils.ovn_name(lrouter_name) # TODO(lucasagomes): Use lr_get() once we start refactoring this # API to use methods from ovsdbapp. lr = self.db_find_rows('Logical_Router', ('name', '=', lrouter_name)) result = lr.execute(check_error=True) return result[0] if result else None def get_lrouter_port(self, lrp_name): # TODO(mangelajo): Implement lrp_get() ovsdbapp and use from here lrp = self.db_find_rows('Logical_Router_Port', ('name', '=', lrp_name)) result = lrp.execute(check_error=True) return result[0] if result else None def delete_lrouter_ext_gw(self, lrouter_name, if_exists=True): return cmd.DeleteLRouterExtGwCommand(self, lrouter_name, if_exists) class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend): def __init__(self, connection): super(OvsdbSbOvnIdl, self).__init__(connection) # TODO(twilson) This direct access of the idl should be removed in # favor of a backend-agnostic method self.idl._session.reconnect.set_probe_interval( cfg.get_ovn_ovsdb_probe_interval()) def _get_chassis_physnets(self, chassis): bridge_mappings = chassis.external_ids.get('ovn-bridge-mappings', '') mapping_dict = helpers.parse_mappings(bridge_mappings.split(','), unique_values=False) return list(mapping_dict.keys()) def chassis_exists(self, hostname): cmd = self.db_find('Chassis', ('hostname', '=', hostname)) return bool(cmd.execute(check_error=True)) def get_chassis_hostname_and_physnets(self): chassis_info_dict = {} for ch in self.chassis_list().execute(check_error=True): chassis_info_dict[ch.hostname] = self._get_chassis_physnets(ch) return chassis_info_dict def get_chassis_and_physnets(self): chassis_info_dict = {} for ch in self.chassis_list().execute(check_error=True): chassis_info_dict[ch.name] = self._get_chassis_physnets(ch) return chassis_info_dict def get_all_chassis(self, chassis_type=None): # TODO(azbiswas): Use chassis_type as input once the compute type # preference patch (as part of external ids) merges. return [c.name for c in self.chassis_list().execute(check_error=True)] def get_chassis_data_for_ml2_bind_port(self, hostname): try: cmd = self.db_find_rows('Chassis', ('hostname', '=', hostname)) chassis = next(c for c in cmd.execute(check_error=True)) except StopIteration: msg = _('Chassis with hostname %s does not exist') % hostname raise RuntimeError(msg) return (chassis.external_ids.get('datapath-type', ''), chassis.external_ids.get('iface-types', ''), self._get_chassis_physnets(chassis)) def get_metadata_port_network(self, network): # TODO(twilson) This function should really just take a Row/RowView try: dp = self.lookup('Datapath_Binding', uuid.UUID(network)) except idlutils.RowNotFound: return None cmd = self.db_find_rows('Port_Binding', ('datapath', '=', dp), ('type', '=', 'localport')) return next(iter(cmd.execute(check_error=True)), None) def get_chassis_metadata_networks(self, chassis_name): """Return a list with the metadata networks the chassis is hosting.""" chassis = self.lookup('Chassis', chassis_name) proxy_networks = chassis.external_ids.get( 'neutron-metadata-proxy-networks', None) return proxy_networks.split(',') if proxy_networks else [] def set_chassis_metadata_networks(self, chassis, networks): nets = ','.join(networks) if networks else '' # TODO(twilson) This could just use DbSetCommand return cmd.UpdateChassisExtIdsCommand( self, chassis, {'neutron-metadata-proxy-networks': nets}, if_exists=True) def get_network_port_bindings_by_ip(self, network, ip_address): rows = self.db_list_rows('Port_Binding').execute(check_error=True) # TODO(twilson) It would be useful to have a db_find that takes a # comparison function return [r for r in rows if (r.mac and str(r.datapath.uuid) == network) and ip_address in r.mac[0].split(' ')] def set_port_cidrs(self, name, cidrs): # TODO(twilson) add if_exists to db commands return self.db_set('Port_Binding', name, 'external_ids', {'neutron-port-cidrs': cidrs}, if_exists=True) def get_ports_on_chassis(self, chassis): # TODO(twilson) Some day it would be nice to stop passing names around # and just start using chassis objects so db_find_rows could be used rows = self.db_list_rows('Port_Binding').execute(check_error=True) return [r for r in rows if r.chassis and r.chassis[0].name == chassis] def get_logical_port_chassis_and_datapath(self, name): rows = self.db_list_rows('Port_Binding').execute(check_error=True) for port in rows: if port.logical_port == name: datapath = str(port.datapath.uuid) chassis = port.chassis[0].name if port.chassis else None return chassis, datapath networking-ovn-4.0.0/networking_ovn/ovsdb/__init__.py0000666000175100017510000000000013245511145023016 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/ovsdb/ovn_api.py0000666000175100017510000006221713245511145022734 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 ovsdbapp import api import six @six.add_metaclass(abc.ABCMeta) class API(api.API): @abc.abstractmethod def set_lswitch_ext_ids(self, name, ext_ids, if_exists=True): """Create a command to set OVN lswitch external ids :param name: The name of the lswitch :type name: string :param ext_ids The external ids to set for the lswitch :type ext_ids: dictionary :param if_exists: Do not fail if lswitch does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def create_lswitch_port(self, lport_name, lswitch_name, may_exist=True, **columns): """Create a command to add an OVN logical switch port :param lport_name: The name of the lport :type lport_name: string :param lswitch_name: The name of the lswitch the lport is created on :type lswitch_name: string :param may_exist: Do not fail if lport already exists :type may_exist: bool :param columns: Dictionary of port columns Supported columns: macs, external_ids, parent_name, tag, enabled :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def set_lswitch_port(self, lport_name, if_exists=True, **columns): """Create a command to set OVN logical switch port fields :param lport_name: The name of the lport :type lport_name: string :param columns: Dictionary of port columns Supported columns: macs, external_ids, parent_name, tag, enabled :param if_exists: Do not fail if lport does not exist :type if_exists: bool :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_lswitch_port(self, lport_name=None, lswitch_name=None, ext_id=None, if_exists=True): """Create a command to delete an OVN logical switch port :param lport_name: The name of the lport :type lport_name: string :param lswitch_name: The name of the lswitch :type lswitch_name: string :param ext_id: The external id of the lport :type ext_id: pair of :param if_exists: Do not fail if the lport does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def create_lrouter(self, name, may_exist=True, **columns): """Create a command to add an OVN lrouter :param name: The id of the lrouter :type name: string :param may_exist: Do not fail if lrouter already exists :type may_exist: bool :param columns: Dictionary of lrouter columns Supported columns: external_ids, default_gw, ip :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def update_lrouter(self, name, if_exists=True, **columns): """Update a command to add an OVN lrouter :param name: The id of the lrouter :type name: string :param if_exists: Do not fail if the lrouter does not exist :type if_exists: bool :param columns: Dictionary of lrouter columns Supported columns: external_ids, default_gw, ip :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_lrouter(self, name, if_exists=True): """Create a command to delete an OVN lrouter :param name: The id of the lrouter :type name: string :param if_exists: Do not fail if the lrouter does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def add_lrouter_port(self, name, lrouter, may_exist=True, **columns): """Create a command to add an OVN lrouter port :param name: The unique name of the lrouter port :type name: string :param lrouter: The unique name of the lrouter :type lrouter: string :param lswitch: The unique name of the lswitch :type lswitch: string :param may_exist: If true, do not fail if lrouter port set already exists. :type may_exist: bool :param columns: Dictionary of lrouter columns Supported columns: external_ids, mac, network :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def update_lrouter_port(self, name, if_exists=True, **columns): """Update a command to add an OVN lrouter port :param name: The unique name of the lrouter port :type name: string :param if_exists: Do not fail if the lrouter port does not exist :type if_exists: bool :param columns: Dictionary of lrouter columns Supported columns: networks :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_lrouter_port(self, name, lrouter, if_exists=True): """Create a command to delete an OVN lrouter port :param name: The unique name of the lport :type name: string :param lrouter: The unique name of the lrouter :type lrouter: string :param if_exists: Do not fail if the lrouter port does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def set_lrouter_port_in_lswitch_port(self, lswitch_port, lrouter_port, is_gw_port=False, if_exists=True): """Create a command to set lswitch_port as lrouter_port :param lswitch_port: The name of logical switch port :type lswitch_port: string :param lrouter_port: The name of logical router port :type lrouter_port: string :param is_gw_port: True if logical router port is gw port :type is_gw_port: bool :param if_exists: Do not fail if the lswitch port does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def add_acl(self, lswitch, lport, **columns): """Create an ACL for a logical port. :param lswitch: The logical switch the port is attached to. :type lswitch: string :param lport: The logical port this ACL is associated with. :type lport: string :param columns: Dictionary of ACL columns Supported columns: see ACL table in OVN_Northbound :type columns: dictionary """ @abc.abstractmethod def delete_acl(self, lswitch, lport, if_exists=True): """Delete all ACLs for a logical port. :param lswitch: The logical switch the port is attached to. :type lswitch: string :param lport: The logical port this ACL is associated with. :type lport: string :param if_exists: Do not fail if the ACL for this lport does not exist :type if_exists: bool """ @abc.abstractmethod def update_acls(self, lswitch_names, port_list, acl_new_values_dict, need_compare=True, is_add_acl=True): """Update the list of acls on logical switches with new values. :param lswitch_names: List of logical switch names :type lswitch_name: [] :param port_list: Iterator of list of ports :type port_list: [] :param acl_new_values_dict: Dictionary of acls indexed by port id :type acl_new_values_dict: {} :param need_compare: If acl_new_values_dict need compare with existing acls :type need_compare: bool :is_add_acl: If updating is caused by adding acl :type is_add_acl: bool """ @abc.abstractmethod def get_acl_by_id(self, acl_id): """Get an ACL by its ID. :param acl_id: ID of the ACL to lookup :type acl_id: string :returns The ACL row or None: """ @abc.abstractmethod def add_static_route(self, lrouter, **columns): """Add static route to logical router. :param lrouter: The unique name of the lrouter :type lrouter: string :param columns: Dictionary of static columns Supported columns: prefix, nexthop, valid :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_static_route(self, lrouter, ip_prefix, nexthop, if_exists=True): """Delete static route from logical router. :param lrouter: The unique name of the lrouter :type lrouter: string :param ip_prefix: The prefix of the static route :type ip_prefix: string :param nexthop: The nexthop of the static route :type nexthop: string :param if_exists: Do not fail if router does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def create_address_set(self, name, may_exist=True, **columns): """Create an address set :param name: The name of the address set :type name: string :param may_exist: Do not fail if address set already exists :type may_exist: bool :param columns: Dictionary of address set columns Supported columns: external_ids, addresses :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_address_set(self, name, if_exists=True): """Delete an address set :param name: The name of the address set :type name: string :param if_exists: Do not fail if the address set does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def update_address_set(self, name, addrs_add, addrs_remove, if_exists=True): """Updates addresses in an address set :param name: The name of the address set :type name: string :param addrs_add: The addresses to be added :type addrs_add: [] :param addrs_remove: The addresses to be removed :type addrs_remove: [] :param if_exists: Do not fail if the address set does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def update_address_set_ext_ids(self, name, external_ids, if_exists=True): """Update external IDs for an address set :param name: The name of the address set :type name: string :param external_ids: The external IDs for the address set :type external_ids: dict :param if_exists: Do not fail if the address set does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def get_all_chassis_gateway_bindings(self, chassis_candidate_list=None): """Return a dictionary of chassis name:list of gateways :param chassis_candidate_list: List of possible chassis candidates :type chassis_candidate_list: [] :returns: {} of chassis to routers mapping """ @abc.abstractmethod def get_gateway_chassis_binding(self, gateway_id): """Return the chassis to which the gateway is bound to :param gateway_id: The gateway id :type gateway_id: string :returns: string containing the chassis name """ @abc.abstractmethod def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets): """Return a list of gateways not hosted on chassis :param port_physnet_dict: Dictionary of gateway ports and their physnet :param chassis_physnets: Dictionary of chassis and physnets :returns: List of gateways not hosted on a valid chassis """ @abc.abstractmethod def add_dhcp_options(self, subnet_id, port_id=None, may_exist=True, **columns): """Adds the DHCP options specified in the @columns in DHCP_Options If the DHCP options already exist in the DHC_Options table for the @subnet_id (and @lsp_name), updates the row, else creates a new row. :param subnet_id: The subnet id to which the DHCP options belong to :type subnet_id: string :param port_id: The port id to which the DHCP options belong to if specified :type port_id: string :param may_exist: If true, checks if the DHCP options for subnet_id exists or not. If it already exists, it updates the row with the columns specified. Else creates a new row. :type may_exist: bool :type columns: Dictionary of DHCP_Options columns Supported columns: see DHCP_Options table in OVN_Northbound :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_dhcp_options(self, row_uuid, if_exists=True): """Deletes the row in DHCP_Options with the @row_uuid :param row_uuid: The UUID of the row to be deleted. :type row_uuid: string :param if_exists: Do not fail if the DHCP_Options row does not exist :type if_exists: bool """ @abc.abstractmethod def get_subnet_dhcp_options(self, subnet_id, with_ports=False): """Returns the Subnet DHCP options as a dictionary :param subnet_id: The subnet id whose DHCP options are returned :type subnet_id: string :param with_ports: If True, also returns the ports DHCP options. :type with_ports: bool :returns: Returns a dictionary containing two keys: subnet and ports. """ @abc.abstractmethod def get_subnets_dhcp_options(self, subnet_ids): """Returns the Subnets DHCP options as list of dictionary :param subnet_ids: The subnet ids whose DHCP options are returned :type subnet_ids: list of string :returns: Returns the columns of the DHCP_Options as list of dictionary. Empty list is returned if no DHCP_Options matched found. """ @abc.abstractmethod def get_address_sets(self): """Gets all address sets in the OVN_Northbound DB :returns: dictionary indexed by name, DB columns as values """ @abc.abstractmethod def get_router_port_options(self, lsp_name): """Get options set for lsp of type router :returns: router port options """ @abc.abstractmethod def add_nat_rule_in_lrouter(self, lrouter, **columns): """Add NAT rule in logical router :param lrouter: The unique name of the lrouter :type lrouter: string :param columns: Dictionary of nat columns Supported columns: type, logical_ip, external_ip :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_nat_rule_in_lrouter(self, lrouter, type, logical_ip, external_ip, if_exists=True): """Delete NAT rule in logical router :param lrouter: The unique name of the lrouter :type lrouter: string :param type: Type of nat. Supported values are 'snat', 'dnat' and 'dnat_and_snat' :type type: string :param logical_ip: IP or network that needs to be natted :type logical_ip: string :param external_ip: External IP to be used for nat :type external_ip: string :param if_exists: Do not fail if the Logical_Router row does not exist :type if_exists: bool :returns: :class:`Command` with no result """ @abc.abstractmethod def add_nat_ip_to_lrport_peer_options(self, lport, nat_ip): """Add nat address in peer port of lrouter port :param lport: The unique name of the lswitch port :type lport: string :param nat_ip: nat ip to be added :type nat_ip: string :returns: :class:`Command` with no result """ @abc.abstractmethod def delete_nat_ip_from_lrport_peer_options(self, lport, nat_ip): """Delete nat address from peer port of lrouter port :param lport: The unique name of the lswitch port :type lport: string :param nat_ip: nat ip to be removed :type nat_ip: string :returns: :class:`Command` with no result """ @abc.abstractmethod def get_lrouter_nat_rules(self, lrouter): """Returns the nat rules of a router :param lrouter: The unique name of the router :type lrouter: string :returns: A list of nat rules of the router, with each item as a dict with the keys - 'external_ip', 'logical_ip' 'type' and 'uuid' of the row. """ @abc.abstractmethod def set_nat_rule_in_lrouter(self, lrouter, nat_rule_uuid, **columns): """Sets the NAT rule fields :param lrouter: The unique name of the router to which this the NAT rule belongs to. :type lrouter: string :param nat_rule_uuid: The uuid of the NAT rule row to be updated. :type nat_rule_uuid: string :type columns: dictionary :returns: :class:`Command` with no result """ @abc.abstractmethod def get_lswitch(self, lswitch_name): """Returns the logical switch :param lswitch_name: The unique name of the logical switch :type lswitch_name: string :returns: Returns logical switch or None """ @abc.abstractmethod def get_ls_and_dns_record(self, lswitch_name): """Returns the logical switch and 'dns' records :param lswitch_name: The unique name of the logical switch :type lswitch_name: string :returns: Returns logical switch and dns records as a tuple """ @abc.abstractmethod def get_floatingip(self, fip_id): """Get a Floating IP by its ID :param fip_id: The floating IP id :type fip_id: string :returns: The NAT rule row or None """ @abc.abstractmethod def get_floatingip_by_ips(self, router_id, logical_ip, external_ip): """Get a Floating IP based on it's logical and external IPs. DEPRECATED. In the Rocky release of OpenStack this method can be removed and get_floatingip() should be used instead. This method is a backward compatibility layer for the Pike -> Queens release. :param router_id: The ID of the router to which the FIP belongs to. :type lrouter: string :param logical_ip: The FIP's logical IP address :type logical_ip: string :param external_ip: The FIP's external IP address :type external_ip: string :returns: The NAT rule row or None """ def check_revision_number(self, name, resource, resource_type, if_exists=True): """Compare the revision number from Neutron and OVN. Check if the revision number in OVN is lower than the one from the Neutron resource, otherwise raise RevisionConflict and abort the transaction. :param name: The unique name of the resource :type name: string :param resource: The neutron resource object :type resource: dictionary :param resource_type: The resource object type :type resource_type: dictionary :param if_exists: Do not fail if resource does not exist :type if_exists: bool :returns: :class:`Command` with no result :raise: RevisionConflict if the revision number in OVN is equal or higher than the neutron object """ @abc.abstractmethod def get_lswitch_port(self, lsp_name): """Get a Logical Switch Port by its name. :param lsp_name: The Logical Switch Port name :type lsp_name: string :returns: The Logical Switch Port row or None """ @abc.abstractmethod def get_lrouter(self, lrouter_name): """Get a Logical Router by its name :param lrouter_name: The name of the logical router :type lrouter_name: string :returns: The Logical_Router row or None """ @abc.abstractmethod def delete_lrouter_ext_gw(self, lrouter_name): """Delete Logical Router external gateway. :param lrouter_name: The name of the logical router :type lrouter_name: string :returns: :class:`Command` with no result """ @abc.abstractmethod def get_address_set(self, addrset_id, ip_version='ip4'): """Get a Address Set by its ID. :param addrset_id: The Address Set ID :type addrset_id: string :param ip_version: Either "ip4" or "ip6". Defaults to "ip4" :type addr_name: string :returns: The Address Set row or None """ @six.add_metaclass(abc.ABCMeta) class SbAPI(api.API): @abc.abstractmethod def chassis_exists(self, hostname): """Test if chassis for given hostname exists. @param hostname: The hostname of the chassis @type hostname: string :returns: True if the chassis exists, else False. """ @abc.abstractmethod def get_chassis_hostname_and_physnets(self): """Return a dict contains hostname and physnets mapping. Hostname will be dict key, and a list of physnets will be dict value. And hostname and physnets are related to the same host. """ @abc.abstractmethod def get_chassis_and_physnets(self): """Return a dict contains chassis name and physnets mapping. Chassis name will be dict key, and a list of physnets will be dict value. And chassis name and physnets are related to the same chassis. """ @abc.abstractmethod def get_all_chassis(self, chassis_type=None): """Return a list of all chassis which match the compute_type :param chassis_type: The type of chassis :type chassis_type: string """ @abc.abstractmethod def get_chassis_data_for_ml2_bind_port(self, hostname): """Return chassis data for ML2 port binding. @param hostname: The hostname of the chassis @type hostname: string :returns: Tuple containing the chassis datapath type, iface types and physical networks for the OVN bridge mappings. :raises: RuntimeError exception if an OVN chassis does not exist. """ networking-ovn-4.0.0/networking_ovn/ovsdb/commands.py0000666000175100017510000012733613245511145023106 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 ovsdbapp.backend.ovs_idl import command from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn._i18n import _ from networking_ovn.common import constants as ovn_const from networking_ovn.common import exceptions as ovn_exc from networking_ovn.common import utils RESOURCE_TYPE_MAP = { ovn_const.TYPE_NETWORKS: 'Logical_Switch', ovn_const.TYPE_PORTS: 'Logical_Switch_Port', ovn_const.TYPE_ROUTERS: 'Logical_Router', ovn_const.TYPE_ROUTER_PORTS: 'Logical_Router_Port', ovn_const.TYPE_FLOATINGIPS: 'NAT', ovn_const.TYPE_SUBNETS: 'DHCP_Options', } def _addvalue_to_list(row, column, new_value): row.addvalue(column, new_value) def _delvalue_from_list(row, column, old_value): row.delvalue(column, old_value) def _updatevalues_in_list(row, column, new_values=None, old_values=None): new_values = new_values or [] old_values = old_values or [] for new_value in new_values: row.addvalue(column, new_value) for old_value in old_values: row.delvalue(column, old_value) def get_lsp_dhcp_options_uuids(lsp, lsp_name): # Get dhcpv4_options and dhcpv6_options uuids from Logical_Switch_Port, # which are references of port dhcp options in DHCP_Options table. uuids = set() for dhcp_opts in getattr(lsp, 'dhcpv4_options', []): external_ids = getattr(dhcp_opts, 'external_ids', {}) if external_ids.get('port_id') == lsp_name: uuids.add(dhcp_opts.uuid) for dhcp_opts in getattr(lsp, 'dhcpv6_options', []): external_ids = getattr(dhcp_opts, 'external_ids', {}) if external_ids.get('port_id') == lsp_name: uuids.add(dhcp_opts.uuid) return uuids def _add_gateway_chassis(api, txn, lrp_name, val): gateway_chassis = api._tables.get('Gateway_Chassis') if gateway_chassis: prio = len(val) uuid_list = [] for chassis in val: gwc_name = '%s_%s' % (lrp_name, chassis) try: gwc = idlutils.row_by_value(api.idl, 'Gateway_Chassis', 'name', gwc_name) except idlutils.RowNotFound: gwc = txn.insert(gateway_chassis) gwc.name = gwc_name gwc.chassis_name = chassis gwc.priority = prio prio = prio - 1 uuid_list.append(gwc.uuid) return 'gateway_chassis', uuid_list else: chassis = {ovn_const.OVN_GATEWAY_CHASSIS_KEY: val[0]} return 'options', chassis class LSwitchSetExternalIdsCommand(command.BaseCommand): def __init__(self, api, name, ext_ids, if_exists): super(LSwitchSetExternalIdsCommand, self).__init__(api) self.name = name self.ext_ids = ext_ids self.if_exists = if_exists def run_idl(self, txn): try: lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Switch %s does not exist") % self.name raise RuntimeError(msg) lswitch.verify('external_ids') external_ids = getattr(lswitch, 'external_ids', {}) for key, value in self.ext_ids.items(): external_ids[key] = value lswitch.external_ids = external_ids class AddLSwitchPortCommand(command.BaseCommand): def __init__(self, api, lport, lswitch, may_exist, **columns): super(AddLSwitchPortCommand, self).__init__(api) self.lport = lport self.lswitch = lswitch self.may_exist = may_exist self.columns = columns def run_idl(self, txn): try: lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', self.lswitch) except idlutils.RowNotFound: msg = _("Logical Switch %s does not exist") % self.lswitch raise RuntimeError(msg) if self.may_exist: port = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lport, None) if port: return port = txn.insert(self.api._tables['Logical_Switch_Port']) port.name = self.lport dhcpv4_options = self.columns.pop('dhcpv4_options', []) if isinstance(dhcpv4_options, list): port.dhcpv4_options = dhcpv4_options else: port.dhcpv4_options = [dhcpv4_options.result] dhcpv6_options = self.columns.pop('dhcpv6_options', []) if isinstance(dhcpv6_options, list): port.dhcpv6_options = dhcpv6_options else: port.dhcpv6_options = [dhcpv6_options.result] for col, val in self.columns.items(): setattr(port, col, val) # add the newly created port to existing lswitch _addvalue_to_list(lswitch, 'ports', port.uuid) self.result = port.uuid def post_commit(self, txn): self.result = txn.get_insert_uuid(self.result) class SetLSwitchPortCommand(command.BaseCommand): def __init__(self, api, lport, if_exists, **columns): super(SetLSwitchPortCommand, self).__init__(api) self.lport = lport self.columns = columns self.if_exists = if_exists def run_idl(self, txn): try: port = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lport) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Switch Port %s does not exist") % self.lport raise RuntimeError(msg) # Delete DHCP_Options records no longer referred by this port. # The table rows should be consistent for the same transaction. # After we get DHCP_Options rows uuids from port dhcpv4_options # and dhcpv6_options references, the rows shouldn't disappear for # this transaction before we delete it. cur_port_dhcp_opts = get_lsp_dhcp_options_uuids( port, self.lport) new_port_dhcp_opts = set() dhcpv4_options = self.columns.pop('dhcpv4_options', None) if dhcpv4_options is None: new_port_dhcp_opts.update([option.uuid for option in getattr(port, 'dhcpv4_options', [])]) elif isinstance(dhcpv4_options, list): new_port_dhcp_opts.update(dhcpv4_options) port.dhcpv4_options = dhcpv4_options else: new_port_dhcp_opts.add(dhcpv4_options.result) port.dhcpv4_options = [dhcpv4_options.result] dhcpv6_options = self.columns.pop('dhcpv6_options', None) if dhcpv6_options is None: new_port_dhcp_opts.update([option.uuid for option in getattr(port, 'dhcpv6_options', [])]) elif isinstance(dhcpv6_options, list): new_port_dhcp_opts.update(dhcpv6_options) port.dhcpv6_options = dhcpv6_options else: new_port_dhcp_opts.add(dhcpv6_options.result) port.dhcpv6_options = [dhcpv6_options.result] for uuid in cur_port_dhcp_opts - new_port_dhcp_opts: self.api._tables['DHCP_Options'].rows[uuid].delete() for col, val in self.columns.items(): setattr(port, col, val) class DelLSwitchPortCommand(command.BaseCommand): def __init__(self, api, lport, lswitch, if_exists): super(DelLSwitchPortCommand, self).__init__(api) self.lport = lport self.lswitch = lswitch self.if_exists = if_exists def run_idl(self, txn): try: lport = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lport) lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', self.lswitch) except idlutils.RowNotFound: if self.if_exists: return msg = _("Port %s does not exist") % self.lport raise RuntimeError(msg) # Delete DHCP_Options records no longer referred by this port. cur_port_dhcp_opts = get_lsp_dhcp_options_uuids( lport, self.lport) for uuid in cur_port_dhcp_opts: self.api._tables['DHCP_Options'].rows[uuid].delete() _delvalue_from_list(lswitch, 'ports', lport) self.api._tables['Logical_Switch_Port'].rows[lport.uuid].delete() class AddLRouterCommand(command.BaseCommand): def __init__(self, api, name, may_exist, **columns): super(AddLRouterCommand, self).__init__(api) self.name = name self.columns = columns self.may_exist = may_exist def run_idl(self, txn): if self.may_exist: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.name, None) if lrouter: return row = txn.insert(self.api._tables['Logical_Router']) row.name = self.name for col, val in self.columns.items(): setattr(row, col, val) class UpdateLRouterCommand(command.BaseCommand): def __init__(self, api, name, if_exists, **columns): super(UpdateLRouterCommand, self).__init__(api) self.name = name self.columns = columns self.if_exists = if_exists def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.name, None) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router %s does not exist") % self.name raise RuntimeError(msg) if lrouter: for col, val in self.columns.items(): setattr(lrouter, col, val) return class DelLRouterCommand(command.BaseCommand): def __init__(self, api, name, if_exists): super(DelLRouterCommand, self).__init__(api) self.name = name self.if_exists = if_exists def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router %s does not exist") % self.name raise RuntimeError(msg) self.api._tables['Logical_Router'].rows[lrouter.uuid].delete() class AddLRouterPortCommand(command.BaseCommand): def __init__(self, api, name, lrouter, may_exist, **columns): super(AddLRouterPortCommand, self).__init__(api) self.name = name self.lrouter = lrouter self.may_exist = may_exist self.columns = columns def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) try: idlutils.row_by_value(self.api.idl, 'Logical_Router_Port', 'name', self.name) if self.may_exist: return # The LRP entry with certain name has already exist, raise an # exception to notice caller. It's caller's responsibility to # call UpdateLRouterPortCommand to get LRP entry processed # correctly. msg = _("Logical Router Port with name \"%s\" " "already exists.") % self.name raise RuntimeError(msg) except idlutils.RowNotFound: lrouter_port = txn.insert(self.api._tables['Logical_Router_Port']) lrouter_port.name = self.name for col, val in self.columns.items(): if col == 'gateway_chassis': col, val = _add_gateway_chassis(self.api, txn, self.name, val) setattr(lrouter_port, col, val) _addvalue_to_list(lrouter, 'ports', lrouter_port) class UpdateLRouterPortCommand(command.BaseCommand): def __init__(self, api, name, if_exists, **columns): super(UpdateLRouterPortCommand, self).__init__(api) self.name = name self.columns = columns self.if_exists = if_exists def run_idl(self, txn): try: lrouter_port = idlutils.row_by_value(self.api.idl, 'Logical_Router_Port', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router Port %s does not exist") % self.name raise RuntimeError(msg) if lrouter_port: for col, val in self.columns.items(): if col == 'gateway_chassis': col, val = _add_gateway_chassis(self.api, txn, self.name, val) setattr(lrouter_port, col, val) return class DelLRouterPortCommand(command.BaseCommand): def __init__(self, api, name, lrouter, if_exists): super(DelLRouterPortCommand, self).__init__(api) self.name = name self.lrouter = lrouter self.if_exists = if_exists def run_idl(self, txn): try: lrouter_port = idlutils.row_by_value(self.api.idl, 'Logical_Router_Port', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router Port %s does not exist") % self.name raise RuntimeError(msg) try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) _delvalue_from_list(lrouter, 'ports', lrouter_port) class SetLRouterPortInLSwitchPortCommand(command.BaseCommand): def __init__(self, api, lswitch_port, lrouter_port, is_gw_port, if_exists): super(SetLRouterPortInLSwitchPortCommand, self).__init__(api) self.lswitch_port = lswitch_port self.lrouter_port = lrouter_port self.is_gw_port = is_gw_port self.if_exists = if_exists def run_idl(self, txn): try: port = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lswitch_port) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Switch Port %s does not " "exist") % self.lswitch_port raise RuntimeError(msg) options = {'router-port': self.lrouter_port} if self.is_gw_port: options[ovn_const.OVN_GATEWAY_NAT_ADDRESSES_KEY] = 'router' setattr(port, 'options', options) setattr(port, 'type', 'router') setattr(port, 'addresses', 'router') class AddACLCommand(command.BaseCommand): def __init__(self, api, lswitch, lport, **columns): super(AddACLCommand, self).__init__(api) self.lswitch = lswitch self.lport = lport self.columns = columns def run_idl(self, txn): try: lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', self.lswitch) except idlutils.RowNotFound: msg = _("Logical Switch %s does not exist") % self.lswitch raise RuntimeError(msg) row = txn.insert(self.api._tables['ACL']) for col, val in self.columns.items(): setattr(row, col, val) _addvalue_to_list(lswitch, 'acls', row.uuid) class DelACLCommand(command.BaseCommand): def __init__(self, api, lswitch, lport, if_exists): super(DelACLCommand, self).__init__(api) self.lswitch = lswitch self.lport = lport self.if_exists = if_exists def run_idl(self, txn): try: lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', self.lswitch) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Switch %s does not exist") % self.lswitch raise RuntimeError(msg) acls_to_del = [] acls = getattr(lswitch, 'acls', []) for acl in acls: ext_ids = getattr(acl, 'external_ids', {}) if ext_ids.get('neutron:lport') == self.lport: acls_to_del.append(acl) for acl in acls_to_del: acl.delete() _updatevalues_in_list(lswitch, 'acls', old_values=acls_to_del) class UpdateACLsCommand(command.BaseCommand): def __init__(self, api, lswitch_names, port_list, acl_new_values_dict, need_compare=True, is_add_acl=True): """This command updates the acl list for the logical switches @param lswitch_names: List of Logical Switch Names @type lswitch_names: [] @param port_list: Iterator of List of Ports @type port_list: [] @param acl_new_values_dict: Dictionary of acls indexed by port id @type acl_new_values_dict: {} @need_compare: If acl_new_values_dict needs be compared with existing acls. @type: Boolean. @is_add_acl: If updating is caused by acl adding action. @type: Boolean. """ super(UpdateACLsCommand, self).__init__(api) self.lswitch_names = lswitch_names self.port_list = port_list self.acl_new_values_dict = acl_new_values_dict self.need_compare = need_compare self.is_add_acl = is_add_acl def _acl_list_sub(self, acl_list1, acl_list2): """Compute the elements in acl_list1 but not in acl_list2. If acl_list1 and acl_list2 were sets, the result of this routine could be thought of as acl_list1 - acl_list2. Note that acl_list1 and acl_list2 cannot actually be sets as they contain dictionary items i.e. set([{'a':1}) doesn't work. """ acl_diff = [] for acl in acl_list1: if acl not in acl_list2: acl_diff.append(acl) return acl_diff def _compute_acl_differences(self, port_list, acl_old_values_dict, acl_new_values_dict, acl_obj_dict): """Compute the difference between the new and old sets of acls @param port_list: Iterator of a List of ports @type port_list: [] @param acl_old_values_dict: Dictionary of old acl values indexed by port id @param acl_new_values_dict: Dictionary of new acl values indexed by port id @param acl_obj_dict: Dictionary of acl objects indexed by the acl value in string format. @var acl_del_objs_dict: Dictionary of acl objects to be deleted indexed by the lswitch. @var acl_add_values_dict: Dictionary of acl values to be added indexed by the lswitch. @return: (acl_del_objs_dict, acl_add_values_dict) @rtype: ({}, {}) """ acl_del_objs_dict = {} acl_add_values_dict = {} for port in port_list: lswitch_name = port['network_id'] acls_old = acl_old_values_dict.get(port['id'], []) acls_new = acl_new_values_dict.get(port['id'], []) acls_del = self._acl_list_sub(acls_old, acls_new) acls_add = self._acl_list_sub(acls_new, acls_old) acl_del_objs = acl_del_objs_dict.setdefault(lswitch_name, []) for acl in acls_del: acl_del_objs.append(acl_obj_dict[str(acl)]) acl_add_values = acl_add_values_dict.setdefault(lswitch_name, []) for acl in acls_add: # Remove lport and lswitch columns del acl['lswitch'] del acl['lport'] acl_add_values.append(acl) return acl_del_objs_dict, acl_add_values_dict def _get_update_data_without_compare(self): lswitch_ovsdb_dict = {} for switch_name in self.lswitch_names: switch_name = utils.ovn_name(switch_name) lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', 'name', switch_name) lswitch_ovsdb_dict[switch_name] = lswitch if self.is_add_acl: acl_add_values_dict = {} for port in self.port_list: switch_name = utils.ovn_name(port['network_id']) if switch_name not in acl_add_values_dict: acl_add_values_dict[switch_name] = [] if port['id'] in self.acl_new_values_dict: acl_add_values_dict[switch_name].append( self.acl_new_values_dict[port['id']]) acl_del_objs_dict = {} else: acl_add_values_dict = {} acl_del_objs_dict = {} del_acl_extids = [] for acl_dict in self.acl_new_values_dict.values(): del_acl_extids.append({acl_dict['match']: acl_dict['external_ids']}) for switch_name, lswitch in lswitch_ovsdb_dict.items(): if switch_name not in acl_del_objs_dict: acl_del_objs_dict[switch_name] = [] acls = getattr(lswitch, 'acls', []) for acl in acls: match = getattr(acl, 'match') acl_extids = {match: getattr(acl, 'external_ids')} if acl_extids in del_acl_extids: acl_del_objs_dict[switch_name].append(acl) return lswitch_ovsdb_dict, acl_del_objs_dict, acl_add_values_dict def run_idl(self, txn): if self.need_compare: # Get all relevant ACLs in 1 shot acl_values_dict, acl_obj_dict, lswitch_ovsdb_dict = \ self.api.get_acls_for_lswitches(self.lswitch_names) # Compute the difference between the new and old set of ACLs acl_del_objs_dict, acl_add_values_dict = \ self._compute_acl_differences( self.port_list, acl_values_dict, self.acl_new_values_dict, acl_obj_dict) else: lswitch_ovsdb_dict, acl_del_objs_dict, acl_add_values_dict = \ self._get_update_data_without_compare() for lswitch_name, lswitch in lswitch_ovsdb_dict.items(): acl_del_objs = acl_del_objs_dict.get(lswitch_name, []) acl_add_values = acl_add_values_dict.get(lswitch_name, []) # Continue if no ACLs to add or delete. if not acl_del_objs and not acl_add_values: continue # Delete old ACLs. if acl_del_objs: for acl_del_obj in acl_del_objs: acl_del_obj.delete() # Add new ACLs. acl_add_objs = None if acl_add_values: acl_add_objs = [] for acl_value in acl_add_values: row = txn.insert(self.api._tables['ACL']) for col, val in acl_value.items(): setattr(row, col, val) acl_add_objs.append(row.uuid) # Update logical switch ACLs. _updatevalues_in_list(lswitch, 'acls', new_values=acl_add_objs, old_values=acl_del_objs) class AddStaticRouteCommand(command.BaseCommand): def __init__(self, api, lrouter, **columns): super(AddStaticRouteCommand, self).__init__(api) self.lrouter = lrouter self.columns = columns def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) row = txn.insert(self.api._tables['Logical_Router_Static_Route']) for col, val in self.columns.items(): setattr(row, col, val) _addvalue_to_list(lrouter, 'static_routes', row.uuid) class DelStaticRouteCommand(command.BaseCommand): def __init__(self, api, lrouter, ip_prefix, nexthop, if_exists): super(DelStaticRouteCommand, self).__init__(api) self.lrouter = lrouter self.ip_prefix = ip_prefix self.nexthop = nexthop self.if_exists = if_exists def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) static_routes = getattr(lrouter, 'static_routes', []) for route in static_routes: ip_prefix = getattr(route, 'ip_prefix', '') nexthop = getattr(route, 'nexthop', '') if self.ip_prefix == ip_prefix and self.nexthop == nexthop: _delvalue_from_list(lrouter, 'static_routes', route) route.delete() break class AddAddrSetCommand(command.BaseCommand): def __init__(self, api, name, may_exist, **columns): super(AddAddrSetCommand, self).__init__(api) self.name = name self.columns = columns self.may_exist = may_exist def run_idl(self, txn): if self.may_exist: addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', 'name', self.name, None) if addrset: return row = txn.insert(self.api._tables['Address_Set']) row.name = self.name for col, val in self.columns.items(): setattr(row, col, val) class DelAddrSetCommand(command.BaseCommand): def __init__(self, api, name, if_exists): super(DelAddrSetCommand, self).__init__(api) self.name = name self.if_exists = if_exists def run_idl(self, txn): try: addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Address set %s does not exist. " "Can't delete.") % self.name raise RuntimeError(msg) self.api._tables['Address_Set'].rows[addrset.uuid].delete() class UpdateAddrSetCommand(command.BaseCommand): def __init__(self, api, name, addrs_add, addrs_remove, if_exists): super(UpdateAddrSetCommand, self).__init__(api) self.name = name self.addrs_add = addrs_add self.addrs_remove = addrs_remove self.if_exists = if_exists def run_idl(self, txn): try: addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Address set %s does not exist. " "Can't update addresses") % self.name raise RuntimeError(msg) _updatevalues_in_list( addrset, 'addresses', new_values=self.addrs_add, old_values=self.addrs_remove) class UpdateAddrSetExtIdsCommand(command.BaseCommand): def __init__(self, api, name, external_ids, if_exists): super(UpdateAddrSetExtIdsCommand, self).__init__(api) self.name = name self.external_ids = external_ids self.if_exists = if_exists def run_idl(self, txn): try: addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Address set %s does not exist. " "Can't update external IDs") % self.name raise RuntimeError(msg) addrset.verify('external_ids') addrset_external_ids = getattr(addrset, 'external_ids', {}) for ext_id_key, ext_id_value in self.external_ids.items(): addrset_external_ids[ext_id_key] = ext_id_value addrset.external_ids = addrset_external_ids class UpdateChassisExtIdsCommand(command.BaseCommand): def __init__(self, api, name, external_ids, if_exists): super(UpdateChassisExtIdsCommand, self).__init__(api) self.name = name self.external_ids = external_ids self.if_exists = if_exists def run_idl(self, txn): try: chassis = idlutils.row_by_value(self.api.idl, 'Chassis', 'name', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Chassis %s does not exist. " "Can't update external IDs") % self.name raise RuntimeError(msg) chassis.verify('external_ids') chassis_external_ids = getattr(chassis, 'external_ids', {}) for ext_id_key, ext_id_value in self.external_ids.items(): chassis_external_ids[ext_id_key] = ext_id_value chassis.external_ids = chassis_external_ids class UpdatePortBindingExtIdsCommand(command.BaseCommand): def __init__(self, api, name, external_ids, if_exists): super(UpdatePortBindingExtIdsCommand, self).__init__(api) self.name = name self.external_ids = external_ids self.if_exists = if_exists def run_idl(self, txn): try: port = idlutils.row_by_value(self.api.idl, 'Port_Binding', 'logical_port', self.name) except idlutils.RowNotFound: if self.if_exists: return msg = _("Port %s does not exist. " "Can't update external IDs") % self.name raise RuntimeError(msg) port.verify('external_ids') port_external_ids = getattr(port, 'external_ids', {}) for ext_id_key, ext_id_value in self.external_ids.items(): port_external_ids[ext_id_key] = ext_id_value port.external_ids = port_external_ids class AddDHCPOptionsCommand(command.BaseCommand): def __init__(self, api, subnet_id, port_id=None, may_exist=True, **columns): super(AddDHCPOptionsCommand, self).__init__(api) self.columns = columns self.may_exist = may_exist self.subnet_id = subnet_id self.port_id = port_id self.new_insert = False def _get_dhcp_options_row(self): for row in self.api._tables['DHCP_Options'].rows.values(): external_ids = getattr(row, 'external_ids', {}) port_id = external_ids.get('port_id') if self.subnet_id == external_ids.get('subnet_id'): if self.port_id == port_id: return row def run_idl(self, txn): row = None if self.may_exist: row = self._get_dhcp_options_row() if not row: row = txn.insert(self.api._tables['DHCP_Options']) self.new_insert = True for col, val in self.columns.items(): setattr(row, col, val) self.result = row.uuid def post_commit(self, txn): # Update the result with inserted uuid for new inserted row, or the # uuid get in run_idl should be real uuid already. if self.new_insert: self.result = txn.get_insert_uuid(self.result) class DelDHCPOptionsCommand(command.BaseCommand): def __init__(self, api, row_uuid, if_exists=True): super(DelDHCPOptionsCommand, self).__init__(api) self.if_exists = if_exists self.row_uuid = row_uuid def run_idl(self, txn): if self.row_uuid not in self.api._tables['DHCP_Options'].rows: if self.if_exists: return msg = _("DHCP Options row %s does not exist") % self.row_uuid raise RuntimeError(msg) self.api._tables['DHCP_Options'].rows[self.row_uuid].delete() class AddNATRuleInLRouterCommand(command.BaseCommand): # TODO(chandrav): Add unit tests, bug #1638715. def __init__(self, api, lrouter, **columns): super(AddNATRuleInLRouterCommand, self).__init__(api) self.lrouter = lrouter self.columns = columns def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) row = txn.insert(self.api._tables['NAT']) for col, val in self.columns.items(): setattr(row, col, val) # TODO(chandrav): convert this to ovs transaction mutate lrouter.verify('nat') nat = getattr(lrouter, 'nat', []) nat.append(row.uuid) setattr(lrouter, 'nat', nat) class DeleteNATRuleInLRouterCommand(command.BaseCommand): # TODO(chandrav): Add unit tests, bug #1638715. def __init__(self, api, lrouter, type, logical_ip, external_ip, if_exists): super(DeleteNATRuleInLRouterCommand, self).__init__(api) self.lrouter = lrouter self.type = type self.logical_ip = logical_ip self.external_ip = external_ip self.if_exists = if_exists def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) lrouter.verify('nat') # TODO(chandrav): convert this to ovs transaction mutate nats = getattr(lrouter, 'nat', []) for nat in nats: type = getattr(nat, 'type', '') external_ip = getattr(nat, 'external_ip', '') logical_ip = getattr(nat, 'logical_ip', '') if self.type == type and \ self.external_ip == external_ip and \ self.logical_ip == logical_ip: nats.remove(nat) nat.delete() break setattr(lrouter, 'nat', nats) class SetNATRuleInLRouterCommand(command.BaseCommand): def __init__(self, api, lrouter, nat_rule_uuid, **columns): super(SetNATRuleInLRouterCommand, self).__init__(api) self.lrouter = lrouter self.nat_rule_uuid = nat_rule_uuid self.columns = columns def run_idl(self, txn): try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) lrouter.verify('nat') nat_rules = getattr(lrouter, 'nat', []) for nat_rule in nat_rules: if nat_rule.uuid == self.nat_rule_uuid: for col, val in self.columns.items(): setattr(nat_rule, col, val) break class AddNatIpToLRPortPeerOptionsCommand(command.BaseCommand): # TODO(chandrav): Add unit tests, bug #1638715. def __init__(self, api, lport, nat_ip): super(AddNatIpToLRPortPeerOptionsCommand, self).__init__(api) self.lport = lport self.nat_ip = nat_ip def run_idl(self, txn): try: lport = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lport) except idlutils.RowNotFound: msg = _("Logical Switch Port %s does not exist") % self.lport raise RuntimeError(msg) lport.verify('addresses') addresses = getattr(lport, 'addresses', []) lport.verify('options') options = getattr(lport, 'options', {}) nat_ip_list = options.get('nat-addresses', '').split() if not nat_ip_list: nat_ips = addresses[0].split()[0] + ' ' + self.nat_ip elif self.nat_ip not in nat_ip_list: nat_ip_list.append(self.nat_ip) nat_ips = ' '.join(nat_ip_list) else: return options['nat-addresses'] = nat_ips lport.options = options class DeleteNatIpFromLRPortPeerOptionsCommand(command.BaseCommand): # TODO(chandrav): Add unit tests, bug #1638715. def __init__(self, api, lport, nat_ip): super(DeleteNatIpFromLRPortPeerOptionsCommand, self).__init__(api) self.lport = lport self.nat_ip = nat_ip def run_idl(self, txn): try: lport = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', 'name', self.lport) except idlutils.RowNotFound: msg = _("Logical Switch Port %s does not exist") % self.lport raise RuntimeError(msg) lport.verify('options') options = getattr(lport, 'options', {}) nat_ip_list = options.get('nat-addresses', '').split() if self.nat_ip not in nat_ip_list: return nat_ip_list.remove(self.nat_ip) if len(nat_ip_list) is 1: nat_ips = '' else: nat_ips = ' '.join(nat_ip_list) if not nat_ips: del options['nat-addresses'] else: options['nat-addresses'] = nat_ips lport.options = options class CheckRevisionNumberCommand(command.BaseCommand): def __init__(self, api, name, resource, resource_type, if_exists): super(CheckRevisionNumberCommand, self).__init__(api) self.name = name self.resource = resource self.resource_type = resource_type self.if_exists = if_exists def _get_floatingip(self): # TODO(lucasagomes): We can't use self.api.lookup() because that # method does not introspect map type columns. We could either: # 1. Enhance it to look into maps or, 2. Add a new ``name`` column # to the NAT table so that we can use lookup() just like we do # for other resources for nat in self.api._tables['NAT'].rows.values(): if nat.type != 'dnat_and_snat': continue ext_ids = getattr(nat, 'external_ids', {}) if ext_ids.get(ovn_const.OVN_FIP_EXT_ID_KEY) == self.name: return nat raise idlutils.RowNotFound( table='NAT', col='external_ids', match=self.name) def _get_subnet(self): for dhcp in self.api._tables['DHCP_Options'].rows.values(): ext_ids = getattr(dhcp, 'external_ids', {}) # Ignore ports DHCP Options if ext_ids.get('port_id'): continue if ext_ids.get('subnet_id') == self.name: return dhcp raise idlutils.RowNotFound( table='DHCP_Options', col='external_ids', match=self.name) def run_idl(self, txn): try: ovn_table = RESOURCE_TYPE_MAP[self.resource_type] # TODO(lucasagomes): After OVS 2.8.2 is released all tables should # have the external_ids column. We can remove this conditional # here by then. if not self.api.is_col_present(ovn_table, 'external_ids'): return ovn_resource = None if self.resource_type == ovn_const.TYPE_FLOATINGIPS: ovn_resource = self._get_floatingip() elif self.resource_type == ovn_const.TYPE_SUBNETS: ovn_resource = self._get_subnet() else: ovn_resource = self.api.lookup(ovn_table, self.name) except idlutils.RowNotFound: if self.if_exists: return msg = (_('Failed to check the revision number for %s: Resource ' 'does not exist') % self.name) raise RuntimeError(msg) external_ids = getattr(ovn_resource, 'external_ids', {}) ovn_revision = int(external_ids.get( ovn_const.OVN_REV_NUM_EXT_ID_KEY, -1)) neutron_revision = utils.get_revision_number(self.resource, self.resource_type) if ovn_revision > neutron_revision: raise ovn_exc.RevisionConflict( resource_id=self.name, resource_type=self.resource_type) ovn_resource.verify('external_ids') ovn_resource.setkey('external_ids', ovn_const.OVN_REV_NUM_EXT_ID_KEY, str(neutron_revision)) def post_commit(self, txn): self.result = ovn_const.TXN_COMMITTED class DeleteLRouterExtGwCommand(command.BaseCommand): def __init__(self, api, lrouter, if_exists): super(DeleteLRouterExtGwCommand, self).__init__(api) self.lrouter = lrouter self.if_exists = if_exists def run_idl(self, txn): # TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged # (prior to that, the external_ids column didn't exist in this # table). if not self.api.is_col_present('Logical_Router_Static_Route', 'external_ids'): return try: lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', 'name', self.lrouter) except idlutils.RowNotFound: if self.if_exists: return msg = _("Logical Router %s does not exist") % self.lrouter raise RuntimeError(msg) lrouter.verify('static_routes') static_routes = getattr(lrouter, 'static_routes', []) for route in static_routes: external_ids = getattr(route, 'external_ids', {}) if ovn_const.OVN_ROUTER_IS_EXT_GW in external_ids: _delvalue_from_list(lrouter, 'static_routes', route) route.delete() break lrouter.verify('nat') nats = getattr(lrouter, 'nat', []) for nat in nats: if nat.type != 'snat': continue _delvalue_from_list(lrouter, 'nat', nat) nat.delete() lrouter_ext_ids = getattr(lrouter, 'external_ids', {}) gw_port_id = lrouter_ext_ids.get(ovn_const.OVN_GW_PORT_EXT_ID_KEY) if not gw_port_id: return try: lrouter_port = idlutils.row_by_value( self.api.idl, 'Logical_Router_Port', 'name', utils.ovn_lrouter_port_name(gw_port_id)) except idlutils.RowNotFound: return _delvalue_from_list(lrouter, 'ports', lrouter_port) networking-ovn-4.0.0/networking_ovn/__init__.py0000666000175100017510000000123613245511145021715 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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__ = pbr.version.VersionInfo( 'networking_ovn').version_string() networking-ovn-4.0.0/networking_ovn/_i18n.py0000666000175100017510000000203013245511145021065 0ustar zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import oslo_i18n DOMAIN = "networking_ovn" _translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) # The translation function using the well-known name "_" _ = _translators.primary # The contextual translation function using the name "_C" _C = _translators.contextual_form # The plural translation function using the name "_P" _P = _translators.plural_form def get_available_languages(): return oslo_i18n.get_available_languages(DOMAIN) networking-ovn-4.0.0/networking_ovn/locale/0000775000175100017510000000000013245511554021043 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/locale/en_GB/0000775000175100017510000000000013245511554022015 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/locale/en_GB/LC_MESSAGES/0000775000175100017510000000000013245511554023602 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/locale/en_GB/LC_MESSAGES/networking_ovn.po0000666000175100017510000003377313245511145027226 0ustar zuulzuul00000000000000# Andi Chandler , 2016. #zanata # Andi Chandler , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: networking-ovn 4.0.0.0b3.dev24\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2017-12-14 16:58+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-12-13 12:29+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en-GB\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #, python-format msgid "%(match)s does not exist in %(column)s of %(table)s" msgstr "%(match)s does not exist in %(column)s of %(table)s" #, python-format msgid "Address set %s does not exist. Can't delete." msgstr "Address set %s does not exist. Can't delete." #, python-format msgid "Address set %s does not exist. Can't update addresses" msgstr "Address set %s does not exist. Can't update addresses" #, python-format msgid "Address set %s does not exist. Can't update external IDs" msgstr "Address set %s does not exist. Can't update external IDs" msgid "Allow to perform insecure SSL (https) requests to nova metadata" msgstr "Allow to perform insecure SSL (https) requests to nova metadata" msgid "An unknown error has occurred. Please try your request again." msgstr "An unknown error has occurred. Please try your request again." msgid "Certificate Authority public key (CA cert) file for ssl" msgstr "Certificate Authority public key (CA cert) file for SSL" #, python-format msgid "Chassis %s does not exist. Can't update external IDs" msgstr "Chassis %s does not exist. Can't update external IDs" #, python-format msgid "Chassis with hostname %s does not exist" msgstr "Chassis with hostname %s does not exist" msgid "Client certificate for nova metadata api server." msgstr "Client certificate for Nova metadata API server." msgid "Could not find LISTEN port." msgstr "Could not find LISTEN port." msgid "Currently only supports delete by lport-name" msgstr "Currently only supports delete by lport-name" #, python-format msgid "DHCP Options row %s does not exist" msgstr "DHCP Options row %s does not exist" msgid "Default least time (in seconds) to use with OVN's native DHCP service." msgstr "Default least time (in seconds) to use with OVN's native DHCP service." msgid "" "Enable distributed floating IP support.\n" "If True, the NAT action for floating IPs will be done locally and not in the " "centralized gateway. This saves the path to the external network. This " "requires the user to configure the physical network map (i.e. ovn-bridge-" "mappings) on each compute node." msgstr "" "Enable distributed Floating IP support.\n" "If True, the NAT action for Floating IPs will be done locally and not in the " "centralised gateway. This saves the path to the external network. This " "requires the user to configure the physical network map (i.e. ovn-bridge-" "mappings) on each compute node." msgid "" "Group (gid or name) running metadata proxy after its initialization (if " "empty: agent effective group)." msgstr "" "Group (gid or name) running metadata proxy after its initialisation (if " "empty: agent effective group)." msgid "IP address or DNS name of Nova metadata server." msgstr "IP address or DNS name of Nova metadata server." #, python-format msgid "Invalid binding:profile. %(key)s %(value)s value invalid type" msgstr "Invalid binding:profile. %(key)s %(value)s value invalid type" #, python-format msgid "Invalid binding:profile. %s are all required." msgstr "Invalid binding:profile. %s are all required." #, python-format msgid "" "Invalid binding:profile. tag \"%s\" must be an integer between 0 and 4095, " "inclusive" msgstr "" "Invalid binding:profile. tag \"%s\" must be an integer between 0 and 4095, " "inclusive" msgid "Invalid binding:profile. too many parameters" msgstr "Invalid binding:profile. too many parameters" #, python-format msgid "Invalid group/gid: '%s'" msgstr "Invalid group/gid: '%s'" #, python-format msgid "Invalid user/uid: '%s'" msgstr "Invalid user/uid: '%s'" msgid "Location for Metadata Proxy UNIX domain socket." msgstr "Location for Metadata Proxy UNIX domain socket." #, python-format msgid "Logical Router %s does not exist" msgstr "Logical Router %s does not exist" #, python-format msgid "Logical Router Port %s does not exist" msgstr "Logical Router Port %s does not exist" #, python-format msgid "Logical Router Port with name \"%s\" already exists." msgstr "Logical Router Port with name \"%s\" already exists." #, python-format msgid "Logical Switch %s does not exist" msgstr "Logical Switch %s does not exist" #, python-format msgid "Logical Switch Port %s does not exist" msgstr "Logical Switch Port %s does not exist" msgid "" "Metadata Proxy UNIX domain socket mode, 4 values allowed: 'deduce': deduce " "mode from metadata_proxy_user/group values, 'user': set metadata proxy " "socket mode to 0o644, to use when metadata_proxy_user is agent effective " "user or root, 'group': set metadata proxy socket mode to 0o664, to use when " "metadata_proxy_group is agent effective group or root, 'all': set metadata " "proxy socket mode to 0o666, to use otherwise." msgstr "" "Metadata Proxy UNIX domain socket mode, 4 values allowed: 'deduce': deduce " "mode from metadata_proxy_user/group values, 'user': set metadata proxy " "socket mode to 0o644, to use when metadata_proxy_user is agent effective " "user or root, 'group': set metadata proxy socket mode to 0o664, to use when " "metadata_proxy_group is agent effective group or root, 'all': set metadata " "proxy socket mode to 0o666, to use otherwise." msgid "Name of Open vSwitch bridge to use" msgstr "Name of Open vSwitch bridge to use" #, python-format msgid "Network type %s is not supported" msgstr "Network type %s is not supported" msgid "Number of backlog requests to configure the metadata server socket with" msgstr "" "Number of backlog requests to configure the metadata server socket with" msgid "" "Number of separate worker processes for metadata server (defaults to half of " "the number of CPUs)" msgstr "" "Number of separate worker processes for metadata server (defaults to half of " "the number of CPUs)" #, python-format msgid "" "OVS database connection to %(db_schema)s failed with error: '%(error)s'. " "Verify that the OVS and OVN services are available and that the " "'ovn_nb_connection' and 'ovn_sb_connection' configuration options are " "correct." msgstr "" "OVS database connection to %(db_schema)s failed with error: '%(error)s'. " "Verify that the OVS and OVN services are available and that the " "'ovn_nb_connection' and 'ovn_sb_connection' configuration options are " "correct." #, python-format msgid "Port %s does not exist" msgstr "Port %s does not exist" #, python-format msgid "Port %s does not exist. Can't update external IDs" msgstr "Port %s does not exist. Can't update external IDs" msgid "Private key of client certificate." msgstr "Private key of client certificate." msgid "Protocol to access nova metadata, http or https" msgstr "Protocol to access Nova metadata, http or https" msgid "Remote metadata server experienced an internal server error." msgstr "Remote metadata server experienced an internal server error." msgid "TCP Port used by Nova metadata server." msgstr "TCP Port used by Nova metadata server." msgid "" "The OVN L3 Scheduler type used to schedule router gateway ports on " "hypervisors/chassis. \n" "leastloaded - chassis with fewest gateway ports selected \n" "chance - chassis randomly selected" msgstr "" "The OVN L3 Scheduler type used to schedule router gateway ports on " "hypervisors/chassis. \n" "leastloaded - chassis with fewest gateway ports selected \n" "chance - chassis randomly selected" msgid "" "The PEM file with CA certificate that OVN should use to verify certificates " "presented to it by SSL peers" msgstr "" "The PEM file with CA certificate that OVN should use to verify certificates " "presented to it by SSL peers" msgid "" "The PEM file with certificate that certifies the private key specified in " "ovn_nb_private_key" msgstr "" "The PEM file with certificate that certifies the private key specified in " "ovn_nb_private_key" msgid "" "The PEM file with certificate that certifies the private key specified in " "ovn_sb_private_key" msgstr "" "The PEM file with certificate that certifies the private key specified in " "ovn_sb_private_key" msgid "The PEM file with private key for SSL connection to OVN-NB-DB" msgstr "The PEM file with private key for SSL connection to OVN-NB-DB" msgid "The PEM file with private key for SSL connection to OVN-SB-DB" msgstr "The PEM file with private key for SSL connection to OVN-SB-DB" msgid "" "The connection string for the OVN_Northbound OVSDB.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use ssl:IP:PORT for SSL connection. The ovn_nb_private_key, " "ovn_nb_certificate and ovn_nb_ca_cert are mandatory.\n" "Use unix:FILE for unix domain socket connection." msgstr "" "The connection string for the OVN_Northbound OVSDB.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use ssl:IP:PORT for SSL connection. The ovn_nb_private_key, " "ovn_nb_certificate and ovn_nb_ca_cert are mandatory.\n" "Use unix:FILE for unix domain socket connection." msgid "" "The connection string for the OVN_Southbound OVSDB.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use ssl:IP:PORT for SSL connection. The ovn_sb_private_key, " "ovn_sb_certificate and ovn_sb_ca_cert are mandatory.\n" "Use unix:FILE for unix domain socket connection." msgstr "" "The connection string for the OVN_Southbound OVSDB.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use ssl:IP:PORT for SSL connection. The ovn_sb_private_key, " "ovn_sb_certificate and ovn_sb_ca_cert are mandatory.\n" "Use unix:FILE for Unix domain socket connection." msgid "" "The connection string for the native OVSDB backend.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use unix:FILE for unix domain socket connection." msgstr "" "The connection string for the native OVSDB backend.\n" "Use tcp:IP:PORT for TCP connection.\n" "Use unix:FILE for unix domain socket connection." msgid "" "The directory in which vhost virtio socket is created by all the vswitch " "daemons" msgstr "" "The directory in which vhost virtio socket is created by all the vswitch " "daemons" msgid "The log level used for OVSDB" msgstr "The log level used for OVSDB" msgid "" "The probe interval in for the OVSDB session in milliseconds. If this is " "zero, it disables the connection keepalive feature. If non-zero the value " "will be forced to at least 1000 milliseconds. Probing is disabled by default." msgstr "" "The probe interval in for the OVSDB session in milliseconds. If this is " "zero, it disables the connection keepalive feature. If non-zero the value " "will be forced to at least 1000 milliseconds. Probing is disabled by default." #, python-format msgid "" "The protocol \"%(protocol)s\" is not supported. Valid protocols are: " "%(valid_protocols); or protocol numbers ranging from 0 to 255." msgstr "" "The protocol \"%(protocol)s\" is not supported. Valid protocols are: " "%(valid_protocols); or protocol numbers ranging from 0 to 255." msgid "" "The synchronization mode of OVN_Northbound OVSDB with Neutron DB.\n" "off - synchronization is off \n" "log - during neutron-server startup, check to see if OVN is in sync with the " "Neutron database. Log warnings for any inconsistencies found so that an " "admin can investigate \n" "repair - during neutron-server startup, automatically create resources found " "in Neutron but not in OVN. Also remove resources from OVN that are no longer " "in Neutron." msgstr "" "The synchronization mode of OVN_Northbound OVSDB with Neutron DB.\n" "off - synchronisation is off \n" "log - during neutron-server startup, check to see if OVN is in sync with the " "Neutron database. Log warnings for any inconsistencies found so that an " "admin can investigate \n" "repair - during neutron-server startup, automatically create resources found " "in Neutron but not in OVN. Also remove resources from OVN that are no longer " "in Neutron." msgid "Timeout in seconds for the OVSDB connection transaction" msgstr "Timeout in seconds for the OVSDB connection transaction" #, python-format msgid "" "Type of VIF to be used for ports valid values are (%(ovs)s, %(dpdk)s) " "default %(ovs)s" msgstr "" "Type of VIF to be used for ports valid values are (%(ovs)s, %(dpdk)s) " "default %(ovs)s" #, python-format msgid "Unexpected response code: %s" msgstr "Unexpected response code: %s" #, python-format msgid "" "Updating device_owner for port %(port_id)s owned by %(device_owner)s is not " "supported" msgstr "" "Updating device_owner for port %(port_id)s owned by %(device_owner)s is not " "supported" #, python-format msgid "" "Updating device_owner to %(device_owner)s for port %(port_id)s is not " "supported" msgstr "" "Updating device_owner to %(device_owner)s for port %(port_id)s is not " "supported" msgid "" "User (uid or name) running metadata proxy after its initialization (if " "empty: agent effective user)." msgstr "" "User (uid or name) running metadata proxy after its initialisation (if " "empty: agent effective user)." msgid "" "When proxying metadata requests, Neutron signs the Instance-ID header with a " "shared secret to prevent spoofing. You may select any string for a secret, " "but it must match here and in the configuration used by the Nova Metadata " "Server. NOTE: Nova uses the same config key, but in [neutron] section." msgstr "" "When proxying metadata requests, Neutron signs the Instance-ID header with a " "shared secret to prevent spoofing. You may select any string for a secret, " "but it must match here and in the configuration used by the Nova Metadata " "Server. NOTE: Nova uses the same config key, but in [neutron] section." msgid "" "Whether to use OVN native L3 support. Do not change the value for existing " "deployments that contain routers." msgstr "" "Whether to use OVN native L3 support. Do not change the value for existing " "deployments that contain routers." msgid "Whether to use metadata service." msgstr "Whether to use metadata service." networking-ovn-4.0.0/networking_ovn/ovn_db_sync.py0000666000175100017510000013362113245511145022465 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 datetime import datetime import itertools from eventlet import greenthread from neutron.services.segments import db as segments_db from neutron_lib.api.definitions import l3 from neutron_lib.api.definitions import provider_net as pnet from neutron_lib import constants from neutron_lib import context from neutron_lib import exceptions as n_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from neutron_lib.utils import helpers from oslo_log import log import six from networking_ovn.common import acl as acl_utils from networking_ovn.common import config from networking_ovn.common import constants as const from networking_ovn.common import ovn_client from networking_ovn.common import utils LOG = log.getLogger(__name__) SYNC_MODE_OFF = 'off' SYNC_MODE_LOG = 'log' SYNC_MODE_REPAIR = 'repair' @six.add_metaclass(abc.ABCMeta) class OvnDbSynchronizer(object): def __init__(self, core_plugin, ovn_api, ovn_driver): self.ovn_driver = ovn_driver self.ovn_api = ovn_api self.core_plugin = core_plugin def sync(self, delay_seconds=10): self._gt = greenthread.spawn_after_local(delay_seconds, self.do_sync) @abc.abstractmethod def do_sync(self): """Method to sync the OVN DB.""" def stop(self): try: self._gt.kill() except AttributeError: # Haven't started syncing pass class OvnNbSynchronizer(OvnDbSynchronizer): """Synchronizer class for NB.""" def __init__(self, core_plugin, ovn_api, sb_ovn, mode, ovn_driver): super(OvnNbSynchronizer, self).__init__( core_plugin, ovn_api, ovn_driver) self.mode = mode self.l3_plugin = directory.get_plugin(plugin_constants.L3) self._ovn_client = ovn_client.OVNClient(ovn_api, sb_ovn) def stop(self): if utils.is_ovn_l3(self.l3_plugin): self.l3_plugin._ovn.ovsdb_connection.stop() self.l3_plugin._sb_ovn.ovsdb_connection.stop() super(OvnNbSynchronizer, self).stop() def do_sync(self): if self.mode == SYNC_MODE_OFF: LOG.debug("Neutron sync mode is off") return LOG.debug("Starting OVN-Northbound DB sync process") ctx = context.get_admin_context() self.sync_address_sets(ctx) self.sync_networks_ports_and_dhcp_opts(ctx) self.sync_port_dns_records(ctx) self.sync_acls(ctx) self.sync_routers_and_rports(ctx) def _create_port_in_ovn(self, ctx, port): # Remove any old ACLs for the port to avoid creating duplicate ACLs. self.ovn_api.delete_acl( utils.ovn_name(port['network_id']), port['id']).execute(check_error=True) # Create the port in OVN. This will include ACL and Address Set # updates as needed. self._ovn_client.create_port(port) def remove_common_acls(self, neutron_acls, nb_acls): """Take out common acls of the two acl dictionaries. @param neutron_acls: neutron dictionary of port vs acls @type neutron_acls: {} @param nb_acls: nb dictionary of port vs acls @type nb_acls: {} @return: Nothing, original dictionary modified """ for port in neutron_acls.keys(): for acl in list(neutron_acls[port]): if port in nb_acls and acl in nb_acls[port]: neutron_acls[port].remove(acl) nb_acls[port].remove(acl) def compute_address_set_difference(self, neutron_sgs, nb_sgs): neutron_sgs_name_set = set(neutron_sgs.keys()) nb_sgs_name_set = set(nb_sgs.keys()) sgnames_to_add = list(neutron_sgs_name_set - nb_sgs_name_set) sgnames_to_delete = list(nb_sgs_name_set - neutron_sgs_name_set) sgs_common = list(neutron_sgs_name_set & nb_sgs_name_set) sgs_to_update = {} for sg_name in sgs_common: neutron_addr_set = set(neutron_sgs[sg_name]['addresses']) nb_addr_set = set(nb_sgs[sg_name]['addresses']) addrs_to_add = list(neutron_addr_set - nb_addr_set) addrs_to_delete = list(nb_addr_set - neutron_addr_set) if addrs_to_add or addrs_to_delete: sgs_to_update[sg_name] = {'name': sg_name, 'addrs_add': addrs_to_add, 'addrs_remove': addrs_to_delete} return sgnames_to_add, sgnames_to_delete, sgs_to_update def get_acls(self, context): """create the list of ACLS in OVN. @param context: neutron_lib.context @type context: object of type neutron_lib.context.Context @var lswitch_names: List of lswitch names @var acl_list: List of NB acls @var acl_list_dict: Dictionary of acl-lists based on lport as key @return: acl_list-dict """ lswitch_names = set([]) for network in self.core_plugin.get_networks(context): lswitch_names.add(network['id']) acl_dict, ignore1, ignore2 = \ self.ovn_api.get_acls_for_lswitches(lswitch_names) acl_list = list(itertools.chain(*acl_dict.values())) acl_list_dict = {} for acl in acl_list: key = acl['lport'] if key in acl_list_dict: acl_list_dict[key].append(acl) else: acl_list_dict[key] = list([acl]) return acl_list_dict def get_address_sets(self): return self.ovn_api.get_address_sets() def sync_address_sets(self, ctx): """Sync Address Sets between neutron and NB. @param ctx: neutron_lib.context @type ctx: object of type neutron_lib.context.Context @var db_ports: List of ports from neutron DB """ LOG.debug('Address-Set-SYNC: started @ %s' % str(datetime.now())) neutron_sgs = {} with ctx.session.begin(subtransactions=True): db_sgs = self.core_plugin.get_security_groups(ctx) db_ports = self.core_plugin.get_ports(ctx) for sg in db_sgs: for ip_version in ['ip4', 'ip6']: name = utils.ovn_addrset_name(sg['id'], ip_version) neutron_sgs[name] = { 'name': name, 'addresses': [], 'external_ids': {const.OVN_SG_EXT_ID_KEY: sg['id']}} for port in db_ports: sg_ids = utils.get_lsp_security_groups(port) if port.get('fixed_ips') and sg_ids: addresses = acl_utils.acl_port_ips(port) for sg_id in sg_ids: for ip_version in addresses: name = utils.ovn_addrset_name(sg_id, ip_version) neutron_sgs[name]['addresses'].extend( addresses[ip_version]) nb_sgs = self.get_address_sets() sgnames_to_add, sgnames_to_delete, sgs_to_update =\ self.compute_address_set_difference(neutron_sgs, nb_sgs) LOG.debug('Address_Sets added %d, removed %d, updated %d', len(sgnames_to_add), len(sgnames_to_delete), len(sgs_to_update)) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Address-Set-SYNC: transaction started @ %s' % str(datetime.now())) with self.ovn_api.transaction(check_error=True) as txn: for sgname in sgnames_to_add: sg = neutron_sgs[sgname] txn.add(self.ovn_api.create_address_set(**sg)) for sgname, sg in sgs_to_update.items(): txn.add(self.ovn_api.update_address_set(**sg)) for sgname in sgnames_to_delete: txn.add(self.ovn_api.delete_address_set(name=sgname)) LOG.debug('Address-Set-SYNC: transaction finished @ %s' % str(datetime.now())) def sync_acls(self, ctx): """Sync ACLs between neutron and NB. @param ctx: neutron_lib.context @type ctx: object of type neutron_lib.context.Context @var db_ports: List of ports from neutron DB @var neutron_acls: neutron dictionary of port vs list-of-acls @var nb_acls: NB dictionary of port vs list-of-acls @var subnet_cache: cache for subnets @return: Nothing """ LOG.debug('ACL-SYNC: started @ %s' % str(datetime.now())) db_ports = {} for port in self.core_plugin.get_ports(ctx): db_ports[port['id']] = port sg_cache = {} subnet_cache = {} neutron_acls = {} for port_id, port in db_ports.items(): if utils.get_lsp_security_groups(port): acl_list = acl_utils.add_acls(self.core_plugin, ctx, port, sg_cache, subnet_cache, self.ovn_api) if port_id in neutron_acls: neutron_acls[port_id].extend(acl_list) else: neutron_acls[port_id] = acl_list nb_acls = self.get_acls(ctx) self.remove_common_acls(neutron_acls, nb_acls) num_acls_to_add = len(list(itertools.chain(*neutron_acls.values()))) num_acls_to_remove = len(list(itertools.chain(*nb_acls.values()))) if 0 != num_acls_to_add or 0 != num_acls_to_remove: LOG.warning('ACLs-to-be-added %(add)d ' 'ACLs-to-be-removed %(remove)d', {'add': num_acls_to_add, 'remove': num_acls_to_remove}) if self.mode == SYNC_MODE_REPAIR: with self.ovn_api.transaction(check_error=True) as txn: for acla in list(itertools.chain(*neutron_acls.values())): LOG.warning('ACL found in Neutron but not in ' 'OVN DB for port %s', acla['lport']) txn.add(self.ovn_api.add_acl(**acla)) with self.ovn_api.transaction(check_error=True) as txn: for aclr in list(itertools.chain(*nb_acls.values())): # Both lswitch and lport aren't needed within the ACL. lswitchr = aclr.pop('lswitch').replace('neutron-', '') lportr = aclr.pop('lport') aclr_dict = {lportr: aclr} LOG.warning('ACLs found in OVN DB but not in ' 'Neutron for port %s', lportr) txn.add(self.ovn_api.update_acls( [lswitchr], [lportr], aclr_dict, need_compare=False, is_add_acl=False )) LOG.debug('ACL-SYNC: finished @ %s' % str(datetime.now())) def _calculate_fips_differences(self, ovn_fips, db_fips): to_add = [] to_remove = [] for db_fip in db_fips: for ovn_fip in ovn_fips: if (ovn_fip['logical_ip'] == db_fip['fixed_ip_address'] and ovn_fip['external_ip'] == db_fip['floating_ip_address']): break else: to_add.append(db_fip) for ovn_fip in ovn_fips: for db_fip in db_fips: if (ovn_fip['logical_ip'] == db_fip['fixed_ip_address'] and ovn_fip['external_ip'] == db_fip['floating_ip_address']): break else: to_remove.append(ovn_fip) return to_add, to_remove def sync_routers_and_rports(self, ctx): """Sync Routers between neutron and NB. @param ctx: neutron_lib.context @type ctx: object of type neutron_lib.context.Context @var db_routers: List of Routers from neutron DB @var db_router_ports: List of Router ports from neutron DB @var lrouters: NB dictionary of logical routers and the corresponding logical router ports. vs list-of-acls @var del_lrouters_list: List of Routers that need to be deleted from NB @var del_lrouter_ports_list: List of Router ports that need to be deleted from NB @return: Nothing """ if not utils.is_ovn_l3(self.l3_plugin): LOG.debug("OVN L3 mode is disabled, skipping " "sync routers and router ports") return LOG.debug('OVN-NB Sync Routers and Router ports started @ %s' % str(datetime.now())) db_routers = {} db_extends = {} db_router_ports = {} for router in self.l3_plugin.get_routers(ctx): db_routers[router['id']] = router db_extends[router['id']] = {} db_extends[router['id']]['routes'] = [] db_extends[router['id']]['snats'] = [] db_extends[router['id']]['fips'] = [] if not router.get(l3.EXTERNAL_GW_INFO): continue gw_info = self._ovn_client._get_gw_info(ctx, router) if gw_info.gateway_ip: db_extends[router['id']]['routes'].append( {'destination': '0.0.0.0/0', 'nexthop': gw_info.gateway_ip}) if gw_info.router_ip and utils.is_snat_enabled(router): networks = ( self._ovn_client._get_v4_network_of_all_router_ports( ctx, router['id'])) for network in networks: db_extends[router['id']]['snats'].append({ 'logical_ip': network, 'external_ip': gw_info.router_ip, 'type': 'snat'}) fips = self.l3_plugin.get_floatingips( ctx, {'router_id': list(db_routers.keys())}) for fip in fips: db_extends[fip['router_id']]['fips'].append(fip) interfaces = self.l3_plugin._get_sync_interfaces( ctx, list(db_routers.keys()), [constants.DEVICE_OWNER_ROUTER_INTF, constants.DEVICE_OWNER_ROUTER_GW, constants.DEVICE_OWNER_DVR_INTERFACE, constants.DEVICE_OWNER_ROUTER_HA_INTF, constants.DEVICE_OWNER_HA_REPLICATED_INT]) for interface in interfaces: db_router_ports[interface['id']] = interface lrouters = self.ovn_api.get_all_logical_routers_with_rports() del_lrouters_list = [] del_lrouter_ports_list = [] update_sroutes_list = [] update_lrport_list = [] update_snats_list = [] update_fips_list = [] for lrouter in lrouters: if lrouter['name'] in db_routers: for lrport, lrport_nets in lrouter['ports'].items(): if lrport in db_router_ports: # We dont have to check for the networks and # ipv6_ra_configs values. Lets add it to the # update_lrport_list. If they are in sync, then # update_router_port will be a no-op. update_lrport_list.append(db_router_ports[lrport]) del db_router_ports[lrport] else: del_lrouter_ports_list.append( {'port': lrport, 'lrouter': lrouter['name']}) if 'routes' in db_routers[lrouter['name']]: db_routes = db_routers[lrouter['name']]['routes'] else: db_routes = [] if 'routes' in db_extends[lrouter['name']]: db_routes.extend(db_extends[lrouter['name']]['routes']) ovn_routes = lrouter['static_routes'] add_routes, del_routes = helpers.diff_list_of_dict( ovn_routes, db_routes) update_sroutes_list.append({'id': lrouter['name'], 'add': add_routes, 'del': del_routes}) ovn_fips = lrouter['dnat_and_snats'] db_fips = db_extends[lrouter['name']]['fips'] add_fips, del_fips = self._calculate_fips_differences( ovn_fips, db_fips) update_fips_list.append({'id': lrouter['name'], 'add': add_fips, 'del': del_fips}) ovn_nats = lrouter['snats'] db_snats = db_extends[lrouter['name']]['snats'] add_snats, del_snats = helpers.diff_list_of_dict( ovn_nats, db_snats) update_snats_list.append({'id': lrouter['name'], 'add': add_snats, 'del': del_snats}) del db_routers[lrouter['name']] else: del_lrouters_list.append(lrouter) for r_id, router in db_routers.items(): LOG.warning("Router found in Neutron but not in " "OVN DB, router id=%s", router['id']) if self.mode == SYNC_MODE_REPAIR: try: LOG.warning("Creating the router %s in OVN NB DB", router['id']) self._ovn_client.create_router( router, add_external_gateway=False) if 'routes' in router: update_sroutes_list.append( {'id': router['id'], 'add': router['routes'], 'del': []}) if 'routes' in db_extends[router['id']]: update_sroutes_list.append( {'id': router['id'], 'add': db_extends[router['id']]['routes'], 'del': []}) if 'snats' in db_extends[router['id']]: update_snats_list.append( {'id': router['id'], 'add': db_extends[router['id']]['snats'], 'del': []}) if 'fips' in db_extends[router['id']]: update_fips_list.append( {'id': router['id'], 'add': db_extends[router['id']]['fips'], 'del': []}) except RuntimeError: LOG.warning("Create router in OVN NB failed for router %s", router['id']) for rp_id, rrport in db_router_ports.items(): LOG.warning("Router Port found in Neutron but not in OVN " "DB, router port_id=%s", rrport['id']) if self.mode == SYNC_MODE_REPAIR: try: LOG.warning("Creating the router port %s in OVN NB DB", rrport['id']) self._ovn_client.create_router_port( rrport['device_id'], rrport) except RuntimeError: LOG.warning("Create router port in OVN " "NB failed for router port %s", rrport['id']) for rport in update_lrport_list: LOG.warning("Router Port port_id=%s needs to be updated " "for networks changed", rport['id']) if self.mode == SYNC_MODE_REPAIR: try: LOG.warning( "Updating networks on router port %s in OVN NB DB", rport['id']) self._ovn_client.update_router_port(rport) except RuntimeError: LOG.warning("Update router port networks in OVN " "NB failed for router port %s", rport['id']) with self.ovn_api.transaction(check_error=True) as txn: for lrouter in del_lrouters_list: LOG.warning("Router found in OVN but not in " "Neutron, router id=%s", lrouter['name']) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Deleting the router %s from OVN NB DB", lrouter['name']) txn.add(self.ovn_api.delete_lrouter( utils.ovn_name(lrouter['name']))) for lrport_info in del_lrouter_ports_list: LOG.warning("Router Port found in OVN but not in " "Neutron, port_id=%s", lrport_info['port']) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Deleting the port %s from OVN NB DB", lrport_info['port']) txn.add(self.ovn_api.delete_lrouter_port( utils.ovn_lrouter_port_name(lrport_info['port']), utils.ovn_name(lrport_info['lrouter']), if_exists=False)) for sroute in update_sroutes_list: if sroute['add']: LOG.warning("Router %(id)s static routes %(route)s " "found in Neutron but not in OVN", {'id': sroute['id'], 'route': sroute['add']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Add static routes %s to OVN NB DB", sroute['add']) for route in sroute['add']: txn.add(self.ovn_api.add_static_route( utils.ovn_name(sroute['id']), ip_prefix=route['destination'], nexthop=route['nexthop'])) if sroute['del']: LOG.warning("Router %(id)s static routes %(route)s " "found in OVN but not in Neutron", {'id': sroute['id'], 'route': sroute['del']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Delete static routes %s from OVN NB DB", sroute['del']) for route in sroute['del']: txn.add(self.ovn_api.delete_static_route( utils.ovn_name(sroute['id']), ip_prefix=route['destination'], nexthop=route['nexthop'])) for fip in update_fips_list: if fip['del']: LOG.warning("Router %(id)s floating ips %(fip)s " "found in OVN but not in Neutron", {'id': fip['id'], 'fip': fip['del']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning( "Delete floating ips %s from OVN NB DB", fip['del']) for nat in fip['del']: self._ovn_client._delete_floatingip( nat, utils.ovn_name(fip['id']), txn=txn) if fip['add']: LOG.warning("Router %(id)s floating ips %(fip)s " "found in Neutron but not in OVN", {'id': fip['id'], 'fip': fip['add']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Add floating ips %s to OVN NB DB", fip['add']) for nat in fip['add']: self._ovn_client._create_or_update_floatingip( nat, txn=txn) for snat in update_snats_list: if snat['del']: LOG.warning("Router %(id)s snat %(snat)s " "found in OVN but not in Neutron", {'id': snat['id'], 'snat': snat['del']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Delete snats %s from OVN NB DB", snat['del']) for nat in snat['del']: txn.add(self.ovn_api.delete_nat_rule_in_lrouter( utils.ovn_name(snat['id']), logical_ip=nat['logical_ip'], external_ip=nat['external_ip'], type='snat')) if snat['add']: LOG.warning("Router %(id)s snat %(snat)s " "found in Neutron but not in OVN", {'id': snat['id'], 'snat': snat['add']}) if self.mode == SYNC_MODE_REPAIR: LOG.warning("Add snats %s to OVN NB DB", snat['add']) for nat in snat['add']: txn.add(self.ovn_api.add_nat_rule_in_lrouter( utils.ovn_name(snat['id']), logical_ip=nat['logical_ip'], external_ip=nat['external_ip'], type='snat')) LOG.debug('OVN-NB Sync routers and router ports finished %s' % str(datetime.now())) def _sync_subnet_dhcp_options(self, ctx, db_networks, ovn_subnet_dhcp_options): LOG.debug('OVN-NB Sync DHCP options for Neutron subnets started') db_subnets = {} filters = {'enable_dhcp': [1]} for subnet in self.core_plugin.get_subnets(ctx, filters=filters): if subnet['ip_version'] == constants.IP_VERSION_6 and ( subnet.get('ipv6_address_mode') == constants.IPV6_SLAAC): continue db_subnets[subnet['id']] = subnet del_subnet_dhcp_opts_list = [] for subnet_id, ovn_dhcp_opts in ovn_subnet_dhcp_options.items(): if subnet_id in db_subnets: network = db_networks[utils.ovn_name( db_subnets[subnet_id]['network_id'])] if constants.IP_VERSION_6 == db_subnets[subnet_id][ 'ip_version']: server_mac = ovn_dhcp_opts['options'].get('server_id') else: server_mac = ovn_dhcp_opts['options'].get('server_mac') dhcp_options = self._ovn_client._get_ovn_dhcp_options( db_subnets[subnet_id], network, server_mac=server_mac) # Verify that the cidr and options are also in sync. if dhcp_options['cidr'] == ovn_dhcp_opts['cidr'] and ( dhcp_options['options'] == ovn_dhcp_opts['options']): del db_subnets[subnet_id] else: db_subnets[subnet_id]['ovn_dhcp_options'] = dhcp_options else: del_subnet_dhcp_opts_list.append(ovn_dhcp_opts) for subnet_id, subnet in db_subnets.items(): LOG.warning('DHCP options for subnet %s is present in ' 'Neutron but out of sync for OVN', subnet_id) if self.mode == SYNC_MODE_REPAIR: try: LOG.debug('Adding/Updating DHCP options for subnet %s in ' ' OVN NB DB', subnet_id) network = db_networks[utils.ovn_name(subnet['network_id'])] # _ovn_client._add_subnet_dhcp_options doesn't create # a new row in DHCP_Options if the row already exists. # See commands.AddDHCPOptionsCommand. self._ovn_client._add_subnet_dhcp_options( subnet, network, subnet.get('ovn_dhcp_options')) except RuntimeError: LOG.warning('Adding/Updating DHCP options for subnet ' '%s failed in OVN NB DB', subnet_id) txn_commands = [] for dhcp_opt in del_subnet_dhcp_opts_list: LOG.warning('Out of sync subnet DHCP options for subnet %s ' 'found in OVN NB DB which needs to be deleted', dhcp_opt['external_ids']['subnet_id']) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Deleting subnet DHCP options for subnet %s ', dhcp_opt['external_ids']['subnet_id']) txn_commands.append(self.ovn_api.delete_dhcp_options( dhcp_opt['uuid'])) if txn_commands: with self.ovn_api.transaction(check_error=True) as txn: for cmd in txn_commands: txn.add(cmd) LOG.debug('OVN-NB Sync DHCP options for Neutron subnets finished') def _sync_port_dhcp_options(self, ctx, ports_need_sync_dhcp_opts, ovn_port_dhcpv4_opts, ovn_port_dhcpv6_opts): LOG.debug('OVN-NB Sync DHCP options for Neutron ports with extra ' 'dhcp options assigned started') txn_commands = [] lsp_dhcp_key = {constants.IP_VERSION_4: 'dhcpv4_options', constants.IP_VERSION_6: 'dhcpv6_options'} ovn_port_dhcp_opts = {constants.IP_VERSION_4: ovn_port_dhcpv4_opts, constants.IP_VERSION_6: ovn_port_dhcpv6_opts} for port in ports_need_sync_dhcp_opts: if self.mode == SYNC_MODE_REPAIR: LOG.debug('Updating DHCP options for port %s in OVN NB DB', port['id']) set_lsp = {} for ip_v in [constants.IP_VERSION_4, constants.IP_VERSION_6]: dhcp_opts = ( self._ovn_client._get_port_dhcp_options( port, ip_v)) if not dhcp_opts or 'uuid' in dhcp_opts: # If the Logical_Switch_Port.dhcpv4_options or # dhcpv6_options no longer refers a port dhcp options # created in DHCP_Options earlier, that port dhcp # options will be deleted in the following # ovn_port_dhcp_options handling. set_lsp[lsp_dhcp_key[ip_v]] = [ dhcp_opts['uuid']] if dhcp_opts else [] else: # If port has extra port dhcp # options, a command will returned by # self._ovn_client._get_port_dhcp_options # to add or update port dhcp options. ovn_port_dhcp_opts[ip_v].pop(port['id'], None) dhcp_options = dhcp_opts['cmd'] txn_commands.append(dhcp_options) set_lsp[lsp_dhcp_key[ip_v]] = dhcp_options if set_lsp: txn_commands.append(self.ovn_api.set_lswitch_port( lport_name=port['id'], **set_lsp)) for ip_v in [constants.IP_VERSION_4, constants.IP_VERSION_6]: for port_id, dhcp_opt in ovn_port_dhcp_opts[ip_v].items(): LOG.warning( 'Out of sync port DHCPv%(ip_version)d options for ' '(subnet %(subnet_id)s port %(port_id)s) found in OVN ' 'NB DB which needs to be deleted', {'ip_version': ip_v, 'subnet_id': dhcp_opt['external_ids']['subnet_id'], 'port_id': port_id}) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Deleting port DHCPv%d options for (subnet %s, ' 'port %s)', ip_v, dhcp_opt['external_ids']['subnet_id'], port_id) txn_commands.append(self.ovn_api.delete_dhcp_options( dhcp_opt['uuid'])) if txn_commands: with self.ovn_api.transaction(check_error=True) as txn: for cmd in txn_commands: txn.add(cmd) LOG.debug('OVN-NB Sync DHCP options for Neutron ports with extra ' 'dhcp options assigned finished') def _sync_metadata_ports(self, ctx, db_ports): """Ensure metadata ports in all Neutron networks. This method will ensure that all networks have one and only one metadata port. """ if not config.is_ovn_metadata_enabled(): return LOG.debug('OVN sync metadata ports started') for net in self.core_plugin.get_networks(ctx): dhcp_ports = self.core_plugin.get_ports(ctx, filters=dict( network_id=[net['id']], device_owner=[constants.DEVICE_OWNER_DHCP])) if not dhcp_ports: LOG.warning('Missing metadata port found in Neutron for ' 'network %s', net['id']) if self.mode == SYNC_MODE_REPAIR: try: # Create the missing port in both Neutron and OVN. LOG.warning('Creating missing metadadata port in ' 'Neutron and OVN for network %s', net['id']) self._ovn_client.create_metadata_port(ctx, net) except n_exc.IpAddressGenerationFailure: LOG.error('Could not allocate IP addresses for ' 'metadata port in network %s', net['id']) continue else: # Delete all but one DHCP ports. Only one is needed for # metadata. for port in dhcp_ports[1:]: LOG.warning('Unnecessary DHCP port %s for network %s ' 'found in Neutron', port['id'], net['id']) if self.mode == SYNC_MODE_REPAIR: LOG.warning('Deleting unnecessary DHCP port %s for ' 'network %s', port['id'], net['id']) self.core_plugin.delete_port(ctx, port['id']) db_ports.pop(port['id'], None) port = dhcp_ports[0] if port['id'] in db_ports.keys(): LOG.warning('Metadata port %s for network %s found in ' 'Neutron but not in OVN', port['id'], net['id']) if self.mode == SYNC_MODE_REPAIR: LOG.warning('Creating metadata port %s for network ' '%s in OVN', port['id'], net['id']) self._create_port_in_ovn(ctx, port) db_ports.pop(port['id']) if self.mode == SYNC_MODE_REPAIR: # Make sure that this port has an IP address in all the subnets self._ovn_client.update_metadata_port(ctx, net['id']) LOG.debug('OVN sync metadata ports finished') def sync_networks_ports_and_dhcp_opts(self, ctx): LOG.debug('OVN-NB Sync networks, ports and DHCP options started') db_networks = {} for net in self.core_plugin.get_networks(ctx): db_networks[utils.ovn_name(net['id'])] = net # Ignore the floating ip ports with device_owner set to # constants.DEVICE_OWNER_FLOATINGIP db_ports = {port['id']: port for port in self.core_plugin.get_ports(ctx) if not utils.is_lsp_ignored(port)} ovn_all_dhcp_options = self.ovn_api.get_all_dhcp_options() db_network_cache = dict(db_networks) ports_need_sync_dhcp_opts = [] lswitches = self.ovn_api.get_all_logical_switches_with_ports() del_lswitchs_list = [] del_lports_list = [] add_provnet_ports_list = [] for lswitch in lswitches: if lswitch['name'] in db_networks: for lport in lswitch['ports']: if lport in db_ports: port = db_ports.pop(lport) if not utils.is_network_device_port(port): ports_need_sync_dhcp_opts.append(port) else: del_lports_list.append({'port': lport, 'lswitch': lswitch['name']}) db_network = db_networks[lswitch['name']] physnet = db_network.get(pnet.PHYSICAL_NETWORK) # Updating provider attributes is forbidden by neutron, thus # we only need to consider missing provnet-ports in OVN DB. if physnet and not lswitch['provnet_port']: add_provnet_ports_list.append( {'network': db_network, 'lswitch': lswitch['name']}) del db_networks[lswitch['name']] else: del_lswitchs_list.append(lswitch) for net_id, network in db_networks.items(): LOG.warning("Network found in Neutron but not in " "OVN DB, network_id=%s", network['id']) if self.mode == SYNC_MODE_REPAIR: try: LOG.debug('Creating the network %s in OVN NB DB', network['id']) self._ovn_client.create_network(network) except RuntimeError: LOG.warning("Create network in OVN NB failed for " "network %s", network['id']) self._sync_metadata_ports(ctx, db_ports) self._sync_subnet_dhcp_options( ctx, db_network_cache, ovn_all_dhcp_options['subnets']) for port_id, port in db_ports.items(): LOG.warning("Port found in Neutron but not in OVN " "DB, port_id=%s", port['id']) if self.mode == SYNC_MODE_REPAIR: try: LOG.debug('Creating the port %s in OVN NB DB', port['id']) self._create_port_in_ovn(ctx, port) if port_id in ovn_all_dhcp_options['ports_v4']: _, lsp_opts = utils.get_lsp_dhcp_opts( port, constants.IP_VERSION_4) if lsp_opts: ovn_all_dhcp_options['ports_v4'].pop(port_id) if port_id in ovn_all_dhcp_options['ports_v6']: _, lsp_opts = utils.get_lsp_dhcp_opts( port, constants.IP_VERSION_6) if lsp_opts: ovn_all_dhcp_options['ports_v6'].pop(port_id) except RuntimeError: LOG.warning("Create port in OVN NB failed for" " port %s", port['id']) with self.ovn_api.transaction(check_error=True) as txn: for lswitch in del_lswitchs_list: LOG.warning("Network found in OVN but not in " "Neutron, network_id=%s", lswitch['name']) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Deleting the network %s from OVN NB DB', lswitch['name']) txn.add(self.ovn_api.ls_del(lswitch['name'])) for provnet_port_info in add_provnet_ports_list: network = provnet_port_info['network'] LOG.warning("Provider network found in Neutron but " "provider network port not found in OVN DB, " "network_id=%s", provnet_port_info['lswitch']) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Creating the provnet port %s in OVN NB DB', utils.ovn_provnet_port_name(network['id'])) self._ovn_client._create_provnet_port( txn, network, network.get(pnet.PHYSICAL_NETWORK), network.get(pnet.SEGMENTATION_ID)) for lport_info in del_lports_list: LOG.warning("Port found in OVN but not in " "Neutron, port_id=%s", lport_info['port']) if self.mode == SYNC_MODE_REPAIR: LOG.debug('Deleting the port %s from OVN NB DB', lport_info['port']) txn.add(self.ovn_api.delete_lswitch_port( lport_name=lport_info['port'], lswitch_name=lport_info['lswitch'])) if lport_info['port'] in ovn_all_dhcp_options['ports_v4']: LOG.debug('Deleting port DHCPv4 options for (port %s)', lport_info['port']) txn.add(self.ovn_api.delete_dhcp_options( ovn_all_dhcp_options['ports_v4'].pop( lport_info['port'])['uuid'])) if lport_info['port'] in ovn_all_dhcp_options['ports_v6']: LOG.debug('Deleting port DHCPv6 options for (port %s)', lport_info['port']) txn.add(self.ovn_api.delete_dhcp_options( ovn_all_dhcp_options['ports_v6'].pop( lport_info['port'])['uuid'])) self._sync_port_dhcp_options(ctx, ports_need_sync_dhcp_opts, ovn_all_dhcp_options['ports_v4'], ovn_all_dhcp_options['ports_v6']) LOG.debug('OVN-NB Sync networks, ports and DHCP options finished') def sync_port_dns_records(self, ctx): if self.mode != SYNC_MODE_REPAIR: return LOG.debug('OVN-NB Sync port dns records') # Ignore the floating ip ports with device_owner set to # constants.DEVICE_OWNER_FLOATINGIP db_ports = [port for port in self.core_plugin.get_ports(ctx) if not port.get('device_owner', '').startswith( constants.DEVICE_OWNER_FLOATINGIP)] dns_records = {} for port in db_ports: if self._ovn_client.is_dns_required_for_port(port): port_dns_records = self._ovn_client.get_port_dns_records(port) if port['network_id'] not in dns_records: dns_records[port['network_id']] = {} dns_records[port['network_id']].update(port_dns_records) for network_id, port_dns_records in dns_records.items(): self._set_dns_records(network_id, port_dns_records) def _set_dns_records(self, network_id, dns_records): lswitch_name = utils.ovn_name(network_id) ls, ls_dns_record = self.ovn_api.get_ls_and_dns_record(lswitch_name) with self.ovn_api.transaction(check_error=True) as txn: if not ls_dns_record: dns_add_txn = txn.add(self.ovn_api.dns_add( external_ids={'ls_name': ls.name}, records=dns_records)) txn.add(self.ovn_api.ls_set_dns_records(ls.uuid, dns_add_txn)) else: txn.add(self.ovn_api.dns_set_records(ls_dns_record.uuid, **dns_records)) class OvnSbSynchronizer(OvnDbSynchronizer): """Synchronizer class for SB.""" def __init__(self, core_plugin, ovn_api, ovn_driver): super(OvnSbSynchronizer, self).__init__( core_plugin, ovn_api, ovn_driver) self.l3_plugin = directory.get_plugin(plugin_constants.L3) def do_sync(self): """Method to sync the OVN_Southbound DB with neutron DB. OvnSbSynchronizer will sync data from OVN_Southbound to neutron. And the synchronization will always be performed, no matter what mode it is. """ LOG.debug("Starting OVN-Southbound DB sync process") ctx = context.get_admin_context() self.sync_hostname_and_physical_networks(ctx) if utils.is_ovn_l3(self.l3_plugin): self.l3_plugin.schedule_unhosted_gateways() def sync_hostname_and_physical_networks(self, ctx): LOG.debug('OVN-SB Sync hostname and physical networks started') host_phynets_map = self.ovn_api.get_chassis_hostname_and_physnets() current_hosts = set(host_phynets_map) previous_hosts = segments_db.get_hosts_mapped_with_segments(ctx) stale_hosts = previous_hosts - current_hosts for host in stale_hosts: LOG.debug('Stale host %s found in Neutron, but not in OVN SB DB. ' 'Clear its SegmentHostMapping in Neutron', host) self.ovn_driver.update_segment_host_mapping(host, []) new_hosts = current_hosts - previous_hosts for host in new_hosts: LOG.debug('New host %s found in OVN SB DB, but not in Neutron. ' 'Add its SegmentHostMapping in Neutron', host) self.ovn_driver.update_segment_host_mapping( host, host_phynets_map[host]) for host in current_hosts & previous_hosts: LOG.debug('Host %s found both in OVN SB DB and Neutron. ' 'Trigger updating its SegmentHostMapping in Neutron, ' 'to keep OVN SB DB and Neutron have consistent data', host) self.ovn_driver.update_segment_host_mapping( host, host_phynets_map[host]) LOG.debug('OVN-SB Sync hostname and physical networks finished') networking-ovn-4.0.0/networking_ovn/ml2/0000775000175100017510000000000013245511554020276 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/ml2/trunk_driver.py0000666000175100017510000001112613245511145023365 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib import context as n_context from neutron_lib import exceptions as n_exc from oslo_config import cfg from oslo_db import exception as os_db_exc from oslo_log import log from networking_ovn.common.constants import OVN_ML2_MECH_DRIVER_NAME from neutron.services.trunk import constants as trunk_consts from neutron.services.trunk.drivers import base as trunk_base from neutron_lib.api.definitions import portbindings SUPPORTED_INTERFACES = ( portbindings.VIF_TYPE_OVS, portbindings.VIF_TYPE_VHOST_USER, ) SUPPORTED_SEGMENTATION_TYPES = ( trunk_consts.VLAN, ) LOG = log.getLogger(__name__) class OVNTrunkHandler(object): def __init__(self, plugin_driver): self.plugin_driver = plugin_driver def _set_binding_profile(self, port_id, parent_port, tag=None): context = n_context.get_admin_context() binding_profile = {} if parent_port and tag: binding_profile = {'parent_name': parent_port, 'tag': tag} port = {'port': {'binding:profile': binding_profile}} try: self.plugin_driver._plugin.update_port(context, port_id, port) except (os_db_exc.DBReferenceError, n_exc.PortNotFound): LOG.debug("Port not found trying to set binding_profile: %s", port_id) def _set_sub_ports(self, parent_port, subports): for port in subports: self._set_binding_profile(port.port_id, parent_port, tag=port.segmentation_id) def _unset_sub_ports(self, subports): for port in subports: self._set_binding_profile(port.port_id, None) def trunk_created(self, trunk): self._set_sub_ports(trunk.port_id, trunk.sub_ports) trunk.update(status=trunk_consts.ACTIVE_STATUS) def trunk_deleted(self, trunk): self._unset_sub_ports(trunk.sub_ports) def subports_added(self, trunk, subports): self._set_sub_ports(trunk.port_id, subports) trunk.update(status=trunk_consts.ACTIVE_STATUS) def subports_deleted(self, trunk, subports): self._unset_sub_ports(subports) trunk.update(status=trunk_consts.ACTIVE_STATUS) def trunk_event(self, resource, event, trunk_plugin, payload): if event == events.AFTER_CREATE: self.trunk_created(payload.current_trunk) elif event == events.AFTER_DELETE: self.trunk_deleted(payload.original_trunk) def subport_event(self, resource, event, trunk_plugin, payload): if event == events.AFTER_CREATE: self.subports_added(payload.original_trunk, payload.subports) elif event == events.AFTER_DELETE: self.subports_deleted(payload.original_trunk, payload.subports) class OVNTrunkDriver(trunk_base.DriverBase): @property def is_loaded(self): try: return OVN_ML2_MECH_DRIVER_NAME in cfg.CONF.ml2.mechanism_drivers except cfg.NoSuchOptError: return False @registry.receives(trunk_consts.TRUNK_PLUGIN, [events.AFTER_INIT]) def register(self, resource, event, trigger, payload=None): super(OVNTrunkDriver, self).register( resource, event, trigger, payload=payload) self._handler = OVNTrunkHandler(self.plugin_driver) for trunk_event in (events.AFTER_CREATE, events.AFTER_DELETE): registry.subscribe(self._handler.trunk_event, trunk_consts.TRUNK, trunk_event) registry.subscribe(self._handler.subport_event, trunk_consts.SUBPORTS, trunk_event) @classmethod def create(cls, plugin_driver): cls.plugin_driver = plugin_driver return cls(OVN_ML2_MECH_DRIVER_NAME, SUPPORTED_INTERFACES, SUPPORTED_SEGMENTATION_TYPES, None, can_trunk_bound_port=True) networking-ovn-4.0.0/networking_ovn/ml2/mech_driver.py0000666000175100017510000010642013245511145023140 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 threading from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as const from neutron_lib import context as n_context from neutron_lib import exceptions as n_exc from neutron_lib.plugins import directory from neutron_lib.plugins.ml2 import api from neutron_lib.services.qos import constants as qos_consts from oslo_config import cfg from oslo_db import exception as os_db_exc from oslo_log import log from neutron.common import utils as n_utils from neutron.db import provisioning_blocks from neutron.services.segments import db as segment_service_db from networking_ovn._i18n import _ from networking_ovn.common import acl as ovn_acl from networking_ovn.common import config from networking_ovn.common import constants as ovn_const from networking_ovn.common import maintenance from networking_ovn.common import ovn_client from networking_ovn.common import utils from networking_ovn.db import revision as db_rev from networking_ovn.ml2 import qos_driver from networking_ovn.ml2 import trunk_driver from networking_ovn import ovn_db_sync from networking_ovn.ovsdb import impl_idl_ovn from networking_ovn.ovsdb import ovsdb_monitor LOG = log.getLogger(__name__) METADATA_READY_WAIT_TIMEOUT = 15 class MetadataServiceReadyWaitTimeoutException(Exception): pass class OVNPortUpdateError(n_exc.BadRequest): pass class OVNMechanismDriver(api.MechanismDriver): """OVN ML2 mechanism driver A mechanism driver is called on the creation, update, and deletion of networks and ports. For every event, there are two methods that get called - one within the database transaction (method suffix of _precommit), one right afterwards (method suffix of _postcommit). Exceptions raised by methods called inside the transaction can rollback, but should not make any blocking calls (for example, REST requests to an outside controller). Methods called after transaction commits can make blocking external calls, though these will block the entire process. Exceptions raised in calls after the transaction commits may cause the associated resource to be deleted. Because rollback outside of the transaction is not done in the update network/port case, all data validation must be done within methods that are part of the database transaction. """ supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT] def initialize(self): """Perform driver initialization. Called after all drivers have been loaded and the database has been initialized. No abstract methods defined below will be called prior to this method being called. """ LOG.info("Starting OVNMechanismDriver") self._nb_ovn = None self._sb_ovn = None self._plugin_property = None self._ovn_client_inst = None self._maintenance_thread = None self.sg_enabled = ovn_acl.is_sg_enabled() self._post_fork_event = threading.Event() if cfg.CONF.SECURITYGROUP.firewall_driver: LOG.warning('Firewall driver configuration is ignored') self._setup_vif_port_bindings() self.subscribe() self.qos_driver = qos_driver.OVNQosNotificationDriver.create(self) self.trunk_driver = trunk_driver.OVNTrunkDriver.create(self) @property def _plugin(self): if self._plugin_property is None: self._plugin_property = directory.get_plugin() return self._plugin_property @property def _ovn_client(self): if self._ovn_client_inst is None: if not(self._nb_ovn and self._sb_ovn): # Wait until the post_fork_initialize method has finished and # IDLs have been correctly setup. self._post_fork_event.wait() self._ovn_client_inst = ovn_client.OVNClient(self._nb_ovn, self._sb_ovn) return self._ovn_client_inst def _setup_vif_port_bindings(self): self.supported_vnic_types = [portbindings.VNIC_NORMAL, portbindings.VNIC_DIRECT] self.vif_details = { portbindings.VIF_TYPE_OVS: { portbindings.CAP_PORT_FILTER: self.sg_enabled }, portbindings.VIF_TYPE_VHOST_USER: { portbindings.CAP_PORT_FILTER: False, portbindings.VHOST_USER_MODE: portbindings.VHOST_USER_MODE_CLIENT, portbindings.VHOST_USER_OVS_PLUG: True } } def subscribe(self): registry.subscribe(self.post_fork_initialize, resources.PROCESS, events.AFTER_INIT) registry.subscribe(self._add_segment_host_mapping_for_segment, resources.SEGMENT, events.PRECOMMIT_CREATE) # Handle security group/rule notifications if self.sg_enabled: registry.subscribe(self._create_security_group_precommit, resources.SECURITY_GROUP, events.PRECOMMIT_CREATE) registry.subscribe(self._update_security_group, resources.SECURITY_GROUP, events.AFTER_UPDATE) registry.subscribe(self._create_security_group, resources.SECURITY_GROUP, events.AFTER_CREATE) registry.subscribe(self._delete_security_group, resources.SECURITY_GROUP, events.AFTER_DELETE) registry.subscribe(self._create_sg_rule_precommit, resources.SECURITY_GROUP_RULE, events.PRECOMMIT_CREATE) registry.subscribe(self._process_sg_rule_notification, resources.SECURITY_GROUP_RULE, events.AFTER_CREATE) registry.subscribe(self._process_sg_rule_notification, resources.SECURITY_GROUP_RULE, events.BEFORE_DELETE) def post_fork_initialize(self, resource, event, trigger, payload=None): # NOTE(rtheis): This will initialize all workers (API, RPC, # plugin service and OVN) with OVN IDL connections. self._post_fork_event.clear() self._ovn_client_inst = None self._nb_ovn, self._sb_ovn = impl_idl_ovn.get_ovn_idls(self, trigger) # Now IDL connections can be safely used. self._post_fork_event.set() if trigger.im_class == ovsdb_monitor.OvnWorker: # Call the synchronization task if its ovn worker # This sync neutron DB to OVN-NB DB only in inconsistent states self.nb_synchronizer = ovn_db_sync.OvnNbSynchronizer( self._plugin, self._nb_ovn, self._sb_ovn, config.get_ovn_neutron_sync_mode(), self ) self.nb_synchronizer.sync() # This sync neutron DB to OVN-SB DB only in inconsistent states self.sb_synchronizer = ovn_db_sync.OvnSbSynchronizer( self._plugin, self._sb_ovn, self ) self.sb_synchronizer.sync() if trigger.im_class == maintenance.MaintenanceWorker: self._maintenance_thread = maintenance.MaintenanceThread() self._maintenance_thread.add_periodics( maintenance.DBInconsistenciesPeriodics(self._ovn_client)) self._maintenance_thread.start() def _create_security_group_precommit(self, resource, event, trigger, security_group, context, **kwargs): db_rev.create_initial_revision( security_group['id'], ovn_const.TYPE_SECURITY_GROUPS, context.session) def _create_security_group(self, resource, event, trigger, security_group, **kwargs): self._ovn_client.create_security_group(security_group) def _delete_security_group(self, resource, event, trigger, security_group_id, **kwargs): self._ovn_client.delete_security_group(security_group_id) def _update_security_group(self, resource, event, trigger, security_group, **kwargs): # OVN doesn't care about updates to security groups, only if they # exist or not. We are bumping the revision number here so it # doesn't show as inconsistent to the maintenance periodic task db_rev.bump_revision(security_group, ovn_const.TYPE_SECURITY_GROUPS) def _create_sg_rule_precommit(self, resource, event, trigger, **kwargs): sg_rule = kwargs.get('security_group_rule') context = kwargs.get('context') db_rev.create_initial_revision(sg_rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES, context.session) def _process_sg_rule_notification( self, resource, event, trigger, **kwargs): if event == events.AFTER_CREATE: self._ovn_client.create_security_group_rule( kwargs.get('security_group_rule')) elif event == events.BEFORE_DELETE: admin_context = n_context.get_admin_context() sg_rule = self._plugin.get_security_group_rule( admin_context, kwargs.get('security_group_rule_id')) self._ovn_client.delete_security_group_rule(sg_rule) def _is_network_type_supported(self, network_type): return (network_type in [const.TYPE_LOCAL, const.TYPE_FLAT, const.TYPE_GENEVE, const.TYPE_VLAN]) def _validate_network_segments(self, network_segments): for network_segment in network_segments: network_type = network_segment['network_type'] segmentation_id = network_segment['segmentation_id'] physical_network = network_segment['physical_network'] LOG.debug('Validating network segment with ' 'type %(network_type)s, ' 'segmentation ID %(segmentation_id)s, ' 'physical network %(physical_network)s' % {'network_type': network_type, 'segmentation_id': segmentation_id, 'physical_network': physical_network}) if not self._is_network_type_supported(network_type): msg = _('Network type %s is not supported') % network_type raise n_exc.InvalidInput(error_message=msg) def create_network_precommit(self, context): """Allocate resources for a new network. :param context: NetworkContext instance describing the new network. Create a new network, allocating resources as necessary in the database. Called inside transaction context on session. Call cannot block. Raising an exception will result in a rollback of the current transaction. """ self._validate_network_segments(context.network_segments) db_rev.create_initial_revision( context.current['id'], ovn_const.TYPE_NETWORKS, context._plugin_context.session) def create_network_postcommit(self, context): """Create a network. :param context: NetworkContext instance describing the new network. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. """ network = context.current self._ovn_client.create_network(network) def update_network_precommit(self, context): """Update resources of a network. :param context: NetworkContext instance describing the new state of the network, as well as the original state prior to the update_network call. Update values of a network, updating the associated resources in the database. Called inside transaction context on session. Raising an exception will result in rollback of the transaction. update_network_precommit is called for all changes to the network state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. """ self._validate_network_segments(context.network_segments) def update_network_postcommit(self, context): """Update a network. :param context: NetworkContext instance describing the new state of the network, as well as the original state prior to the update_network call. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will cause the deletion of the resource. update_network_postcommit is called for all changes to the network state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. """ # FIXME(lucasagomes): We can delete this conditional after # https://bugs.launchpad.net/neutron/+bug/1739798 is fixed. if context._plugin_context.session.is_active: return self._ovn_client.update_network(context.current) def delete_network_postcommit(self, context): """Delete a network. :param context: NetworkContext instance describing the current state of the network, prior to the call to delete it. Called after the transaction commits. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Runtime errors are not expected, and will not prevent the resource from being deleted. """ self._ovn_client.delete_network(context.current['id']) def create_subnet_precommit(self, context): db_rev.create_initial_revision( context.current['id'], ovn_const.TYPE_SUBNETS, context._plugin_context.session) def create_subnet_postcommit(self, context): self._ovn_client.create_subnet(context.current, context.network.current) def update_subnet_postcommit(self, context): self._ovn_client.update_subnet( context.current, context.network.current) def delete_subnet_postcommit(self, context): self._ovn_client.delete_subnet(context.current['id']) def create_port_precommit(self, context): """Allocate resources for a new port. :param context: PortContext instance describing the port. Create a new port, allocating resources as necessary in the database. Called inside transaction context on session. Call cannot block. Raising an exception will result in a rollback of the current transaction. """ port = context.current if utils.is_lsp_ignored(port): return utils.validate_and_get_data_from_binding_profile(port) if self._is_port_provisioning_required(port, context.host): self._insert_port_provisioning_block(context._plugin_context, port) db_rev.create_initial_revision(port['id'], ovn_const.TYPE_PORTS, context._plugin_context.session) # in the case of router ports we also need to # track the creation and update of the LRP OVN objects if utils.is_lsp_router_port(port): db_rev.create_initial_revision(port['id'], ovn_const.TYPE_ROUTER_PORTS, context._plugin_context.session) def _is_port_provisioning_required(self, port, host, original_host=None): vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) if vnic_type not in self.supported_vnic_types: LOG.debug('No provisioning block for port %(port_id)s due to ' 'unsupported vnic_type: %(vnic_type)s', {'port_id': port['id'], 'vnic_type': vnic_type}) return False if port['status'] == const.PORT_STATUS_ACTIVE: LOG.debug('No provisioning block for port %s since it is active', port['id']) return False if not host: LOG.debug('No provisioning block for port %s since it does not ' 'have a host', port['id']) return False if host == original_host: LOG.debug('No provisioning block for port %s since host unchanged', port['id']) return False if not self._sb_ovn.chassis_exists(host): LOG.debug('No provisioning block for port %(port_id)s since no ' 'OVN chassis for host: %(host)s', {'port_id': port['id'], 'host': host}) return False return True def _insert_port_provisioning_block(self, context, port): # Insert a provisioning block to prevent the port from # transitioning to active until OVN reports back that # the port is up. provisioning_blocks.add_provisioning_component( context, port['id'], resources.PORT, provisioning_blocks.L2_AGENT_ENTITY ) def _notify_dhcp_updated(self, port_id): """Notifies Neutron that the DHCP has been update for port.""" if provisioning_blocks.is_object_blocked( n_context.get_admin_context(), port_id, resources.PORT): provisioning_blocks.provisioning_complete( n_context.get_admin_context(), port_id, resources.PORT, provisioning_blocks.DHCP_ENTITY) def _validate_ignored_port(self, port, original_port): if utils.is_lsp_ignored(port): if not utils.is_lsp_ignored(original_port): # From not ignored port to ignored port msg = (_('Updating device_owner to %(device_owner)s for port ' '%(port_id)s is not supported') % {'device_owner': port['device_owner'], 'port_id': port['id']}) raise OVNPortUpdateError(resource='port', msg=msg) elif utils.is_lsp_ignored(original_port): # From ignored port to not ignored port msg = (_('Updating device_owner for port %(port_id)s owned by ' '%(device_owner)s is not supported') % {'port_id': port['id'], 'device_owner': original_port['device_owner']}) raise OVNPortUpdateError(resource='port', msg=msg) def create_port_postcommit(self, context): """Create a port. :param context: PortContext instance describing the port. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will result in the deletion of the resource. """ port = context.current self._ovn_client.create_port(port) self._notify_dhcp_updated(port['id']) def update_port_precommit(self, context): """Update resources of a port. :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. Called inside transaction context on session to complete a port update as defined by this mechanism driver. Raising an exception will result in rollback of the transaction. update_port_precommit is called for all changes to the port state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. """ port = context.current original_port = context.original self._validate_ignored_port(port, original_port) utils.validate_and_get_data_from_binding_profile(port) if self._is_port_provisioning_required(port, context.host, context.original_host): self._insert_port_provisioning_block(context._plugin_context, port) if utils.is_lsp_router_port(port): # handle the case when an existing port is added to a # logical router so we need to track the creation of the lrp if not utils.is_lsp_router_port(original_port): db_rev.create_initial_revision(port['id'], ovn_const.TYPE_ROUTER_PORTS, context._plugin_context.session) def update_port_postcommit(self, context): """Update a port. :param context: PortContext instance describing the new state of the port, as well as the original state prior to the update_port call. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Raising an exception will result in the deletion of the resource. update_port_postcommit is called for all changes to the port state. It is up to the mechanism driver to ignore state or state changes that it does not know or care about. """ port = context.current original_port = context.original self._ovn_client.update_port(port, port_object=original_port) self._notify_dhcp_updated(port['id']) def delete_port_postcommit(self, context): """Delete a port. :param context: PortContext instance describing the current state of the port, prior to the call to delete it. Called after the transaction completes. Call can block, though will block the entire process so care should be taken to not drastically affect performance. Runtime errors are not expected, and will not prevent the resource from being deleted. """ port = context.current self._ovn_client.delete_port(port['id'], port_object=port) def bind_port(self, context): """Attempt to bind a port. :param context: PortContext instance describing the port This method is called outside any transaction to attempt to establish a port binding using this mechanism driver. Bindings may be created at each of multiple levels of a hierarchical network, and are established from the top level downward. At each level, the mechanism driver determines whether it can bind to any of the network segments in the context.segments_to_bind property, based on the value of the context.host property, any relevant port or network attributes, and its own knowledge of the network topology. At the top level, context.segments_to_bind contains the static segments of the port's network. At each lower level of binding, it contains static or dynamic segments supplied by the driver that bound at the level above. If the driver is able to complete the binding of the port to any segment in context.segments_to_bind, it must call context.set_binding with the binding details. If it can partially bind the port, it must call context.continue_binding with the network segments to be used to bind at the next lower level. If the binding results are committed after bind_port returns, they will be seen by all mechanism drivers as update_port_precommit and update_port_postcommit calls. But if some other thread or process concurrently binds or updates the port, these binding results will not be committed, and update_port_precommit and update_port_postcommit will not be called on the mechanism drivers with these results. Because binding results can be discarded rather than committed, drivers should avoid making persistent state changes in bind_port, or else must ensure that such state changes are eventually cleaned up. Implementing this method explicitly declares the mechanism driver as having the intention to bind ports. This is inspected by the QoS service to identify the available QoS rules you can use with ports. """ port = context.current vnic_type = port.get(portbindings.VNIC_TYPE, portbindings.VNIC_NORMAL) if vnic_type not in self.supported_vnic_types: LOG.debug('Refusing to bind port %(port_id)s due to unsupported ' 'vnic_type: %(vnic_type)s' % {'port_id': port['id'], 'vnic_type': vnic_type}) return profile = port.get(portbindings.PROFILE) capabilities = [] if profile: capabilities = profile.get('capabilities', []) if (vnic_type == portbindings.VNIC_DIRECT and 'switchdev' not in capabilities): LOG.debug("Refusing to bind port due to unsupported vnic_type: %s" "with no switchdev capability", portbindings.VNIC_DIRECT) return # OVN chassis information is needed to ensure a valid port bind. # Collect port binding data and refuse binding if the OVN chassis # cannot be found. chassis_physnets = [] try: datapath_type, iface_types, chassis_physnets = \ self._sb_ovn.get_chassis_data_for_ml2_bind_port(context.host) iface_types = iface_types.split(',') if iface_types else [] except RuntimeError: LOG.debug('Refusing to bind port %(port_id)s due to ' 'no OVN chassis for host: %(host)s' % {'port_id': port['id'], 'host': context.host}) return for segment_to_bind in context.segments_to_bind: network_type = segment_to_bind['network_type'] segmentation_id = segment_to_bind['segmentation_id'] physical_network = segment_to_bind['physical_network'] LOG.debug('Attempting to bind port %(port_id)s on host %(host)s ' 'for network segment with type %(network_type)s, ' 'segmentation ID %(segmentation_id)s, ' 'physical network %(physical_network)s' % {'port_id': port['id'], 'host': context.host, 'network_type': network_type, 'segmentation_id': segmentation_id, 'physical_network': physical_network}) # TODO(rtheis): This scenario is only valid on an upgrade from # neutron ML2 OVS since invalid network types are prevented during # network creation and update. The upgrade should convert invalid # network types. Once bug/1621879 is fixed, refuse to bind # ports with unsupported network types. if not self._is_network_type_supported(network_type): LOG.info('Upgrade allowing bind port %(port_id)s with ' 'unsupported network type: %(network_type)s', {'port_id': port['id'], 'network_type': network_type}) if (network_type in ['flat', 'vlan']) and \ (physical_network not in chassis_physnets): LOG.info('Refusing to bind port %(port_id)s on ' 'host %(host)s due to the OVN chassis ' 'bridge mapping physical networks ' '%(chassis_physnets)s not supporting ' 'physical network: %(physical_network)s', {'port_id': port['id'], 'host': context.host, 'chassis_physnets': chassis_physnets, 'physical_network': physical_network}) else: if datapath_type == ovn_const.CHASSIS_DATAPATH_NETDEV and ( ovn_const.CHASSIS_IFACE_DPDKVHOSTUSER in iface_types): vhost_user_socket = utils.ovn_vhu_sockpath( config.get_ovn_vhost_sock_dir(), port['id']) vif_type = portbindings.VIF_TYPE_VHOST_USER port[portbindings.VIF_DETAILS].update({ portbindings.VHOST_USER_SOCKET: vhost_user_socket }) vif_details = dict(self.vif_details[vif_type]) vif_details[portbindings.VHOST_USER_SOCKET] = \ vhost_user_socket else: vif_type = portbindings.VIF_TYPE_OVS vif_details = self.vif_details[vif_type] context.set_binding(segment_to_bind[api.ID], vif_type, vif_details) break def get_workers(self): """Get any worker instances that should have their own process Any driver that needs to run processes separate from the API or RPC workers, can return a sequence of worker instances. """ # See doc/source/design/ovn_worker.rst for more details. return [ovsdb_monitor.OvnWorker(), maintenance.MaintenanceWorker()] def _update_subport_host_if_needed(self, port_id): parent_port = self._ovn_client.get_parent_port(port_id) if parent_port: admin_context = n_context.get_admin_context() try: port = self._plugin.get_port(admin_context, parent_port) host_id = port.get(portbindings.HOST_ID, '') subport = {'port': {'binding:host_id': host_id}} self._plugin.update_port(admin_context, port_id, subport) except (os_db_exc.DBReferenceError, n_exc.PortNotFound): LOG.debug("Error trying to set host_id %s for subport %s", host_id, port_id) def set_port_status_up(self, port_id): # Port provisioning is complete now that OVN has reported that the # port is up. Any provisioning block (possibly added during port # creation or when OVN reports that the port is down) must be removed. LOG.info("OVN reports status up for port: %s", port_id) self._wait_for_metadata_provisioned_if_needed(port_id) # If this port is a subport, we need to update the host_id and set it # to its parent's. Otherwise, Neutron won't even try to bind it and # it will not transition from DOWN to ACTIVE. self._update_subport_host_if_needed(port_id) provisioning_blocks.provisioning_complete( n_context.get_admin_context(), port_id, resources.PORT, provisioning_blocks.L2_AGENT_ENTITY) def set_port_status_down(self, port_id): # Port provisioning is required now that OVN has reported that the # port is down. Insert a provisioning block and mark the port down # in neutron. The block is inserted before the port status update # to prevent another entity from bypassing the block with its own # port status update. LOG.info("OVN reports status down for port: %s", port_id) admin_context = n_context.get_admin_context() try: port = self._plugin.get_port(admin_context, port_id) self._insert_port_provisioning_block(admin_context, port) self._plugin.update_port_status(admin_context, port['id'], const.PORT_STATUS_DOWN) except (os_db_exc.DBReferenceError, n_exc.PortNotFound): LOG.debug("Port not found during OVN status down report: %s", port_id) def update_segment_host_mapping(self, host, phy_nets): """Update SegmentHostMapping in DB""" if not host: return ctx = n_context.get_admin_context() segments = segment_service_db.get_segments_with_phys_nets( ctx, phy_nets) available_seg_ids = { segment['id'] for segment in segments if segment['network_type'] in ('flat', 'vlan')} segment_service_db.update_segment_host_mapping( ctx, host, available_seg_ids) def _add_segment_host_mapping_for_segment(self, resource, event, trigger, context, segment): phynet = segment.physical_network if not phynet: return host_phynets_map = self._sb_ovn.get_chassis_hostname_and_physnets() hosts = {host for host, phynets in host_phynets_map.items() if phynet in phynets} segment_service_db.map_segment_to_hosts(context, segment.id, hosts) def _wait_for_metadata_provisioned_if_needed(self, port_id): """Wait for metadata service to be provisioned. Wait until metadata service has been setup for this port in the chassis it resides. If metadata is disabled, this function will return right away. """ if config.is_ovn_metadata_enabled() and self._sb_ovn: # Wait until metadata service has been setup for this port in the # chassis it resides. result = ( self._sb_ovn.get_logical_port_chassis_and_datapath(port_id)) if not result: LOG.warning("Logical port %s doesn't exist in OVN", port_id) return chassis, datapath = result if not chassis: LOG.warning("Logical port %s is not bound to a " "chassis", port_id) return try: n_utils.wait_until_true( lambda: datapath in self._sb_ovn.get_chassis_metadata_networks(chassis), timeout=METADATA_READY_WAIT_TIMEOUT, exception=MetadataServiceReadyWaitTimeoutException) except MetadataServiceReadyWaitTimeoutException: # If we reach this point it means that metadata agent didn't # provision the datapath for this port on its chassis. Either # the agent is not running or it crashed. We'll complete the # provisioning block though. LOG.warning("Metadata service is not ready for port %s, check" " networking-ovn-metadata-agent status/logs.", port_id) networking-ovn-4.0.0/networking_ovn/ml2/__init__.py0000666000175100017510000000000013245511145022373 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/ml2/qos_driver.py0000666000175100017510000001410113245511164023021 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 as logging from neutron_lib.api.definitions import portbindings from neutron_lib import constants from neutron_lib import context as n_context from neutron_lib.db import constants as db_consts from neutron_lib.plugins import directory from neutron_lib.services.qos import base from neutron_lib.services.qos import constants as qos_consts from neutron.objects.qos import policy as qos_policy from neutron.objects.qos import rule as qos_rule from networking_ovn.common import utils LOG = logging.getLogger(__name__) OVN_QOS = 'qos' SUPPORTED_RULES = { qos_consts.RULE_TYPE_BANDWIDTH_LIMIT: { qos_consts.MAX_KBPS: { 'type:range': [0, db_consts.DB_INTEGER_MAX_VALUE]}, qos_consts.MAX_BURST: { 'type:range': [0, db_consts.DB_INTEGER_MAX_VALUE]}, qos_consts.DIRECTION: { 'type:values': [constants.EGRESS_DIRECTION]} }, } VIF_TYPES = [portbindings.VIF_TYPE_OVS, portbindings.VIF_TYPE_VHOST_USER] VNIC_TYPES = [portbindings.VNIC_NORMAL] class OVNQosNotificationDriver(base.DriverBase): """OVN notification driver for QoS.""" def __init__(self, name='OVNQosDriver', vif_types=VIF_TYPES, vnic_types=VNIC_TYPES, supported_rules=SUPPORTED_RULES, requires_rpc_notifications=False): super(OVNQosNotificationDriver, self).__init__( name, vif_types, vnic_types, supported_rules, requires_rpc_notifications) @classmethod def create(cls, plugin_driver): cls._driver = plugin_driver return cls() @property def is_loaded(self): return OVN_QOS in cfg.CONF.ml2.extension_drivers def create_policy(self, context, policy): # No need to update OVN on create pass def update_policy(self, context, policy): # Call into OVN client to update the policy self._driver._ovn_client._qos_driver.update_policy(context, policy) def delete_policy(self, context, policy): # No need to update OVN on delete pass class OVNQosDriver(object): """Qos driver for OVN""" def __init__(self, driver): LOG.info("Starting OVNQosDriver") super(OVNQosDriver, self).__init__() self._driver = driver self._plugin_property = None @property def _plugin(self): if self._plugin_property is None: self._plugin_property = directory.get_plugin() return self._plugin_property def _generate_port_options(self, context, policy_id): if policy_id is None: return {} options = {} # The policy might not have any rules all_rules = qos_rule.get_rules(context, policy_id) for rule in all_rules: if isinstance(rule, qos_rule.QosBandwidthLimitRule): if rule.max_kbps: options['qos_max_rate'] = str(rule.max_kbps * 1000) if rule.max_burst_kbps: options['qos_burst'] = str(rule.max_burst_kbps * 1000) return options def get_qos_options(self, port): # Is qos service enabled if 'qos_policy_id' not in port: return {} # Don't apply qos rules to network devices if utils.is_network_device_port(port): return {} # Determine if port or network policy should be used context = n_context.get_admin_context() port_policy_id = port.get('qos_policy_id') network_policy_id = None if not port_policy_id: network_policy = qos_policy.QosPolicy.get_network_policy( context, port['network_id']) network_policy_id = network_policy.id if network_policy else None # Generate qos options for the selected policy policy_id = port_policy_id or network_policy_id return self._generate_port_options(context, policy_id) def _update_network_ports(self, context, network_id, options): # Retrieve all ports for this network ports = self._plugin.get_ports(context, filters={'network_id': [network_id]}) for port in ports: # Don't apply qos rules if port has a policy port_policy_id = port.get('qos_policy_id') if port_policy_id: continue # Don't apply qos rules to network devices if utils.is_network_device_port(port): continue # Call into OVN client to update port self._driver.update_port(port, qos_options=options) def update_network(self, network): # Is qos service enabled if 'qos_policy_id' not in network: return # Update the qos options on each network port context = n_context.get_admin_context() options = self._generate_port_options( context, network['qos_policy_id']) self._update_network_ports(context, network.get('id'), options) def update_policy(self, context, policy): options = self._generate_port_options(context, policy.id) # Update each network bound to this policy network_bindings = policy.get_bound_networks() for network_id in network_bindings: self._update_network_ports(context, network_id, options) # Update each port bound to this policy port_bindings = policy.get_bound_ports() for port_id in port_bindings: port = self._plugin.get_port(context, port_id) self._driver.update_port(port, qos_options=options) networking-ovn-4.0.0/networking_ovn/tests/0000775000175100017510000000000013245511554020746 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/0000775000175100017510000000000013245511554021725 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/db/0000775000175100017510000000000013245511554022312 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/db/test_maintenance.py0000666000175100017510000001473513245511145026215 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron.api import extensions from neutron.api.v2 import attributes from neutron.common import config import neutron.extensions from neutron.services.revisions import revision_plugin from neutron.tests.unit.extensions import test_l3 from neutron.tests.unit.extensions import test_securitygroup from neutron_lib import constants as n_const from neutron_lib.db import api as db_api from networking_ovn.common import constants from networking_ovn.db import maintenance as db_maint from networking_ovn.db import revision as db_rev EXTENSIONS_PATH = ':'.join(neutron.extensions.__path__) PLUGIN_CLASS = ( 'networking_ovn.tests.unit.db.test_maintenance.TestMaintenancePlugin') class TestMaintenancePlugin(test_securitygroup.SecurityGroupTestPlugin, test_l3.TestL3NatBasePlugin): __native_pagination_support = True __native_sorting_support = True supported_extension_aliases = ['external-net', 'security-group'] class TestMaintenance(test_securitygroup.SecurityGroupsTestCase, test_l3.L3NatTestCaseMixin): def setUp(self): service_plugins = { 'router': 'neutron.tests.unit.extensions.test_l3.TestL3NatServicePlugin'} super(TestMaintenance, self).setUp(plugin=PLUGIN_CLASS, service_plugins=service_plugins) l3_plugin = test_l3.TestL3NatServicePlugin() sec_plugin = test_securitygroup.SecurityGroupTestPlugin() ext_mgr = extensions.PluginAwareExtensionManager( EXTENSIONS_PATH, {'router': l3_plugin, 'sec': sec_plugin} ) ext_mgr.extend_resources('2.0', attributes.RESOURCE_ATTRIBUTE_MAP) app = config.load_paste_app('extensions_test_app') self.ext_api = extensions.ExtensionMiddleware(app, ext_mgr=ext_mgr) self.session = db_api.get_writer_session() revision_plugin.RevisionPlugin() self.net = self._make_network(self.fmt, 'net1', True)['network'] def test_get_inconsistent_resources(self): # Set the intial revision to -1 to force it to be incosistent db_rev.create_initial_revision( self.net['id'], constants.TYPE_NETWORKS, self.session, revision_number=-1) res = db_maint.get_inconsistent_resources() self.assertEqual(1, len(res)) self.assertEqual(self.net['id'], res[0].resource_uuid) def test_get_inconsistent_resources_consistent(self): # Set the initial revision to 0 which is the initial revision_number # for recently created resources db_rev.create_initial_revision( self.net['id'], constants.TYPE_NETWORKS, self.session, revision_number=0) res = db_maint.get_inconsistent_resources() # Assert nothing is inconsistent self.assertEqual([], res) def test_get_deleted_resources(self): db_rev.create_initial_revision( self.net['id'], constants.TYPE_NETWORKS, self.session, revision_number=0) self._delete('networks', self.net['id']) res = db_maint.get_deleted_resources() self.assertEqual(1, len(res)) self.assertEqual(self.net['id'], res[0].resource_uuid) self.assertIsNone(res[0].standard_attr_id) def _prepare_resources_for_ordering_test(self, delete=False): subnet = self._make_subnet(self.fmt, {'network': self.net}, '10.0.0.1', '10.0.0.0/24')['subnet'] self._set_net_external(self.net['id']) info = {'network_id': self.net['id']} router = self._make_router(self.fmt, None, external_gateway_info=info)['router'] fip = self._make_floatingip(self.fmt, self.net['id'])['floatingip'] port = self._make_port(self.fmt, self.net['id'])['port'] sg = self._make_security_group(self.fmt, 'sg1', '')['security_group'] rule = self._build_security_group_rule( sg['id'], 'ingress', n_const.PROTO_NUM_TCP) sg_rule = self._make_security_group_rule( self.fmt, rule)['security_group_rule'] db_rev.create_initial_revision( router['id'], constants.TYPE_ROUTERS, self.session) db_rev.create_initial_revision( subnet['id'], constants.TYPE_SUBNETS, self.session) db_rev.create_initial_revision( fip['id'], constants.TYPE_FLOATINGIPS, self.session) db_rev.create_initial_revision( port['id'], constants.TYPE_PORTS, self.session) db_rev.create_initial_revision( port['id'], constants.TYPE_ROUTER_PORTS, self.session) db_rev.create_initial_revision( sg['id'], constants.TYPE_SECURITY_GROUPS, self.session) db_rev.create_initial_revision( sg_rule['id'], constants.TYPE_SECURITY_GROUP_RULES, self.session) db_rev.create_initial_revision( self.net['id'], constants.TYPE_NETWORKS, self.session) if delete: self._delete('security-group-rules', sg_rule['id']) self._delete('floatingips', fip['id']) self._delete('ports', port['id']) self._delete('security-groups', sg['id']) self._delete('routers', router['id']) self._delete('subnets', subnet['id']) self._delete('networks', self.net['id']) def test_get_inconsistent_resources_order(self): self._prepare_resources_for_ordering_test() res = db_maint.get_inconsistent_resources() actual_order = tuple(r.resource_type for r in res) self.assertEqual(constants._TYPES_PRIORITY_ORDER, actual_order) def test_get_deleted_resources_order(self): self._prepare_resources_for_ordering_test(delete=True) res = db_maint.get_deleted_resources() actual_order = tuple(r.resource_type for r in res) self.assertEqual(tuple(reversed(constants._TYPES_PRIORITY_ORDER)), actual_order) networking-ovn-4.0.0/networking_ovn/tests/unit/db/base.py0000666000175100017510000000264313245511145023601 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 neutron.db import api as db_api from neutron.tests.unit.testlib_api import SqlTestCaseLight from neutron_lib import context from sqlalchemy.orm import exc from networking_ovn.db import models class DBTestCase(SqlTestCaseLight): def setUp(self): super(DBTestCase, self).setUp() self.session = context.get_admin_context().session def tearDown(self): super(DBTestCase, self).tearDown() self.session.query(models.OVNRevisionNumbers).delete() def get_revision_row(self, resource_uuid): try: session = db_api.get_reader_session() with session.begin(): return session.query(models.OVNRevisionNumbers).filter_by( resource_uuid=resource_uuid).one() except exc.NoResultFound: pass networking-ovn-4.0.0/networking_ovn/tests/unit/db/__init__.py0000666000175100017510000000000013245511145024407 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/db/test_revision.py0000666000175100017510000000546313245511145025567 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. import mock from neutron.tests.unit.plugins.ml2 import test_plugin from neutron_lib.db import api as db_api from networking_ovn.common import constants from networking_ovn.db import revision as db_rev from networking_ovn.tests.unit.db import base as db_base class TestRevisionNumber(db_base.DBTestCase, test_plugin.Ml2PluginV2TestCase): def setUp(self): super(TestRevisionNumber, self).setUp() res = self._create_network(fmt=self.fmt, name='net', admin_state_up=True) self.net = self.deserialize(self.fmt, res)['network'] self.session = db_api.get_writer_session() def test_bump_revision(self): db_rev.create_initial_revision(self.net['id'], constants.TYPE_NETWORKS, self.session) self.net['revision_number'] = 123 db_rev.bump_revision(self.net, constants.TYPE_NETWORKS) row = self.get_revision_row(self.net['id']) self.assertEqual(123, row.revision_number) def test_bump_older_revision(self): db_rev.create_initial_revision(self.net['id'], constants.TYPE_NETWORKS, self.session, revision_number=123) self.net['revision_number'] = 1 db_rev.bump_revision(self.net, constants.TYPE_NETWORKS) # Assert the revision number wasn't bumped row = self.get_revision_row(self.net['id']) self.assertEqual(123, row.revision_number) @mock.patch.object(db_rev.LOG, 'warning') def test_bump_revision_row_not_found(self, mock_log): self.net['revision_number'] = 123 db_rev.bump_revision(self.net, constants.TYPE_NETWORKS) # Assert the revision number wasn't bumped row = self.get_revision_row(self.net['id']) self.assertEqual(123, row.revision_number) self.assertIn('No revision row found for', mock_log.call_args[0][0]) def test_delete_revision(self): db_rev.create_initial_revision(self.net['id'], constants.TYPE_NETWORKS, self.session) db_rev.delete_revision(self.net['id'], constants.TYPE_NETWORKS) row = self.get_revision_row(self.net['id']) self.assertIsNone(row) networking-ovn-4.0.0/networking_ovn/tests/unit/common/0000775000175100017510000000000013245511554023215 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/common/test_acl.py0000666000175100017510000007553013245511145025375 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import mock from neutron_lib import constants as const from networking_ovn.common import acl as ovn_acl from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils as ovn_utils from networking_ovn.ovsdb import commands as cmd from networking_ovn.tests import base from networking_ovn.tests.unit import fakes class TestACLs(base.TestCase): def setUp(self): super(TestACLs, self).setUp() self.driver = mock.Mock() self.driver._nb_ovn = fakes.FakeOvsdbNbOvnIdl() self.plugin = fakes.FakePlugin() self.admin_context = mock.Mock() self.fake_port = fakes.FakePort.create_one_port({ 'id': 'fake_port_id1', 'network_id': 'network_id1', 'fixed_ips': [{'subnet_id': 'subnet_id1', 'ip_address': '1.1.1.1'}], }).info() self.fake_subnet = fakes.FakeSubnet.create_one_subnet({ 'id': 'subnet_id1', 'ip_version': 4, 'cidr': '1.1.1.0/24', }).info() patcher = mock.patch( 'ovsdbapp.backend.ovs_idl.idlutils.row_by_value', lambda *args, **kwargs: mock.MagicMock()) patcher.start() mock.patch( "networking_ovn.common.acl._acl_columns_name_severity_supported", return_value=True ).start() def test_drop_all_ip_traffic_for_port(self): acls = ovn_acl.drop_all_ip_traffic_for_port(self.fake_port) acl_to_lport = {'action': 'drop', 'direction': 'to-lport', 'external_ids': {'neutron:lport': self.fake_port['id']}, 'log': False, 'name': [], 'severity': [], 'lport': self.fake_port['id'], 'lswitch': 'neutron-network_id1', 'match': 'outport == "fake_port_id1" && ip', 'priority': 1001} acl_from_lport = {'action': 'drop', 'direction': 'from-lport', 'external_ids': {'neutron:lport': self.fake_port['id']}, 'log': False, 'name': [], 'severity': [], 'lport': self.fake_port['id'], 'lswitch': 'neutron-network_id1', 'match': 'inport == "fake_port_id1" && ip', 'priority': 1001} for acl in acls: if 'to-lport' in acl.values(): self.assertEqual(acl_to_lport, acl) if 'from-lport' in acl.values(): self.assertEqual(acl_from_lport, acl) def test_add_acl_dhcp(self): ovn_dhcp_acls = ovn_acl.add_acl_dhcp(self.fake_port, self.fake_subnet) other_dhcp_acls = ovn_acl.add_acl_dhcp(self.fake_port, self.fake_subnet, ovn_dhcp=False) expected_match_to_lport = ( 'outport == "%s" && ip4 && ip4.src == %s && udp && udp.src == 67 ' '&& udp.dst == 68') % (self.fake_port['id'], self.fake_subnet['cidr']) acl_to_lport = {'action': 'allow', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'fake_port_id1'}, 'log': False, 'name': [], 'severity': [], 'lport': 'fake_port_id1', 'lswitch': 'neutron-network_id1', 'match': expected_match_to_lport, 'priority': 1002} expected_match_from_lport = ( 'inport == "%s" && ip4 && ' 'ip4.dst == {255.255.255.255, %s} && ' 'udp && udp.src == 68 && udp.dst == 67' ) % (self.fake_port['id'], self.fake_subnet['cidr']) acl_from_lport = {'action': 'allow', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'fake_port_id1'}, 'log': False, 'name': [], 'severity': [], 'lport': 'fake_port_id1', 'lswitch': 'neutron-network_id1', 'match': expected_match_from_lport, 'priority': 1002} self.assertEqual(1, len(ovn_dhcp_acls)) self.assertEqual(acl_from_lport, ovn_dhcp_acls[0]) self.assertEqual(2, len(other_dhcp_acls)) for acl in other_dhcp_acls: if 'to-lport' in acl.values(): self.assertEqual(acl_to_lport, acl) if 'from-lport' in acl.values(): self.assertEqual(acl_from_lport, acl) def _test_add_sg_rule_acl_for_port(self, sg_rule, direction, match): port = {'id': 'port-id', 'network_id': 'network-id'} acl = ovn_acl.add_sg_rule_acl_for_port(port, sg_rule, match) self.assertEqual({'lswitch': 'neutron-network-id', 'lport': 'port-id', 'priority': ovn_const.ACL_PRIORITY_ALLOW, 'action': ovn_const.ACL_ACTION_ALLOW_RELATED, 'log': False, 'name': [], 'severity': [], 'direction': direction, 'match': match, 'external_ids': { 'neutron:lport': 'port-id', 'neutron:security_group_rule_id': 'sgr_id'}}, acl) def test_add_sg_rule_acl_for_port_remote_ip_prefix(self): sg_rule = {'id': 'sgr_id', 'direction': 'ingress', 'ethertype': 'IPv4', 'remote_group_id': None, 'remote_ip_prefix': '1.1.1.0/24', 'protocol': None} match = 'outport == "port-id" && ip4 && ip4.src == 1.1.1.0/24' self._test_add_sg_rule_acl_for_port(sg_rule, 'to-lport', match) sg_rule['direction'] = 'egress' match = 'inport == "port-id" && ip4 && ip4.dst == 1.1.1.0/24' self._test_add_sg_rule_acl_for_port(sg_rule, 'from-lport', match) def test_add_sg_rule_acl_for_port_remote_group(self): sg_rule = {'id': 'sgr_id', 'direction': 'ingress', 'ethertype': 'IPv4', 'remote_group_id': 'sg1', 'remote_ip_prefix': None, 'protocol': None} match = 'outport == "port-id" && ip4 && (ip4.src == 1.1.1.100' \ ' || ip4.src == 1.1.1.101' \ ' || ip4.src == 1.1.1.102)' self._test_add_sg_rule_acl_for_port(sg_rule, 'to-lport', match) sg_rule['direction'] = 'egress' match = 'inport == "port-id" && ip4 && (ip4.dst == 1.1.1.100' \ ' || ip4.dst == 1.1.1.101' \ ' || ip4.dst == 1.1.1.102)' self._test_add_sg_rule_acl_for_port(sg_rule, 'from-lport', match) def test__update_acls_compute_difference(self): lswitch_name = 'lswitch-1' port1 = {'id': 'port-id1', 'network_id': lswitch_name, 'fixed_ips': [{'subnet_id': 'subnet-id', 'ip_address': '1.1.1.101'}, {'subnet_id': 'subnet-id-v6', 'ip_address': '2001:0db8::1:0:0:1'}]} port2 = {'id': 'port-id2', 'network_id': lswitch_name, 'fixed_ips': [{'subnet_id': 'subnet-id', 'ip_address': '1.1.1.102'}, {'subnet_id': 'subnet-id-v6', 'ip_address': '2001:0db8::1:0:0:2'}]} ports = [port1, port2] # OLD ACLs, allow IPv4 communication aclport1_old1 = {'priority': 1002, 'direction': 'from-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip4 && (ip.src == %s)' % (port1['id'], port1['fixed_ips'][0]['ip_address'])} aclport1_old2 = {'priority': 1002, 'direction': 'from-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip6 && (ip.src == %s)' % (port1['id'], port1['fixed_ips'][1]['ip_address'])} aclport1_old3 = {'priority': 1002, 'direction': 'to-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'ip4 && (ip.src == %s)' % (port2['fixed_ips'][0]['ip_address'])} port1_acls_old = [aclport1_old1, aclport1_old2, aclport1_old3] aclport2_old1 = {'priority': 1002, 'direction': 'from-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip4 && (ip.src == %s)' % (port2['id'], port2['fixed_ips'][0]['ip_address'])} aclport2_old2 = {'priority': 1002, 'direction': 'from-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip6 && (ip.src == %s)' % (port2['id'], port2['fixed_ips'][1]['ip_address'])} aclport2_old3 = {'priority': 1002, 'direction': 'to-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'ip4 && (ip.src == %s)' % (port1['fixed_ips'][0]['ip_address'])} port2_acls_old = [aclport2_old1, aclport2_old2, aclport2_old3] acls_old_dict = {'%s' % (port1['id']): port1_acls_old, '%s' % (port2['id']): port2_acls_old} acl_obj_dict = {str(aclport1_old1): 'row1', str(aclport1_old2): 'row2', str(aclport1_old3): 'row3', str(aclport2_old1): 'row4', str(aclport2_old2): 'row5', str(aclport2_old3): 'row6'} # NEW ACLs, allow IPv6 communication aclport1_new1 = {'priority': 1002, 'direction': 'from-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip4 && (ip.src == %s)' % (port1['id'], port1['fixed_ips'][0]['ip_address'])} aclport1_new2 = {'priority': 1002, 'direction': 'from-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip6 && (ip.src == %s)' % (port1['id'], port1['fixed_ips'][1]['ip_address'])} aclport1_new3 = {'priority': 1002, 'direction': 'to-lport', 'lport': port1['id'], 'lswitch': lswitch_name, 'match': 'ip6 && (ip.src == %s)' % (port2['fixed_ips'][1]['ip_address'])} port1_acls_new = [aclport1_new1, aclport1_new2, aclport1_new3] aclport2_new1 = {'priority': 1002, 'direction': 'from-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip4 && (ip.src == %s)' % (port2['id'], port2['fixed_ips'][0]['ip_address'])} aclport2_new2 = {'priority': 1002, 'direction': 'from-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'inport == %s && ip6 && (ip.src == %s)' % (port2['id'], port2['fixed_ips'][1]['ip_address'])} aclport2_new3 = {'priority': 1002, 'direction': 'to-lport', 'lport': port2['id'], 'lswitch': lswitch_name, 'match': 'ip6 && (ip.src == %s)' % (port1['fixed_ips'][1]['ip_address'])} port2_acls_new = [aclport2_new1, aclport2_new2, aclport2_new3] acls_new_dict = {'%s' % (port1['id']): port1_acls_new, '%s' % (port2['id']): port2_acls_new} acls_new_dict_copy = copy.deepcopy(acls_new_dict) # Invoke _compute_acl_differences update_cmd = cmd.UpdateACLsCommand(self.driver._nb_ovn, [lswitch_name], iter(ports), acls_new_dict ) acl_dels, acl_adds =\ update_cmd._compute_acl_differences(iter(ports), acls_old_dict, acls_new_dict, acl_obj_dict) # Expected Difference (Sorted) acl_del_exp = {lswitch_name: ['row3', 'row6']} acl_adds_exp = {lswitch_name: [{'priority': 1002, 'direction': 'to-lport', 'match': 'ip6 && (ip.src == %s)' % (port2['fixed_ips'][1]['ip_address'])}, {'priority': 1002, 'direction': 'to-lport', 'match': 'ip6 && (ip.src == %s)' % (port1['fixed_ips'][1]['ip_address'])}]} self.assertEqual(acl_del_exp, acl_dels) self.assertEqual(acl_adds_exp, acl_adds) # make sure argument add_acl=False will take no affect in # need_compare=True scenario update_cmd_with_acl = cmd.UpdateACLsCommand(self.driver._nb_ovn, [lswitch_name], iter(ports), acls_new_dict_copy, need_compare=True, is_add_acl=False) new_acl_dels, new_acl_adds =\ update_cmd_with_acl._compute_acl_differences(iter(ports), acls_old_dict, acls_new_dict_copy, acl_obj_dict) self.assertEqual(acl_dels, new_acl_dels) self.assertEqual(acl_adds, new_acl_adds) def test__get_update_data_without_compare(self): lswitch_name = 'lswitch-1' port1 = {'id': 'port-id1', 'network_id': lswitch_name, 'fixed_ips': mock.Mock()} port2 = {'id': 'port-id2', 'network_id': lswitch_name, 'fixed_ips': mock.Mock()} ports = [port1, port2] aclport1_new = {'priority': 1002, 'direction': 'to-lport', 'match': 'outport == %s && ip4 && icmp4' % (port1['id']), 'external_ids': {}} aclport2_new = {'priority': 1002, 'direction': 'to-lport', 'match': 'outport == %s && ip4 && icmp4' % (port2['id']), 'external_ids': {}} acls_new_dict = {'%s' % (port1['id']): aclport1_new, '%s' % (port2['id']): aclport2_new} # test for creating new acls update_cmd_add_acl = cmd.UpdateACLsCommand(self.driver._nb_ovn, [lswitch_name], iter(ports), acls_new_dict, need_compare=False, is_add_acl=True) lswitch_dict, acl_del_dict, acl_add_dict = \ update_cmd_add_acl._get_update_data_without_compare() self.assertIn('neutron-lswitch-1', lswitch_dict) self.assertEqual({}, acl_del_dict) expected_acls = {'neutron-lswitch-1': [aclport1_new, aclport2_new]} self.assertEqual(expected_acls, acl_add_dict) # test for deleting existing acls acl1 = mock.Mock( match='outport == port-id1 && ip4 && icmp4', external_ids={}) acl2 = mock.Mock( match='outport == port-id2 && ip4 && icmp4', external_ids={}) acl3 = mock.Mock( match='outport == port-id1 && ip4 && (ip4.src == fake_ip)', external_ids={}) lswitch_obj = mock.Mock( name='neutron-lswitch-1', acls=[acl1, acl2, acl3]) with mock.patch('ovsdbapp.backend.ovs_idl.idlutils.row_by_value', return_value=lswitch_obj): update_cmd_del_acl = cmd.UpdateACLsCommand(self.driver._nb_ovn, [lswitch_name], iter(ports), acls_new_dict, need_compare=False, is_add_acl=False) lswitch_dict, acl_del_dict, acl_add_dict = \ update_cmd_del_acl._get_update_data_without_compare() self.assertIn('neutron-lswitch-1', lswitch_dict) expected_acls = {'neutron-lswitch-1': [acl1, acl2]} self.assertEqual(expected_acls, acl_del_dict) self.assertEqual({}, acl_add_dict) def test_acl_protocol_and_ports_for_tcp_udp_and_sctp_number(self): sg_rule = {'port_range_min': None, 'port_range_max': None} sg_rule['protocol'] = str(const.PROTO_NUM_TCP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && tcp', match) sg_rule['protocol'] = str(const.PROTO_NUM_UDP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && udp', match) sg_rule['protocol'] = str(const.PROTO_NUM_SCTP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && sctp', match) def test_acl_protocol_and_ports_for_tcp_udp_and_sctp_number_one(self): sg_rule = {'port_range_min': 22, 'port_range_max': 22} sg_rule['protocol'] = str(const.PROTO_NUM_TCP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && tcp && tcp.dst == 22', match) sg_rule['protocol'] = str(const.PROTO_NUM_UDP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && udp && udp.dst == 22', match) sg_rule['protocol'] = str(const.PROTO_NUM_SCTP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && sctp && sctp.dst == 22', match) def test_acl_protocol_and_ports_for_tcp_udp_and_sctp_number_range(self): sg_rule = {'port_range_min': 21, 'port_range_max': 23} sg_rule['protocol'] = str(const.PROTO_NUM_TCP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && tcp && tcp.dst >= 21 && tcp.dst <= 23', match) sg_rule['protocol'] = str(const.PROTO_NUM_UDP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && udp && udp.dst >= 21 && udp.dst <= 23', match) sg_rule['protocol'] = str(const.PROTO_NUM_SCTP) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && sctp && sctp.dst >= 21 && sctp.dst <= 23', match) def test_acl_protocol_and_ports_for_ipv6_icmp_protocol(self): sg_rule = {'port_range_min': None, 'port_range_max': None} icmp = 'icmp6' expected_match = ' && icmp6' sg_rule['protocol'] = const.PROTO_NAME_ICMP match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) sg_rule['protocol'] = str(const.PROTO_NUM_ICMP) match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) sg_rule['protocol'] = const.PROTO_NAME_IPV6_ICMP match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) sg_rule['protocol'] = const.PROTO_NAME_IPV6_ICMP_LEGACY match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) sg_rule['protocol'] = str(const.PROTO_NUM_IPV6_ICMP) match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) def test_acl_protocol_and_ports_for_icmp4_and_icmp6_port_range(self): match_list = [ (None, None, ' && icmp4'), (0, None, ' && icmp4 && icmp4.type == 0'), (0, 0, ' && icmp4 && icmp4.type == 0 && icmp4.code == 0'), (0, 5, ' && icmp4 && icmp4.type == 0 && icmp4.code == 5')] v6_match_list = [ (None, None, ' && icmp6'), (133, None, ' && icmp6 && icmp6.type == 133'), (1, 1, ' && icmp6 && icmp6.type == 1 && icmp6.code == 1'), (138, 1, ' && icmp6 && icmp6.type == 138 && icmp6.code == 1')] sg_rule = {'protocol': const.PROTO_NAME_ICMP} icmp = 'icmp4' for pmin, pmax, expected_match in match_list: sg_rule['port_range_min'] = pmin sg_rule['port_range_max'] = pmax match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) sg_rule = {'protocol': const.PROTO_NAME_IPV6_ICMP} icmp = 'icmp6' for pmin, pmax, expected_match in v6_match_list: sg_rule['port_range_min'] = pmin sg_rule['port_range_max'] = pmax match = ovn_acl.acl_protocol_and_ports(sg_rule, icmp) self.assertEqual(expected_match, match) def test_acl_protocol_and_ports_protocol_not_supported(self): sg_rule = {'port_range_min': None, 'port_range_max': None} sg_rule['protocol'] = '1234567' self.assertRaises(ovn_acl.ProtocolNotSupported, ovn_acl.acl_protocol_and_ports, sg_rule, None) def test_acl_protocol_and_ports_protocol_range(self): sg_rule = {'port_range_min': None, 'port_range_max': None} # For more common protocols such as TCP, UDP and ICMP, we # prefer to use the protocol name in the match string instead of # the protocol number (e.g: the word "tcp" instead of "ip.proto # == 6"). This improves the readability/debbugability when # troubleshooting the ACLs skip_protos = (const.PROTO_NUM_TCP, const.PROTO_NUM_UDP, const.PROTO_NUM_SCTP, const.PROTO_NUM_ICMP, const.PROTO_NUM_IPV6_ICMP) for proto in range(256): if proto in skip_protos: continue sg_rule['protocol'] = str(proto) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && ip.proto == %s' % proto, match) def test_acl_protocol_and_ports_name_to_number(self): sg_rule = {'port_range_min': None, 'port_range_max': None} sg_rule['protocol'] = str(const.PROTO_NAME_OSPF) match = ovn_acl.acl_protocol_and_ports(sg_rule, None) self.assertEqual(' && ip.proto == 89', match) def test_acl_direction(self): sg_rule = fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'direction': 'ingress' }).info() match = ovn_acl.acl_direction(sg_rule, self.fake_port) self.assertEqual('outport == "' + self.fake_port['id'] + '"', match) sg_rule['direction'] = 'egress' match = ovn_acl.acl_direction(sg_rule, self.fake_port) self.assertEqual('inport == "' + self.fake_port['id'] + '"', match) def test_acl_ethertype(self): sg_rule = fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'ethertype': 'IPv4' }).info() match, ip_version, icmp = ovn_acl.acl_ethertype(sg_rule) self.assertEqual(' && ip4', match) self.assertEqual('ip4', ip_version) self.assertEqual('icmp4', icmp) sg_rule['ethertype'] = 'IPv6' match, ip_version, icmp = ovn_acl.acl_ethertype(sg_rule) self.assertEqual(' && ip6', match) self.assertEqual('ip6', ip_version) self.assertEqual('icmp6', icmp) sg_rule['ethertype'] = 'IPv10' match, ip_version, icmp = ovn_acl.acl_ethertype(sg_rule) self.assertEqual('', match) self.assertIsNone(ip_version) self.assertIsNone(icmp) def test_acl_remote_ip_prefix(self): sg_rule = fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'direction': 'ingress', 'remote_ip_prefix': None }).info() ip_version = 'ip4' remote_ip_prefix = '10.10.0.0/24' match = ovn_acl.acl_remote_ip_prefix(sg_rule, ip_version) self.assertEqual('', match) sg_rule['remote_ip_prefix'] = remote_ip_prefix match = ovn_acl.acl_remote_ip_prefix(sg_rule, ip_version) expected_match = ' && %s.src == %s' % (ip_version, remote_ip_prefix) self.assertEqual(expected_match, match) sg_rule['direction'] = 'egress' match = ovn_acl.acl_remote_ip_prefix(sg_rule, ip_version) expected_match = ' && %s.dst == %s' % (ip_version, remote_ip_prefix) self.assertEqual(expected_match, match) def test_acl_remote_group_id(self): sg_rule = fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'direction': 'ingress', 'remote_group_id': None }).info() ip_version = 'ip4' sg_id = sg_rule['security_group_id'] addrset_name = ovn_utils.ovn_addrset_name(sg_id, ip_version) match = ovn_acl.acl_remote_group_id(sg_rule, ip_version) self.assertEqual('', match) sg_rule['remote_group_id'] = sg_id match = ovn_acl.acl_remote_group_id(sg_rule, ip_version) self.assertEqual(' && ip4.src == $' + addrset_name, match) sg_rule['direction'] = 'egress' match = ovn_acl.acl_remote_group_id(sg_rule, ip_version) self.assertEqual(' && ip4.dst == $' + addrset_name, match) def _test_update_acls_for_security_group(self, use_cache=True): sg = fakes.FakeSecurityGroup.create_one_security_group().info() remote_sg = fakes.FakeSecurityGroup.create_one_security_group().info() sg_rule = fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'security_group_id': sg['id'], 'remote_group_id': remote_sg['id'] }).info() port = fakes.FakePort.create_one_port({ 'security_groups': [sg['id']] }).info() self.plugin.get_ports.return_value = [port] if use_cache: sg_ports_cache = {sg['id']: [{'port_id': port['id']}], remote_sg['id']: []} else: sg_ports_cache = None self.plugin._get_port_security_group_bindings.return_value = \ [{'port_id': port['id']}] # Build ACL for validation. expected_acl = ovn_acl._add_sg_rule_acl_for_port(port, sg_rule) expected_acl.pop('lport') expected_acl.pop('lswitch') # Validate ACLs when port has security groups. ovn_acl.update_acls_for_security_group(self.plugin, self.admin_context, self.driver._nb_ovn, sg['id'], sg_rule, sg_ports_cache=sg_ports_cache) self.driver._nb_ovn.update_acls.assert_called_once_with( [port['network_id']], mock.ANY, {port['id']: expected_acl}, need_compare=False, is_add_acl=True ) def test_update_acls_for_security_group_cache(self): self._test_update_acls_for_security_group(use_cache=True) def test_update_acls_for_security_group_no_cache(self): self._test_update_acls_for_security_group(use_cache=False) def test_acl_port_ips(self): port4 = fakes.FakePort.create_one_port({ 'fixed_ips': [{'subnet_id': 'subnet-ipv4', 'ip_address': '10.0.0.1'}], }).info() port46 = fakes.FakePort.create_one_port({ 'fixed_ips': [{'subnet_id': 'subnet-ipv4', 'ip_address': '10.0.0.2'}, {'subnet_id': 'subnet-ipv6', 'ip_address': 'fde3:d45:df72::1'}], }).info() port6 = fakes.FakePort.create_one_port({ 'fixed_ips': [{'subnet_id': 'subnet-ipv6', 'ip_address': '2001:db8::8'}], }).info() addresses = ovn_acl.acl_port_ips(port4) self.assertEqual({'ip4': [port4['fixed_ips'][0]['ip_address']], 'ip6': []}, addresses) addresses = ovn_acl.acl_port_ips(port46) self.assertEqual({'ip4': [port46['fixed_ips'][0]['ip_address']], 'ip6': [port46['fixed_ips'][1]['ip_address']]}, addresses) addresses = ovn_acl.acl_port_ips(port6) self.assertEqual({'ip4': [], 'ip6': [port6['fixed_ips'][0]['ip_address']]}, addresses) def test_sg_disabled(self): sg = fakes.FakeSecurityGroup.create_one_security_group().info() port = fakes.FakePort.create_one_port({ 'security_groups': [sg['id']] }).info() with mock.patch('networking_ovn.common.acl.is_sg_enabled', return_value=False): acl_list = ovn_acl.add_acls(self.plugin, self.admin_context, port, {}, {}, self.driver._ovn) self.assertEqual([], acl_list) ovn_acl.update_acls_for_security_group(self.plugin, self.admin_context, self.driver._ovn, sg['id'], None) self.driver._ovn.update_acls.assert_not_called() addresses = ovn_acl.acl_port_ips(port) self.assertEqual({'ip4': [], 'ip6': []}, addresses) networking-ovn-4.0.0/networking_ovn/tests/unit/common/test_maintenance.py0000666000175100017510000001632013245511164027111 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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. import mock from neutron.tests.unit.plugins.ml2 import test_security_group as test_sg from neutron_lib.db import api as db_api from networking_ovn.common import constants from networking_ovn.common import maintenance from networking_ovn.common import utils from networking_ovn.db import maintenance as db_maint from networking_ovn.db import revision as db_rev from networking_ovn.tests.unit.db import base as db_base @mock.patch.object(maintenance.DBInconsistenciesPeriodics, 'has_lock', lambda _: True) class TestDBInconsistenciesPeriodics(db_base.DBTestCase, test_sg.Ml2SecurityGroupsTestCase): def setUp(self): super(TestDBInconsistenciesPeriodics, self).setUp() self.net = self._make_network( self.fmt, name='net1', admin_state_up=True)['network'] self.port = self._make_port( self.fmt, self.net['id'], name='port1')['port'] self.fake_ovn_client = mock.Mock() self.periodic = maintenance.DBInconsistenciesPeriodics( self.fake_ovn_client) self.session = db_api.get_writer_session() @mock.patch.object(maintenance.DBInconsistenciesPeriodics, '_fix_create_update') @mock.patch.object(db_maint, 'get_inconsistent_resources') def test_check_for_inconsistencies(self, mock_get_incon_res, mock_fix_net): fake_row = mock.Mock(resource_type=constants.TYPE_NETWORKS) mock_get_incon_res.return_value = [fake_row, ] self.periodic.check_for_inconsistencies() mock_fix_net.assert_called_once_with(fake_row) def _test_fix_create_update_network(self, ovn_rev, neutron_rev): self.net['revision_number'] = neutron_rev # Create an entry to the revision_numbers table and assert the # initial revision_number for our test object is the expected db_rev.create_initial_revision( self.net['id'], constants.TYPE_NETWORKS, self.session, revision_number=ovn_rev) row = self.get_revision_row(self.net['id']) self.assertEqual(ovn_rev, row.revision_number) if ovn_rev < 0: self.fake_ovn_client._nb_idl.get_lswitch.return_value = None else: fake_ls = mock.Mock(external_ids={ constants.OVN_REV_NUM_EXT_ID_KEY: ovn_rev}) self.fake_ovn_client._nb_idl.get_lswitch.return_value = fake_ls self.fake_ovn_client._plugin.get_network.return_value = self.net self.periodic._fix_create_update(row) # Since the revision number was < 0, make sure create_network() # is invoked with the latest version of the object in the neutron # database if ovn_rev < 0: self.fake_ovn_client.create_network.assert_called_once_with( self.net) # If the revision number is > 0 it means that the object already # exist and we just need to update to match the latest in the # neutron database so, update_network() should be called. else: self.fake_ovn_client.update_network.assert_called_once_with( self.net) def test_fix_network_create(self): self._test_fix_create_update_network(ovn_rev=-1, neutron_rev=2) def test_fix_network_update(self): self._test_fix_create_update_network(ovn_rev=5, neutron_rev=7) def _test_fix_create_update_port(self, ovn_rev, neutron_rev): self.port['revision_number'] = neutron_rev # Create an entry to the revision_numbers table and assert the # initial revision_number for our test object is the expected db_rev.create_initial_revision( self.port['id'], constants.TYPE_PORTS, self.session, revision_number=ovn_rev) row = self.get_revision_row(self.port['id']) self.assertEqual(ovn_rev, row.revision_number) if ovn_rev < 0: self.fake_ovn_client._nb_idl.get_lswitch_port.return_value = None else: fake_lsp = mock.Mock(external_ids={ constants.OVN_REV_NUM_EXT_ID_KEY: ovn_rev}) self.fake_ovn_client._nb_idl.get_lswitch_port.return_value = ( fake_lsp) self.fake_ovn_client._plugin.get_port.return_value = self.port self.periodic._fix_create_update(row) # Since the revision number was < 0, make sure create_port() # is invoked with the latest version of the object in the neutron # database if ovn_rev < 0: self.fake_ovn_client.create_port.assert_called_once_with( self.port) # If the revision number is > 0 it means that the object already # exist and we just need to update to match the latest in the # neutron database so, update_port() should be called. else: self.fake_ovn_client.update_port.assert_called_once_with( self.port) def test_fix_port_create(self): self._test_fix_create_update_port(ovn_rev=-1, neutron_rev=2) def test_fix_port_update(self): self._test_fix_create_update_port(ovn_rev=5, neutron_rev=7) @mock.patch.object(db_rev, 'bump_revision') def _test_fix_security_group_create(self, mock_bump, revision_number): sg_name = utils.ovn_addrset_name('fake_id', 'ip4') sg = self._make_security_group(self.fmt, sg_name, '')['security_group'] db_rev.create_initial_revision( sg['id'], constants.TYPE_SECURITY_GROUPS, self.session, revision_number=revision_number) row = self.get_revision_row(sg['id']) self.assertEqual(revision_number, row.revision_number) if revision_number < 0: self.fake_ovn_client._nb_idl.get_address_set.return_value = None else: self.fake_ovn_client._nb_idl.get_address_set.return_value = ( mock.sentinel.AddressSet) self.fake_ovn_client._plugin.get_security_group.return_value = sg self.periodic._fix_create_update(row) if revision_number < 0: self.fake_ovn_client.create_security_group.assert_called_once_with( sg) else: # If the object already exist let's make sure we just bump # the revision number in the ovn_revision_numbers table self.assertFalse(self.fake_ovn_client.create_security_group.called) mock_bump.assert_called_once_with( sg, constants.TYPE_SECURITY_GROUPS) def test_fix_security_group_create_doesnt_exist(self): self._test_fix_security_group_create(revision_number=-1) def test_fix_security_group_create_version_mismatch(self): self._test_fix_security_group_create(revision_number=2) networking-ovn-4.0.0/networking_ovn/tests/unit/common/__init__.py0000666000175100017510000000000013245511145025312 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/agent/0000775000175100017510000000000013245511554023023 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/agent/metadata/0000775000175100017510000000000013245511554024603 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/agent/metadata/test_driver.py0000666000175100017510000001225113245511145027506 0ustar zuulzuul00000000000000# Copyright 2017 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. import os import mock from oslo_config import cfg from oslo_utils import uuidutils from neutron.tests import base from neutron.tests import tools from neutron.tests.unit.agent.linux import test_utils from networking_ovn.agent.metadata import agent as metadata_agent from networking_ovn.agent.metadata import driver as metadata_driver from networking_ovn.conf.agent.metadata import config as meta_conf _uuid = uuidutils.generate_uuid class TestMetadataDriverProcess(base.BaseTestCase): EUNAME = 'neutron' EGNAME = 'neutron' METADATA_PORT = 8080 METADATA_SOCKET = '/socket/path' PIDFILE = 'pidfile' def setUp(self): super(TestMetadataDriverProcess, self).setUp() mock.patch('eventlet.spawn').start() meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, cfg.CONF) def test_spawn_metadata_proxy(self): datapath_id = _uuid() metadata_ns = metadata_agent.NS_PREFIX + datapath_id ip_class_path = 'neutron.agent.linux.ip_lib.IPWrapper' cfg.CONF.set_override('metadata_proxy_user', self.EUNAME) cfg.CONF.set_override('metadata_proxy_group', self.EGNAME) cfg.CONF.set_override('metadata_proxy_socket', self.METADATA_SOCKET) cfg.CONF.set_override('debug', True) agent = metadata_agent.MetadataAgent(cfg.CONF) with mock.patch(ip_class_path) as ip_mock,\ mock.patch( 'neutron.agent.linux.external_process.' 'ProcessManager.get_pid_file_name', return_value=self.PIDFILE),\ mock.patch('pwd.getpwnam', return_value=test_utils.FakeUser(self.EUNAME)),\ mock.patch('grp.getgrnam', return_value=test_utils.FakeGroup(self.EGNAME)),\ mock.patch('os.makedirs'): cfg_file = os.path.join( metadata_driver.HaproxyConfigurator.get_config_path( cfg.CONF.state_path), "%s.conf" % datapath_id) mock_open = self.useFixture( tools.OpenFixture(cfg_file)).mock_open metadata_driver.MetadataDriver.spawn_monitored_metadata_proxy( agent._process_monitor, metadata_ns, self.METADATA_PORT, cfg.CONF, network_id=datapath_id) netns_execute_args = [ 'haproxy', '-f', cfg_file] cfg_contents = metadata_driver._HAPROXY_CONFIG_TEMPLATE % { 'user': self.EUNAME, 'group': self.EGNAME, 'port': self.METADATA_PORT, 'unix_socket_path': self.METADATA_SOCKET, 'res_type': 'Network', 'res_id': datapath_id, 'pidfile': self.PIDFILE, 'log_level': 'debug'} mock_open.assert_has_calls([ mock.call(cfg_file, 'w'), mock.call().write(cfg_contents)], any_order=True) ip_mock.assert_has_calls([ mock.call(namespace=metadata_ns), mock.call().netns.execute(netns_execute_args, addl_env=None, run_as_root=True) ]) def test_create_config_file_wrong_user(self): with mock.patch('pwd.getpwnam', side_effect=KeyError): config = metadata_driver.HaproxyConfigurator(mock.ANY, mock.ANY, mock.ANY, mock.ANY, self.EUNAME, self.EGNAME, mock.ANY, mock.ANY) self.assertRaises(metadata_driver.InvalidUserOrGroupException, config.create_config_file) def test_create_config_file_wrong_group(self): with mock.patch('grp.getgrnam', side_effect=KeyError),\ mock.patch('pwd.getpwnam', return_value=test_utils.FakeUser(self.EUNAME)): config = metadata_driver.HaproxyConfigurator(mock.ANY, mock.ANY, mock.ANY, mock.ANY, self.EUNAME, self.EGNAME, mock.ANY, mock.ANY) self.assertRaises(metadata_driver.InvalidUserOrGroupException, config.create_config_file) networking-ovn-4.0.0/networking_ovn/tests/unit/agent/metadata/__init__.py0000666000175100017510000000000013245511145026700 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/agent/metadata/test_agent.py0000666000175100017510000002522513245511145027316 0ustar zuulzuul00000000000000# Copyright 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 mock from neutron.agent.linux import ip_lib from neutron.agent.linux.ip_lib import IpAddrCommand as ip_addr from neutron.agent.linux.ip_lib import IpLinkCommand as ip_link from neutron.agent.linux.ip_lib import IpNetnsCommand as ip_netns from neutron.agent.linux.ip_lib import IPWrapper as ip_wrap from neutron.tests import base from oslo_config import cfg from oslo_config import fixture as config_fixture from networking_ovn.agent.metadata import agent from networking_ovn.agent.metadata import driver from networking_ovn.conf.agent.metadata import config as meta_conf OvnPortInfo = collections.namedtuple( 'OvnPortInfo', ['datapath', 'type', 'mac', 'external_ids', 'logical_port']) DatapathInfo = collections.namedtuple('DatapathInfo', 'uuid') def makePort(datapath=None, type='', mac=None, external_ids=None, logical_port=None): return OvnPortInfo(datapath, type, mac, external_ids, logical_port) class ConfFixture(config_fixture.Config): def setUp(self): super(ConfFixture, self).setUp() meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, self.conf) meta_conf.register_meta_conf_opts( meta_conf.UNIX_DOMAIN_METADATA_PROXY_OPTS, self.conf) meta_conf.register_meta_conf_opts( meta_conf.METADATA_PROXY_HANDLER_OPTS, self.conf) meta_conf.register_meta_conf_opts(meta_conf.OVS_OPTS, self.conf, group='ovs') class TestMetadataAgent(base.BaseTestCase): fake_conf = cfg.CONF fake_conf_fixture = ConfFixture(fake_conf) def setUp(self): super(TestMetadataAgent, self).setUp() self.useFixture(self.fake_conf_fixture) self.log_p = mock.patch.object(agent, 'LOG') self.log = self.log_p.start() self.agent = agent.MetadataAgent(self.fake_conf) self.agent.sb_idl = mock.Mock() self.agent.ovs_idl = mock.Mock() self.agent.chassis = 'chassis' def test_sync(self): with mock.patch.object( self.agent, 'ensure_all_networks_provisioned') as enp,\ mock.patch.object( ip_wrap, 'get_namespaces') as gns,\ mock.patch.object( self.agent, 'teardown_datapath') as tdp: enp.return_value = ['ovnmeta-1', 'ovnmeta-2'] gns.return_value = ['ovnmeta-1', 'ovnmeta-2'] self.agent.sync() enp.assert_called_once() gns.assert_called_once() tdp.assert_not_called() def test_sync_teardown_namespace(self): """Test that sync tears down unneeded metadata namespaces.""" with mock.patch.object( self.agent, 'ensure_all_networks_provisioned') as enp,\ mock.patch.object( ip_wrap, 'get_namespaces') as gns,\ mock.patch.object( self.agent, 'teardown_datapath') as tdp: enp.return_value = ['ovnmeta-1', 'ovnmeta-2'] gns.return_value = ['ovnmeta-1', 'ovnmeta-2', 'ovnmeta-3', 'ns1', 'ns2'] self.agent.sync() enp.assert_called_once() gns.assert_called_once() tdp.assert_called_once_with('3') def test_ensure_all_networks_provisioned(self): """Test networks are provisioned. This test simulates that this chassis has the following ports: * datapath '0': 1 port * datapath '1': 2 ports * datapath '2': 1 port * datapath '5': 1 port with type 'unk' It is expected that only datapaths '0', '1' and '2' are provisioned once. """ ports = [] for i in range(0, 3): ports.append(makePort(datapath=DatapathInfo(uuid=str(i)))) ports.append(makePort(datapath=DatapathInfo(uuid='1'))) ports.append(makePort(datapath=DatapathInfo(uuid='5'), type='unknown')) with mock.patch.object(self.agent, 'provision_datapath', return_value=None) as pdp,\ mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', return_value=ports): self.agent.ensure_all_networks_provisioned() expected_calls = [mock.call(str(i)) for i in range(0, 3)] self.assertEqual(sorted(expected_calls), sorted(pdp.call_args_list)) def test_update_datapath_provision(self): ports = [] for i in range(0, 3): ports.append(makePort(datapath=DatapathInfo(uuid=str(i)))) with mock.patch.object(self.agent, 'provision_datapath', return_value=None) as pdp,\ mock.patch.object(self.agent, 'teardown_datapath') as tdp,\ mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', return_value=ports): self.agent.update_datapath('1') pdp.assert_called_once_with('1') tdp.assert_not_called() def test_update_datapath_teardown(self): ports = [] for i in range(0, 3): ports.append(makePort(datapath=DatapathInfo(uuid=str(i)))) with mock.patch.object(self.agent, 'provision_datapath', return_value=None) as pdp,\ mock.patch.object(self.agent, 'teardown_datapath') as tdp,\ mock.patch.object(self.agent.sb_idl, 'get_ports_on_chassis', return_value=ports): self.agent.update_datapath('5') tdp.assert_called_once_with('5') pdp.assert_not_called() def test_teardown_datapath(self): """Test teardown datapath. Check that the VETH pair, OVS port and namespace associated to this namespace are deleted and the metadata proxy is destroyed. """ with mock.patch.object(self.agent, 'update_chassis_metadata_networks'),\ mock.patch.object( ip_netns, 'exists', return_value=True),\ mock.patch.object( ip_lib, 'device_exists', return_value=True),\ mock.patch.object( ip_wrap, 'garbage_collect_namespace') as garbage_collect,\ mock.patch.object( ip_wrap, 'del_veth') as del_veth,\ mock.patch.object(agent.MetadataAgent, '_get_veth_name', return_value=['veth_0', 'veth_1']),\ mock.patch.object( driver.MetadataDriver, 'destroy_monitored_metadata_proxy') as destroy_mdp: self.agent.teardown_datapath('1') destroy_mdp.assert_called_once() self.agent.ovs_idl.del_port.assert_called_once_with( 'veth_0', bridge='br-int') del_veth.assert_called_once_with('veth_0') garbage_collect.assert_called_once() def test_provision_datapath(self): """Test datapath provisioning. Check that the VETH pair, OVS port and namespace associated to this namespace are created, that the interface is properly configured with the right IP addresses and that the metadata proxy is spawned. """ metadata_port = makePort(mac=['aa:bb:cc:dd:ee:ff'], external_ids={ 'neutron:cidrs': '10.0.0.1/23 ' '2001:470:9:1224:5595:dd51:6ba2:e788/64'}, logical_port='port') with mock.patch.object(self.agent.sb_idl, 'get_metadata_port_network', return_value=metadata_port),\ mock.patch.object( ip_lib, 'device_exists', return_value=False),\ mock.patch.object(agent.MetadataAgent, '_get_veth_name', return_value=['veth_0', 'veth_1']),\ mock.patch.object(agent.MetadataAgent, '_get_namespace_name', return_value='namespace'),\ mock.patch.object(ip_link, 'set_up') as link_set_up,\ mock.patch.object(ip_link, 'set_address') as link_set_addr,\ mock.patch.object(ip_addr, 'list', return_value=[]),\ mock.patch.object(ip_addr, 'add') as ip_addr_add,\ mock.patch.object( ip_wrap, 'add_veth', return_value=[ip_lib.IPDevice('ip1'), ip_lib.IPDevice('ip2')]) as add_veth,\ mock.patch.object( self.agent, 'update_chassis_metadata_networks') as update_chassis,\ mock.patch.object( driver.MetadataDriver, 'spawn_monitored_metadata_proxy') as spawn_mdp: self.agent.provision_datapath('1') # Check that the VETH pair is created add_veth.assert_called_once_with('veth_0', 'veth_1', 'namespace') # Make sure that the two ends of the VETH pair have been set as up. self.assertEqual(2, link_set_up.call_count) link_set_addr.assert_called_once_with('aa:bb:cc:dd:ee:ff') # Make sure that the port has been added to OVS. self.agent.ovs_idl.add_port.assert_called_once_with( 'br-int', 'veth_0') self.agent.ovs_idl.db_set.assert_called_once_with( 'Interface', 'veth_0', ('external_ids', {'iface-id': 'port'})) # Check that the metadata port has the IP addresses properly # configured and that IPv6 address has been skipped. expected_calls = [mock.call('10.0.0.1/23'), mock.call('169.254.169.254/16')] self.assertEqual(sorted(expected_calls), sorted(ip_addr_add.call_args_list)) # Check that metadata proxy has been spawned spawn_mdp.assert_called_once() # Check that the chassis has been updated with the datapath. update_chassis.assert_called_once_with('1') networking-ovn-4.0.0/networking_ovn/tests/unit/agent/metadata/test_server.py0000666000175100017510000002556413245511145027534 0ustar zuulzuul00000000000000# Copyright 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 mock from neutron.agent.linux import utils as agent_utils from neutron.tests import base from oslo_config import cfg from oslo_config import fixture as config_fixture from oslo_utils import fileutils import testtools import webob from networking_ovn.agent.metadata import server as agent from networking_ovn.conf.agent.metadata import config as meta_conf OvnPortInfo = collections.namedtuple('OvnPortInfo', 'external_ids') class ConfFixture(config_fixture.Config): def setUp(self): super(ConfFixture, self).setUp() meta_conf.register_meta_conf_opts( meta_conf.METADATA_PROXY_HANDLER_OPTS, self.conf) self.config(auth_ca_cert=None, nova_metadata_host='9.9.9.9', nova_metadata_port=8775, metadata_proxy_shared_secret='secret', nova_metadata_protocol='http', nova_metadata_insecure=True, nova_client_cert='nova_cert', nova_client_priv_key='nova_priv_key') class TestMetadataProxyHandler(base.BaseTestCase): fake_conf = cfg.CONF fake_conf_fixture = ConfFixture(fake_conf) def setUp(self): super(TestMetadataProxyHandler, self).setUp() self.useFixture(self.fake_conf_fixture) self.log_p = mock.patch.object(agent, 'LOG') self.log = self.log_p.start() self.handler = agent.MetadataProxyHandler(self.fake_conf) self.handler.sb_idl = mock.Mock() def test_call(self): req = mock.Mock() with mock.patch.object(self.handler, '_get_instance_and_project_id') as get_ids: get_ids.return_value = ('instance_id', 'project_id') with mock.patch.object(self.handler, '_proxy_request') as proxy: proxy.return_value = 'value' retval = self.handler(req) self.assertEqual(retval, 'value') def test_call_no_instance_match(self): req = mock.Mock() with mock.patch.object(self.handler, '_get_instance_and_project_id') as get_ids: get_ids.return_value = None, None retval = self.handler(req) self.assertIsInstance(retval, webob.exc.HTTPNotFound) def test_call_internal_server_error(self): req = mock.Mock() with mock.patch.object(self.handler, '_get_instance_and_project_id') as get_ids: get_ids.side_effect = Exception retval = self.handler(req) self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) self.assertEqual(len(self.log.mock_calls), 2) def _get_instance_and_project_id_helper(self, headers, list_ports_retval, network=None): remote_address = '192.168.1.1' headers['X-Forwarded-For'] = remote_address req = mock.Mock(headers=headers) def mock_get_network_port_bindings_by_ip(*args, **kwargs): return list_ports_retval.pop(0) self.handler.sb_idl.get_network_port_bindings_by_ip.side_effect = ( mock_get_network_port_bindings_by_ip) instance_id, project_id = ( self.handler._get_instance_and_project_id(req)) expected = [mock.call(network, '192.168.1.1')] self.handler.sb_idl.get_network_port_bindings_by_ip.assert_has_calls( expected) return (instance_id, project_id) def test_get_instance_id_network_id(self): network_id = 'the_id' headers = { 'X-OVN-Network-ID': network_id } ovn_port = OvnPortInfo( external_ids={'neutron:device_id': 'device_id', 'neutron:project_id': 'project_id'}) ports = [[ovn_port]] self.assertEqual( self._get_instance_and_project_id_helper(headers, ports, network='the_id'), ('device_id', 'project_id') ) def test_get_instance_id_network_id_no_match(self): network_id = 'the_id' headers = { 'X-OVN-Network-ID': network_id } ports = [[]] expected = (None, None) observed = self._get_instance_and_project_id_helper(headers, ports, network='the_id') self.assertEqual(expected, observed) def _proxy_request_test_helper(self, response_code=200, method='GET'): hdrs = {'X-Forwarded-For': '8.8.8.8'} body = 'body' req = mock.Mock(path_info='/the_path', query_string='', headers=hdrs, method=method, body=body) resp = mock.MagicMock(status=response_code) req.response = resp with mock.patch.object(self.handler, '_sign_instance_id') as sign: sign.return_value = 'signed' with mock.patch('httplib2.Http') as mock_http: resp.__getitem__.return_value = "text/plain" mock_http.return_value.request.return_value = (resp, 'content') retval = self.handler._proxy_request('the_id', 'tenant_id', req) mock_http.assert_called_once_with( ca_certs=None, disable_ssl_certificate_validation=True) mock_http.assert_has_calls([ mock.call().add_certificate( self.fake_conf.nova_client_priv_key, self.fake_conf.nova_client_cert, "%s:%s" % (self.fake_conf.nova_metadata_host, self.fake_conf.nova_metadata_port) ), mock.call().request( 'http://9.9.9.9:8775/the_path', method=method, headers={ 'X-Forwarded-For': '8.8.8.8', 'X-Instance-ID-Signature': 'signed', 'X-Instance-ID': 'the_id', 'X-Tenant-ID': 'tenant_id' }, body=body )] ) return retval def test_proxy_request_post(self): response = self._proxy_request_test_helper(method='POST') self.assertEqual(response.content_type, "text/plain") self.assertEqual(response.body, 'content') def test_proxy_request_200(self): response = self._proxy_request_test_helper(200) self.assertEqual(response.content_type, "text/plain") self.assertEqual(response.body, 'content') def test_proxy_request_400(self): self.assertIsInstance(self._proxy_request_test_helper(400), webob.exc.HTTPBadRequest) def test_proxy_request_403(self): self.assertIsInstance(self._proxy_request_test_helper(403), webob.exc.HTTPForbidden) def test_proxy_request_404(self): self.assertIsInstance(self._proxy_request_test_helper(404), webob.exc.HTTPNotFound) def test_proxy_request_409(self): self.assertIsInstance(self._proxy_request_test_helper(409), webob.exc.HTTPConflict) def test_proxy_request_500(self): self.assertIsInstance(self._proxy_request_test_helper(500), webob.exc.HTTPInternalServerError) def test_proxy_request_other_code(self): with testtools.ExpectedException(Exception): self._proxy_request_test_helper(302) def test_sign_instance_id(self): self.assertEqual( self.handler._sign_instance_id('foo'), '773ba44693c7553d6ee20f61ea5d2757a9a4f4a44d2841ae4e95b52e4cd62db4' ) class TestUnixDomainMetadataProxy(base.BaseTestCase): def setUp(self): super(TestUnixDomainMetadataProxy, self).setUp() self.cfg_p = mock.patch.object(agent, 'cfg') self.cfg = self.cfg_p.start() self.cfg.CONF.metadata_proxy_socket = '/the/path' self.cfg.CONF.metadata_workers = 0 self.cfg.CONF.metadata_backlog = 128 self.cfg.CONF.metadata_proxy_socket_mode = meta_conf.USER_MODE @mock.patch.object(fileutils, 'ensure_tree') def test_init_doesnot_exists(self, ensure_dir): agent.UnixDomainMetadataProxy(mock.Mock()) ensure_dir.assert_called_once_with('/the', mode=0o755) def test_init_exists(self): with mock.patch('os.path.isdir') as isdir: with mock.patch('os.unlink') as unlink: isdir.return_value = True agent.UnixDomainMetadataProxy(mock.Mock()) unlink.assert_called_once_with('/the/path') def test_init_exists_unlink_no_file(self): with mock.patch('os.path.isdir') as isdir: with mock.patch('os.unlink') as unlink: with mock.patch('os.path.exists') as exists: isdir.return_value = True exists.return_value = False unlink.side_effect = OSError agent.UnixDomainMetadataProxy(mock.Mock()) unlink.assert_called_once_with('/the/path') def test_init_exists_unlink_fails_file_still_exists(self): with mock.patch('os.path.isdir') as isdir: with mock.patch('os.unlink') as unlink: with mock.patch('os.path.exists') as exists: isdir.return_value = True exists.return_value = True unlink.side_effect = OSError with testtools.ExpectedException(OSError): agent.UnixDomainMetadataProxy(mock.Mock()) unlink.assert_called_once_with('/the/path') @mock.patch.object(agent, 'MetadataProxyHandler') @mock.patch.object(agent_utils, 'UnixDomainWSGIServer') @mock.patch.object(fileutils, 'ensure_tree') def test_run(self, ensure_dir, server, handler): p = agent.UnixDomainMetadataProxy(self.cfg.CONF) p.run() ensure_dir.assert_called_once_with('/the', mode=0o755) server.assert_has_calls([ mock.call('networking-ovn-metadata-agent'), mock.call().start(handler.return_value, '/the/path', workers=0, backlog=128, mode=0o644)] ) networking-ovn-4.0.0/networking_ovn/tests/unit/agent/__init__.py0000666000175100017510000000000013245511145025120 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/l3/0000775000175100017510000000000013245511554022243 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/l3/test_l3_ovn_scheduler.py0000666000175100017510000001612113245511145027111 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 random import mock from neutron.tests import base from networking_ovn.common import constants as ovn_const from networking_ovn.l3 import l3_ovn_scheduler class FakeOVNGatewaySchedulerNbOvnIdl(object): def __init__(self, chassis_gateway_mapping, gateway): self.get_all_chassis_gateway_bindings = mock.Mock( return_value=chassis_gateway_mapping['Chassis_Bindings']) self.get_gateway_chassis_binding = mock.Mock( return_value=chassis_gateway_mapping['Gateways'].get(gateway, None)) class FakeOVNGatewaySchedulerSbOvnIdl(object): def __init__(self, chassis_gateway_mapping): self.get_all_chassis = mock.Mock( return_value=chassis_gateway_mapping['Chassis']) class TestOVNGatewayScheduler(base.BaseTestCase): def setUp(self): super(TestOVNGatewayScheduler, self).setUp() # Overwritten by derived classes self.l3_scheduler = None # Used for unit tests self.new_gateway_name = 'lrp_new' self.fake_chassis_gateway_mappings = { 'None': {'Chassis': [], 'Gateways': { 'g1': [ovn_const.OVN_GATEWAY_INVALID_CHASSIS]}}, 'Multiple1': {'Chassis': ['hv1', 'hv2'], 'Gateways': {'g1': ['hv1'], 'g2': ['hv2'], 'g3': ['hv1']}}, 'Multiple2': {'Chassis': ['hv1', 'hv2', 'hv3'], 'Gateways': {'g1': ['hv1'], 'g2': ['hv1'], 'g3': ['hv1']}}, 'Multiple3': {'Chassis': ['hv1', 'hv2', 'hv3'], 'Gateways': {'g1': ['hv3'], 'g2': ['hv2'], 'g3': ['hv2']}} } # Determine the chassis to gateway list bindings for details in self.fake_chassis_gateway_mappings.values(): self.assertNotIn(self.new_gateway_name, details['Gateways']) details.setdefault('Chassis_Bindings', {}) for chassis in details['Chassis']: details['Chassis_Bindings'].setdefault(chassis, []) for gateway, chassis_list in details['Gateways'].items(): for chassis in chassis_list: if chassis in details['Chassis_Bindings']: details['Chassis_Bindings'][chassis].append(gateway) def select(self, chassis_gateway_mapping, gateway_name): nb_idl = FakeOVNGatewaySchedulerNbOvnIdl(chassis_gateway_mapping, gateway_name) sb_idl = FakeOVNGatewaySchedulerSbOvnIdl(chassis_gateway_mapping) return self.l3_scheduler.select(nb_idl, sb_idl, gateway_name) class OVNGatewayChanceScheduler(TestOVNGatewayScheduler): def setUp(self): super(OVNGatewayChanceScheduler, self).setUp() self.l3_scheduler = l3_ovn_scheduler.OVNGatewayChanceScheduler() def test_no_chassis_available_for_existing_gateway(self): mapping = self.fake_chassis_gateway_mappings['None'] gateway_name = random.choice(list(mapping['Gateways'].keys())) chassis = self.select(mapping, gateway_name) self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) def test_no_chassis_available_for_new_gateway(self): mapping = self.fake_chassis_gateway_mappings['None'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) def test_random_chassis_available_for_new_gateway(self): mapping = self.fake_chassis_gateway_mappings['Multiple1'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) self.assertItemsEqual(chassis, mapping.get('Chassis')) def test_existing_chassis_available_for_existing_gateway(self): mapping = self.fake_chassis_gateway_mappings['Multiple1'] gateway_name = random.choice(list(mapping['Gateways'].keys())) chassis = self.select(mapping, gateway_name) self.assertEqual(mapping['Gateways'][gateway_name], chassis) class OVNGatewayLeastLoadedScheduler(TestOVNGatewayScheduler): def setUp(self): super(OVNGatewayLeastLoadedScheduler, self).setUp() self.l3_scheduler = l3_ovn_scheduler.OVNGatewayLeastLoadedScheduler() def test_no_chassis_available_for_existing_gateway(self): mapping = self.fake_chassis_gateway_mappings['None'] gateway_name = random.choice(list(mapping['Gateways'].keys())) chassis = self.select(mapping, gateway_name) self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) def test_no_chassis_available_for_new_gateway(self): mapping = self.fake_chassis_gateway_mappings['None'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) self.assertEqual([ovn_const.OVN_GATEWAY_INVALID_CHASSIS], chassis) def test_least_loaded_chassis_available_for_new_gateway1(self): mapping = self.fake_chassis_gateway_mappings['Multiple1'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) self.assertItemsEqual(chassis, mapping.get('Chassis')) # least loaded will be the first one in the list, # networking-ovn will assign highest priority to this first element self.assertEqual(['hv2', 'hv1'], chassis) def test_least_loaded_chassis_available_for_new_gateway2(self): mapping = self.fake_chassis_gateway_mappings['Multiple2'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) # hv1 will have least priority self.assertEqual(chassis[2], 'hv1') def test_least_loaded_chassis_available_for_new_gateway3(self): mapping = self.fake_chassis_gateway_mappings['Multiple3'] gateway_name = self.new_gateway_name chassis = self.select(mapping, gateway_name) # least loaded chassis will be in the front of the list self.assertEqual(['hv1', 'hv3', 'hv2'], chassis) def test_existing_chassis_available_for_existing_gateway(self): mapping = self.fake_chassis_gateway_mappings['Multiple1'] gateway_name = random.choice(list(mapping['Gateways'].keys())) chassis = self.select(mapping, gateway_name) self.assertEqual(mapping['Gateways'][gateway_name], chassis) networking-ovn-4.0.0/networking_ovn/tests/unit/l3/test_l3_ovn.py0000666000175100017510000015742513245511145025070 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import mock from neutron.services.revisions import revision_plugin from neutron.tests.unit.api import test_extensions from neutron.tests.unit.extensions import test_extraroute from neutron.tests.unit.extensions import test_l3 from neutron.tests.unit.extensions import test_l3_ext_gw_mode as test_l3_gw from neutron_lib.callbacks import events from neutron_lib.callbacks import resources from neutron_lib import constants from neutron_lib import exceptions as n_exc from neutron_lib.plugins import constants as plugin_constants from neutron_lib.plugins import directory from oslo_config import cfg from networking_ovn.common import config from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils from networking_ovn.tests.unit import fakes from networking_ovn.tests.unit.ml2 import test_mech_driver class OVNL3RouterPlugin(test_mech_driver.OVNMechanismDriverTestCase): l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin' def _start_mock(self, path, return_value, new_callable=None): patcher = mock.patch(path, return_value=return_value, new_callable=new_callable) patch = patcher.start() self.addCleanup(patcher.stop) return patch def setUp(self): super(OVNL3RouterPlugin, self).setUp() revision_plugin.RevisionPlugin() network_attrs = {'router:external': True} self.fake_network = \ fakes.FakeNetwork.create_one_network(attrs=network_attrs).info() self.fake_router_port = {'device_id': '', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', 'fixed_ips': [{'ip_address': '10.0.0.100', 'subnet_id': 'subnet-id'}], 'id': 'router-port-id'} self.fake_router_port_assert = { 'lrouter': 'neutron-router-id', 'mac': 'aa:aa:aa:aa:aa:aa', 'name': 'lrp-router-port-id', 'may_exist': True, 'networks': ['10.0.0.100/24'], 'external_ids': {ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}} self.fake_router_ports = [self.fake_router_port] self.fake_subnet = {'id': 'subnet-id', 'ip_version': 4, 'cidr': '10.0.0.0/24'} self.fake_router = {'id': 'router-id', 'name': 'router', 'admin_state_up': False, 'routes': [{'destination': '1.1.1.0/24', 'nexthop': '10.0.0.2'}]} self.fake_router_interface_info = { 'port_id': 'router-port-id', 'device_id': '', 'mac_address': 'aa:aa:aa:aa:aa:aa', 'subnet_id': 'subnet-id', 'subnet_ids': ['subnet-id'], 'fixed_ips': [{'ip_address': '10.0.0.100', 'subnet_id': 'subnet-id'}], 'id': 'router-port-id'} self.fake_external_fixed_ips = { 'network_id': 'ext-network-id', 'external_fixed_ips': [{'ip_address': '192.168.1.1', 'subnet_id': 'ext-subnet-id'}]} self.fake_router_with_ext_gw = { 'id': 'router-id', 'name': 'router', 'admin_state_up': True, 'external_gateway_info': self.fake_external_fixed_ips, 'gw_port_id': 'gw-port-id' } self.fake_router_without_ext_gw = { 'id': 'router-id', 'name': 'router', 'admin_state_up': True, } self.fake_ext_subnet = {'id': 'ext-subnet-id', 'ip_version': 4, 'cidr': '192.168.1.0/24', 'gateway_ip': '192.168.1.254'} self.fake_ext_gw_port = {'device_id': '', 'device_owner': 'network:router_gateway', 'fixed_ips': [{'ip_address': '192.168.1.1', 'subnet_id': 'ext-subnet-id'}], 'mac_address': '00:00:00:02:04:06', 'network_id': self.fake_network['id'], 'id': 'gw-port-id'} self.fake_ext_gw_port_assert = { 'lrouter': 'neutron-router-id', 'mac': '00:00:00:02:04:06', 'name': 'lrp-gw-port-id', 'networks': ['192.168.1.1/24'], 'may_exist': True, 'external_ids': {ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'gateway_chassis': ['hv1']} self.fake_floating_ip_attrs = {'floating_ip_address': '192.168.0.10', 'fixed_ip_address': '10.0.0.10'} self.fake_floating_ip = fakes.FakeFloatingIp.create_one_fip( attrs=self.fake_floating_ip_attrs) self.fake_floating_ip_new_attrs = { 'router_id': 'new-router-id', 'floating_ip_address': '192.168.0.10', 'fixed_ip_address': '10.10.10.10', 'port_id': 'new-port_id'} self.fake_floating_ip_new = fakes.FakeFloatingIp.create_one_fip( attrs=self.fake_floating_ip_new_attrs) self.fake_ovn_nat_rule = { 'logical_ip': self.fake_floating_ip['fixed_ip_address'], 'external_ip': self.fake_floating_ip['floating_ip_address'], 'type': 'dnat_and_snat', 'external_ids': { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip['router_id'])}} self.l3_inst = directory.get_plugin(plugin_constants.L3) self._start_mock( 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._ovn', new_callable=mock.PropertyMock, return_value=fakes.FakeOvsdbNbOvnIdl()) self._start_mock( 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._sb_ovn', new_callable=mock.PropertyMock, return_value=fakes.FakeOvsdbSbOvnIdl()) self._start_mock( 'neutron.plugins.ml2.plugin.Ml2Plugin.get_network', return_value=self.fake_network) self._start_mock( 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port', return_value=self.fake_router_port) self._start_mock( 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet', return_value=self.fake_subnet) self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router', return_value=self.fake_router) self._start_mock( 'neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.update_router', return_value=self.fake_router) self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin.remove_router_interface', return_value=self.fake_router_interface_info) self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin.create_router', return_value=self.fake_router_with_ext_gw) self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_router', return_value={}) self._start_mock( 'networking_ovn.common.ovn_client.' 'OVNClient.get_candidates_for_scheduling', return_value=[]) self._start_mock( 'networking_ovn.l3.l3_ovn_scheduler.' 'OVNGatewayLeastLoadedScheduler._schedule_gateway', return_value=['hv1']) # FIXME(lucasagomes): We shouldn't be mocking the creation of # floating IPs here, that makes the FIP to not be registered in # the standardattributes table and therefore we also need to mock # bump_revision. self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin.create_floatingip', return_value=self.fake_floating_ip) self._start_mock( 'networking_ovn.db.revision.bump_revision', return_value=None) self._start_mock( 'neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip', return_value=self.fake_floating_ip) self._start_mock( 'networking_ovn.common.ovn_client.' 'OVNClient.update_floatingip_status', return_value=None) self.bump_rev_p = self._start_mock( 'networking_ovn.db.revision.bump_revision', return_value=None) self.del_rev_p = self._start_mock( 'networking_ovn.db.revision.delete_revision', return_value=None) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface(self, func): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} func.return_value = self.fake_router_interface_info self.l3_inst.add_router_interface(self.context, router_id, interface_info) self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('router-port-id', 'lrp-router-port-id', is_gw_port=False) self.bump_rev_p.assert_called_once_with(self.fake_router_port, ovn_const.TYPE_ROUTER_PORTS) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') def test_add_router_interface_update_lrouter_port(self, getp, func): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} func.return_value = {'id': router_id, 'port_id': 'router-port-id', 'subnet_id': 'subnet-id1', 'subnet_ids': ['subnet-id1'], 'fixed_ips': [ {'ip_address': '2001:db8::1', 'subnet_id': 'subnet-id1'}, {'ip_address': '2001:dba::1', 'subnet_id': 'subnet-id2'}], 'mac_address': 'aa:aa:aa:aa:aa:aa' } getp.return_value = { 'id': 'router-port-id', 'fixed_ips': [ {'ip_address': '2001:db8::1', 'subnet_id': 'subnet-id1'}, {'ip_address': '2001:dba::1', 'subnet_id': 'subnet-id2'}], 'mac_address': 'aa:aa:aa:aa:aa:aa' } fake_rtr_intf_networks = ['2001:db8::1/24', '2001:dba::1/24'] self.l3_inst.add_router_interface(self.context, router_id, interface_info) called_args_dict = ( self.l3_inst._ovn.update_lrouter_port.call_args_list[0][1]) self.assertEqual(1, self.l3_inst._ovn.update_lrouter_port.call_count) self.assertItemsEqual(fake_rtr_intf_networks, called_args_dict.get('networks', [])) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('router-port-id', 'lrp-router-port-id', is_gw_port=False) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') def test_remove_router_interface(self, getp): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} getp.side_effect = n_exc.PortNotFound(port_id='router-port-id') self.l3_inst.remove_router_interface( self.context, router_id, interface_info) self.l3_inst._ovn.lrp_del.assert_called_once_with( 'lrp-router-port-id', 'neutron-router-id', if_exists=True) self.del_rev_p.assert_called_once_with('router-port-id', ovn_const.TYPE_ROUTER_PORTS) def test_remove_router_interface_update_lrouter_port(self): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} self.l3_inst.remove_router_interface( self.context, router_id, interface_info) self.l3_inst._ovn.update_lrouter_port.assert_called_once_with( if_exists=False, name='lrp-router-port-id', ipv6_ra_configs={}, networks=['10.0.0.100/24'], external_ids={ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}) @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') def test_update_router_admin_state_change(self, get_rps, get_r, func): router_id = 'router-id' get_r.return_value = self.fake_router new_router = self.fake_router.copy() updated_data = {'admin_state_up': True} new_router.update(updated_data) func.return_value = new_router self.l3_inst.update_router(self.context, router_id, {'router': updated_data}) self.l3_inst._ovn.update_lrouter.assert_called_once_with( 'neutron-router-id', enabled=True, external_ids={ ovn_const.OVN_GW_PORT_EXT_ID_KEY: '', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router'}) @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') def test_update_router_name_change(self, get_rps, get_r, func): router_id = 'router-id' get_r.return_value = self.fake_router new_router = self.fake_router.copy() updated_data = {'name': 'test'} new_router.update(updated_data) func.return_value = new_router self.l3_inst.update_router(self.context, router_id, {'router': updated_data}) self.l3_inst._ovn.update_lrouter.assert_called_once_with( 'neutron-router-id', enabled=False, external_ids={ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'test', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_GW_PORT_EXT_ID_KEY: ''}) @mock.patch.object(utils, 'get_lrouter_non_gw_routes') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_router') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') def test_update_router_static_route_no_change(self, get_rps, get_r, func, mock_routes): router_id = 'router-id' get_rps.return_value = [{'device_id': '', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', 'fixed_ips': [{'ip_address': '10.0.0.100', 'subnet_id': 'subnet-id'}], 'id': 'router-port-id'}] mock_routes.return_value = self.fake_router['routes'] update_data = {'router': {'routes': [{'destination': '1.1.1.0/24', 'nexthop': '10.0.0.2'}]}} self.l3_inst.update_router(self.context, router_id, update_data) self.assertFalse(self.l3_inst._ovn.add_static_route.called) self.assertFalse(self.l3_inst._ovn.delete_static_route.called) @mock.patch.object(utils, 'get_lrouter_non_gw_routes') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') def test_update_router_static_route_change(self, get_rps, get_r, func, mock_routes): router_id = 'router-id' get_rps.return_value = [{'device_id': '', 'device_owner': 'network:router_interface', 'mac_address': 'aa:aa:aa:aa:aa:aa', 'fixed_ips': [{'ip_address': '10.0.0.100', 'subnet_id': 'subnet-id'}], 'id': 'router-port-id'}] mock_routes.return_value = self.fake_router['routes'] get_r.return_value = self.fake_router new_router = self.fake_router.copy() updated_data = {'routes': [{'destination': '2.2.2.0/24', 'nexthop': '10.0.0.3'}]} new_router.update(updated_data) func.return_value = new_router self.l3_inst.update_router(self.context, router_id, {'router': updated_data}) self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='2.2.2.0/24', nexthop='10.0.0.3') self.l3_inst._ovn.delete_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='1.1.1.0/24', nexthop='10.0.0.2') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') def test_create_router_with_ext_gw(self, get_rps, get_subnet, get_port): self.l3_inst._ovn.is_col_present.return_value = True router = {'router': {'name': 'router'}} get_subnet.return_value = self.fake_ext_subnet get_port.return_value = self.fake_ext_gw_port get_rps.return_value = self.fake_ext_subnet['cidr'] self.l3_inst.create_router(self.context, router) external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'router', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_GW_PORT_EXT_ID_KEY: 'gw-port-id'} self.l3_inst._ovn.create_lrouter.assert_called_once_with( 'neutron-router-id', external_ids=external_ids, enabled=True, options={}) self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) expected_calls = [ mock.call('neutron-router-id', ip_prefix='0.0.0.0/0', nexthop='192.168.1.254', external_ids={ ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: 'ext-subnet-id'})] self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('gw-port-id', 'lrp-gw-port-id', is_gw_port=True) self.l3_inst._ovn.add_static_route.assert_has_calls(expected_calls) bump_rev_calls = [mock.call(self.fake_ext_gw_port, ovn_const.TYPE_ROUTER_PORTS), mock.call(self.fake_router_with_ext_gw, ovn_const.TYPE_ROUTERS), ] self.assertEqual(len(bump_rev_calls), self.bump_rev_p.call_count) self.bump_rev_p.assert_has_calls(bump_rev_calls, any_order=False) @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') def test_delete_router_with_ext_gw(self, gs, gr, gprs): gr.return_value = self.fake_router_with_ext_gw gs.return_value = self.fake_ext_subnet self.l3_inst.delete_router(self.context, 'router-id') self.l3_inst._ovn.delete_lrouter.assert_called_once_with( 'neutron-router-id') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface_with_gateway_set(self, ari, gr, grps, gs, gp): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} ari.return_value = self.fake_router_interface_info gr.return_value = self.fake_router_with_ext_gw gs.return_value = self.fake_subnet gp.return_value = self.fake_router_port self.l3_inst.add_router_interface(self.context, router_id, interface_info) self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('router-port-id', 'lrp-router-port-id', is_gw_port=False) self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', logical_ip='10.0.0.0/24', external_ip='192.168.1.1', type='snat') self.bump_rev_p.assert_called_with(self.fake_router_port, ovn_const.TYPE_ROUTER_PORTS) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.add_router_interface') def test_add_router_interface_with_gateway_set_and_snat_disabled( self, ari, gr, grps, gs, gp): router_id = 'router-id' interface_info = {'port_id': 'router-port-id'} ari.return_value = self.fake_router_interface_info gr.return_value = self.fake_router_with_ext_gw gr.return_value['external_gateway_info']['enable_snat'] = False gs.return_value = self.fake_subnet gp.return_value = self.fake_router_port self.l3_inst.add_router_interface(self.context, router_id, interface_info) self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_router_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('router-port-id', 'lrp-router-port-id', is_gw_port=False) self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_remove_router_interface_with_gateway_set(self, gr, gs, gp): router_id = 'router-id' interface_info = {'port_id': 'router-port-id', 'subnet_id': 'subnet-id'} gr.return_value = self.fake_router_with_ext_gw gs.return_value = self.fake_subnet gp.side_effect = n_exc.PortNotFound(port_id='router-port-id') self.l3_inst.remove_router_interface( self.context, router_id, interface_info) self.l3_inst._ovn.lrp_del.assert_called_once_with( 'lrp-router-port-id', 'neutron-router-id', if_exists=True) self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', logical_ip='10.0.0.0/24', external_ip='192.168.1.1', type='snat') self.del_rev_p.assert_called_with('router-port-id', ovn_const.TYPE_ROUTER_PORTS) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_update_router_with_ext_gw(self, gr, ur, gs, grps, gp): self.l3_inst._ovn.is_col_present.return_value = True router = {'router': {'name': 'router'}} gr.return_value = self.fake_router_without_ext_gw ur.return_value = self.fake_router_with_ext_gw gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('gw-port-id', 'lrp-gw-port-id', is_gw_port=True) self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='0.0.0.0/0', external_ids={ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: 'ext-subnet-id'}, nexthop='192.168.1.254') self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='snat', logical_ip='10.0.0.0/24', external_ip='192.168.1.1') self.bump_rev_p.assert_called_with(self.fake_ext_gw_port, ovn_const.TYPE_ROUTER_PORTS) @mock.patch.object(utils, 'get_lrouter_ext_gw_static_route') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_update_router_ext_gw_change_subnet(self, gr, ur, gs, grps, gp, mock_get_gw): self.l3_inst._ovn.is_col_present.return_value = True mock_get_gw.return_value = mock.sentinel.GwRoute router = {'router': {'name': 'router'}} fake_old_ext_subnet = {'id': 'old-ext-subnet-id', 'ip_version': 4, 'cidr': '192.168.2.0/24', 'gateway_ip': '192.168.2.254'} # Old gateway info with same network and different subnet gr.return_value = copy.copy(self.fake_router_with_ext_gw) gr.return_value['external_gateway_info'] = { 'network_id': 'ext-network-id', 'external_fixed_ips': [{'ip_address': '192.168.2.1', 'subnet_id': 'old-ext-subnet-id'}]} gr.return_value['gw_port_id'] = 'old-gw-port-id' ur.return_value = self.fake_router_with_ext_gw gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet, 'old-ext-subnet-id': fake_old_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) # Check deleting old router gateway self.l3_inst._ovn.delete_lrouter_ext_gw.assert_called_once_with( 'neutron-router-id') # Check adding new router gateway self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('gw-port-id', 'lrp-gw-port-id', is_gw_port=True) self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='0.0.0.0/0', nexthop='192.168.1.254', external_ids={ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: 'ext-subnet-id'}) self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='snat', logical_ip='10.0.0.0/24', external_ip='192.168.1.1') self.bump_rev_p.assert_called_with(self.fake_ext_gw_port, ovn_const.TYPE_ROUTER_PORTS) self.del_rev_p.assert_called_once_with('old-gw-port-id', ovn_const.TYPE_ROUTER_PORTS) @mock.patch.object(utils, 'get_lrouter_ext_gw_static_route') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_update_router_ext_gw_change_ip_address(self, gr, ur, gs, grps, gp, mock_get_gw): self.l3_inst._ovn.is_col_present.return_value = True mock_get_gw.return_value = mock.sentinel.GwRoute router = {'router': {'name': 'router'}} # Old gateway info with same subnet and different ip address gr_value = copy.deepcopy(self.fake_router_with_ext_gw) gr_value['external_gateway_info'][ 'external_fixed_ips'][0]['ip_address'] = '192.168.1.2' gr_value['gw_port_id'] = 'old-gw-port-id' gr.return_value = gr_value ur.return_value = self.fake_router_with_ext_gw gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) # Check deleting old router gateway self.l3_inst._ovn.delete_lrouter_ext_gw.assert_called_once_with( 'neutron-router-id') # Check adding new router gateway self.l3_inst._ovn.add_lrouter_port.assert_called_once_with( **self.fake_ext_gw_port_assert) self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.\ assert_called_once_with('gw-port-id', 'lrp-gw-port-id', is_gw_port=True) self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='0.0.0.0/0', nexthop='192.168.1.254', external_ids={ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: 'ext-subnet-id'}) self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='snat', logical_ip='10.0.0.0/24', external_ip='192.168.1.1') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_update_router_ext_gw_no_change(self, gr, ur, get_rps): router = {'router': {'name': 'router'}} gr.return_value = self.fake_router_with_ext_gw ur.return_value = self.fake_router_with_ext_gw self.l3_inst._ovn.get_lrouter.return_value = ( fakes.FakeOVNRouter.from_neutron_router( self.fake_router_with_ext_gw)) self.l3_inst.update_router(self.context, 'router-id', router) self.l3_inst._ovn.lrp_del.assert_not_called() self.l3_inst._ovn.delete_static_route.assert_not_called() self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_not_called() self.l3_inst._ovn.add_lrouter_port.assert_not_called() self.l3_inst._ovn.set_lrouter_port_in_lswitch_port.assert_not_called() self.l3_inst._ovn.add_static_route.assert_not_called() self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_update_router_with_ext_gw_and_disabled_snat(self, gr, ur, gs, grps, gp): self.l3_inst._ovn.is_col_present.return_value = True router = {'router': {'name': 'router'}} gr.return_value = self.fake_router_without_ext_gw ur.return_value = self.fake_router_with_ext_gw ur.return_value['external_gateway_info']['enable_snat'] = False gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) # Need not check lsp and lrp here, it has been tested in other cases self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-router-id', ip_prefix='0.0.0.0/0', external_ids={ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: 'ext-subnet-id'}, nexthop='192.168.1.254') self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient._get_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_enable_snat(self, gr, ur, gs, grps, gp): router = {'router': {'name': 'router'}} gr.return_value = copy.deepcopy(self.fake_router_with_ext_gw) gr.return_value['external_gateway_info']['enable_snat'] = False ur.return_value = self.fake_router_with_ext_gw self.l3_inst._ovn.get_lrouter.return_value = ( fakes.FakeOVNRouter.from_neutron_router( self.fake_router_with_ext_gw)) gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) self.l3_inst._ovn.delete_static_route.assert_not_called() self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_not_called() self.l3_inst._ovn.add_static_route.assert_not_called() self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='snat', logical_ip='10.0.0.0/24', external_ip='192.168.1.1') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_check_external_ips_changed') @mock.patch.object(utils, 'get_lrouter_snats') @mock.patch.object(utils, 'get_lrouter_ext_gw_static_route') @mock.patch('networking_ovn.common.utils.is_snat_enabled', mock.Mock(return_value=True)) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('networking_ovn.common.ovn_client.OVNClient.' '_get_router_ports') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_subnet') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_router') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_router') def test_disable_snat(self, gr, ur, gs, grps, gp, mock_get_gw, mock_snats, mock_ext_ips): mock_get_gw.return_value = mock.sentinel.GwRoute mock_snats.return_value = [mock.sentinel.NAT] mock_ext_ips.return_value = False router = {'router': {'name': 'router'}} gr.return_value = self.fake_router_with_ext_gw ur.return_value = copy.deepcopy(self.fake_router_with_ext_gw) ur.return_value['external_gateway_info']['enable_snat'] = False gs.side_effect = lambda ctx, sid: { 'ext-subnet-id': self.fake_ext_subnet}.get(sid, self.fake_subnet) gp.return_value = self.fake_ext_gw_port grps.return_value = self.fake_router_ports self.l3_inst.update_router(self.context, 'router-id', router) self.l3_inst._ovn.delete_static_route.assert_not_called() self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='snat', logical_ip='10.0.0.0/24', external_ip='192.168.1.1') self.l3_inst._ovn.add_static_route.assert_not_called() self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') def test_create_floatingip(self, gf): self.l3_inst._ovn.is_col_present.return_value = True gf.return_value = {'floating_port_id': 'fip-port-id'} self.l3_inst.create_floatingip(self.context, 'floatingip') expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) self.l3_inst._ovn.delete_lswitch_port.assert_called_once_with( 'fip-port-id', 'neutron-fip-net-id') @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') def test_create_floatingip_distributed(self, gf, gp): self.l3_inst._ovn.is_col_present.return_value = True gp.return_value = {'mac_address': '00:01:02:03:04:05'} gf.return_value = {'floating_port_id': 'fip-port-id'} config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') self.l3_inst.create_floatingip(self.context, 'floatingip') expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10', external_mac='00:01:02:03:04:05', logical_port='port_id', external_ids=expected_ext_ids) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') def test_create_floatingip_external_ip_present_in_nat_rule(self, gf): self.l3_inst._ovn.is_col_present.return_value = True gf.return_value = {'floating_port_id': 'fip-port-id'} self.l3_inst._ovn.get_lrouter_nat_rules.return_value = [ {'external_ip': '192.168.0.10', 'logical_ip': '10.0.0.6', 'type': 'dnat_and_snat', 'uuid': 'uuid1'}] self.l3_inst.create_floatingip(self.context, 'floatingip') self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip['router_id'])} self.l3_inst._ovn.set_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', 'uuid1', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) self.l3_inst._ovn.delete_lswitch_port.assert_called_once_with( 'fip-port-id', 'neutron-fip-net-id') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') def test_create_floatingip_external_ip_present_type_snat(self, gf): self.l3_inst._ovn.is_col_present.return_value = True gf.return_value = {'floating_port_id': 'fip-port-id'} self.l3_inst._ovn.get_lrouter_nat_rules.return_value = [ {'external_ip': '192.168.0.10', 'logical_ip': '10.0.0.0/24', 'type': 'snat', 'uuid': 'uuid1'}] self.l3_inst.create_floatingip(self.context, 'floatingip') self.l3_inst._ovn.set_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) self.l3_inst._ovn.delete_lswitch_port.assert_called_once_with( 'fip-port-id', 'neutron-fip-net-id') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.delete_floatingip') def test_delete_floatingip(self, df): self.l3_inst._ovn.get_floatingip.return_value = ( self.fake_ovn_nat_rule) self.l3_inst.delete_floatingip(self.context, 'floatingip-id') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_floatingip') def test_update_floatingip(self, uf, gf): self.l3_inst._ovn.is_col_present.return_value = True gf.return_value = self.fake_floating_ip uf.return_value = self.fake_floating_ip_new self.l3_inst._ovn.get_floatingip.return_value = ( self.fake_ovn_nat_rule) self.l3_inst.update_floatingip(self.context, 'id', 'floatingip') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10') expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip_new['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip_new['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-new-router-id', type='dnat_and_snat', logical_ip='10.10.10.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_floatingip') def test_update_floatingip_associate(self, uf, gf): self.l3_inst._ovn.is_col_present.return_value = True self.fake_floating_ip.update({'fixed_port_id': None}) gf.return_value = self.fake_floating_ip uf.return_value = self.fake_floating_ip_new self.l3_inst.update_floatingip(self.context, 'id', 'floatingip') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip_new['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip_new['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-new-router-id', type='dnat_and_snat', logical_ip='10.10.10.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_floatingip') def test_update_floatingip_associate_distributed(self, uf, gf, gp): self.l3_inst._ovn.is_col_present.return_value = True self.fake_floating_ip.update({'fixed_port_id': None}) gp.return_value = {'mac_address': '00:01:02:03:04:05'} gf.return_value = self.fake_floating_ip uf.return_value = self.fake_floating_ip_new config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') self.l3_inst.update_floatingip(self.context, 'id', 'floatingip') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_not_called() expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip_new['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip_new['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-new-router-id', type='dnat_and_snat', logical_ip='10.10.10.10', external_ip='192.168.0.10', external_mac='00:01:02:03:04:05', logical_port='new-port_id', external_ids=expected_ext_ids) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_floatingip') def test_update_floatingip_association_not_changed(self, uf, gf): self.fake_floating_ip.update({'fixed_port_id': None}) self.fake_floating_ip_new.update({'port_id': None}) gf.return_value = self.fake_floating_ip uf.return_value = self.fake_floating_ip_new self.l3_inst.update_floatingip(self.context, 'id', 'floatingip') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_not_called() self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_not_called() @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin._get_floatingip') @mock.patch('neutron.db.extraroute_db.ExtraRoute_dbonly_mixin.' 'update_floatingip') def test_update_floatingip_reassociate_to_same_port_diff_fixed_ip( self, uf, gf): self.l3_inst._ovn.is_col_present.return_value = True self.l3_inst._ovn.get_floatingip.return_value = ( self.fake_ovn_nat_rule) self.fake_floating_ip_new.update({'port_id': 'port_id', 'fixed_port_id': 'port_id'}) gf.return_value = self.fake_floating_ip uf.return_value = self.fake_floating_ip_new self.l3_inst.update_floatingip(self.context, 'id', 'floatingip') self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_called_once_with( 'neutron-router-id', type='dnat_and_snat', logical_ip='10.0.0.10', external_ip='192.168.0.10') expected_ext_ids = { ovn_const.OVN_FIP_EXT_ID_KEY: self.fake_floating_ip_new['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', ovn_const.OVN_FIP_PORT_EXT_ID_KEY: self.fake_floating_ip_new['port_id'], ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: utils.ovn_name( self.fake_floating_ip_new['router_id'])} self.l3_inst._ovn.add_nat_rule_in_lrouter.assert_called_once_with( 'neutron-new-router-id', type='dnat_and_snat', logical_ip='10.10.10.10', external_ip='192.168.0.10', external_ids=expected_ext_ids) @mock.patch('neutron.db.l3_db.L3_NAT_dbonly_mixin.get_floatingips') def test_disassociate_floatingips(self, gfs): gfs.return_value = [{'id': 'fip-id1', 'floating_ip_address': '192.168.0.10', 'router_id': 'router-id', 'port_id': 'port_id', 'floating_port_id': 'fip-port-id1', 'fixed_ip_address': '10.0.0.10'}, {'id': 'fip-id2', 'floating_ip_address': '192.167.0.10', 'router_id': 'router-id', 'port_id': 'port_id', 'floating_port_id': 'fip-port-id2', 'fixed_ip_address': '10.0.0.11'}] self.l3_inst.disassociate_floatingips(self.context, 'port_id', do_notify=False) delete_nat_calls = [mock.call('neutron-router-id', type='dnat_and_snat', logical_ip=fip['fixed_ip_address'], external_ip=fip['floating_ip_address']) for fip in gfs.return_value] self.assertEqual( len(delete_nat_calls), self.l3_inst._ovn.delete_nat_rule_in_lrouter.call_count) self.l3_inst._ovn.delete_nat_rule_in_lrouter.assert_has_calls( delete_nat_calls, any_order=True) @mock.patch('networking_ovn.common.ovn_client.OVNClient' '.update_router_port') def test_port_update_postcommit(self, update_rp_mock): kwargs = {'port': {'device_owner': 'foo'}} self.l3_inst._port_update(resources.PORT, events.AFTER_UPDATE, None, **kwargs) update_rp_mock.assert_not_called() kwargs = {'port': {'device_owner': constants.DEVICE_OWNER_ROUTER_INTF}} self.l3_inst._port_update(resources.PORT, events.AFTER_UPDATE, None, **kwargs) update_rp_mock.assert_called_once_with(kwargs['port'], if_exists=True) class OVNL3ExtrarouteTests(test_l3_gw.ExtGwModeIntTestCase, test_l3.L3NatDBIntTestCase, test_extraroute.ExtraRouteDBTestCaseBase): # TODO(lucasagomes): Ideally, this method should be moved to a base # class which all tests classes in networking-ovn inherits from but, # this base class doesn't seem to exist for now so we need to duplicate # it here def _start_mock(self, path, return_value, new_callable=None): patcher = mock.patch(path, return_value=return_value, new_callable=new_callable) patcher.start() self.addCleanup(patcher.stop) def setUp(self): plugin = 'neutron.tests.unit.extensions.test_l3.TestNoL3NatPlugin' l3_plugin = ('networking_ovn.l3.l3_ovn.OVNL3RouterPlugin') service_plugins = {'l3_plugin_name': l3_plugin} # For these tests we need to enable overlapping ips cfg.CONF.set_default('allow_overlapping_ips', True) cfg.CONF.set_default('max_routes', 3) ext_mgr = test_extraroute.ExtraRouteTestExtensionManager() super(test_l3.L3BaseForIntTests, self).setUp( plugin=plugin, ext_mgr=ext_mgr, service_plugins=service_plugins) revision_plugin.RevisionPlugin() l3_gw_mgr = test_l3_gw.TestExtensionManager() test_extensions.setup_extensions_middleware(l3_gw_mgr) self.l3_inst = directory.get_plugin(plugin_constants.L3) self._start_mock( 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._ovn', new_callable=mock.PropertyMock, return_value=fakes.FakeOvsdbNbOvnIdl()) self._start_mock( 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._sb_ovn', new_callable=mock.PropertyMock, return_value=fakes.FakeOvsdbSbOvnIdl()) self._start_mock( 'networking_ovn.l3.l3_ovn_scheduler.' 'OVNGatewayScheduler._schedule_gateway', return_value='hv1') self._start_mock( 'networking_ovn.common.ovn_client.OVNClient.' '_get_v4_network_of_all_router_ports', return_value=[]) self._start_mock( 'networking_ovn.common.ovn_client.' 'OVNClient.update_floatingip_status', return_value=None) self._start_mock( 'networking_ovn.common.utils.get_revision_number', return_value=1) self.setup_notification_driver() # Note(dongj): According to bug #1657693, status of an unassociated # floating IP is set to DOWN. Revise expected_status to DOWN for related # test cases. def test_floatingip_update( self, expected_status=constants.FLOATINGIP_STATUS_DOWN): super(OVNL3ExtrarouteTests, self).test_floatingip_update( expected_status) def test_floatingip_update_to_same_port_id_twice( self, expected_status=constants.FLOATINGIP_STATUS_DOWN): super(OVNL3ExtrarouteTests, self).\ test_floatingip_update_to_same_port_id_twice(expected_status) def test_floatingip_update_subnet_gateway_disabled( self, expected_status=constants.FLOATINGIP_STATUS_DOWN): super(OVNL3ExtrarouteTests, self).\ test_floatingip_update_subnet_gateway_disabled(expected_status) # Test function _subnet_update of L3 OVN plugin. def test_update_subnet_gateway_for_external_net(self): super(OVNL3ExtrarouteTests, self). \ test_update_subnet_gateway_for_external_net() self.l3_inst._ovn.add_static_route.assert_called_once_with( 'neutron-fake_device', ip_prefix='0.0.0.0/0', nexthop='120.0.0.2') self.l3_inst._ovn.delete_static_route.assert_called_once_with( 'neutron-fake_device', ip_prefix='0.0.0.0/0', nexthop='120.0.0.1') networking-ovn-4.0.0/networking_ovn/tests/unit/l3/__init__.py0000666000175100017510000000000013245511145024340 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/cmd/0000775000175100017510000000000013245511554022470 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/cmd/__init__.py0000666000175100017510000000000013245511145024565 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/cmd/test_neutron_ovn_db_sync_util.py0000666000175100017510000001274113245511145031216 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 mock from networking_ovn.cmd import neutron_ovn_db_sync_util as cmd from networking_ovn.tests import base class TestNeutronOVNDBSyncUtil(base.TestCase): def setUp(self): super(TestNeutronOVNDBSyncUtil, self).setUp() self.cmd_log = mock.Mock() cmd.LOG = self.cmd_log self.cmd_sync = mock.Mock() self.cmd_sync.do_sync = mock.Mock() self.cmd_sb_sync = mock.Mock() self.cmd_sb_sync.do_sync = mock.Mock() def _setup_default_mock_cfg(self, mock_cfg): mock_cfg.ovn.neutron_sync_mode = 'log' mock_cfg.core_plugin = 'neutron.plugins.ml2.plugin.Ml2Plugin' mock_cfg.ml2.mechanism_drivers = ['ovn'] # Test that the configuration can be loaded successfully. def test_setup_conf(self): cmd.setup_conf() def test_main_invalid_conf(self): with mock.patch( 'networking_ovn.cmd.neutron_ovn_db_sync_util.setup_conf', return_value=None): cmd.main() self.cmd_log.error.assert_called_once_with( 'Error parsing the configuration values. Please verify.') @mock.patch('oslo_log.log.setup') @mock.patch('networking_ovn.cmd.neutron_ovn_db_sync_util.setup_conf') @mock.patch('networking_ovn.ovsdb.impl_idl_ovn.get_connection') def test_main_invalid_nb_idl(self, mock_con, mock_conf, mock_log_setup): with mock.patch('oslo_config.cfg.CONF') as mock_cfg, \ mock.patch('networking_ovn.ovsdb.impl_idl_ovn.OvsdbNbOvnIdl', side_effect=RuntimeError): self._setup_default_mock_cfg(mock_cfg) cmd.main() self.cmd_log.error.assert_called_once_with( 'Invalid --ovn-ovn_nb_connection parameter provided.') @mock.patch('oslo_log.log.setup') @mock.patch('networking_ovn.cmd.neutron_ovn_db_sync_util.setup_conf') @mock.patch('networking_ovn.ovsdb.impl_idl_ovn.get_connection') def test_main_invalid_sb_idl(self, mock_con, mock_conf, mock_log_setup): with mock.patch('oslo_config.cfg.CONF') as mock_cfg, \ mock.patch('networking_ovn.ovsdb.impl_idl_ovn.OvsdbSbOvnIdl', side_effect=RuntimeError): self._setup_default_mock_cfg(mock_cfg) cmd.main() self.cmd_log.error.assert_called_once_with( 'Invalid --ovn-ovn_sb_connection parameter provided.') @mock.patch('neutron.manager.init') @mock.patch('neutron_lib.plugins.directory.get_plugin') @mock.patch('networking_ovn.ovsdb.impl_idl_ovn.OvsdbNbOvnIdl') @mock.patch('oslo_log.log.setup') @mock.patch('networking_ovn.cmd.neutron_ovn_db_sync_util.setup_conf') @mock.patch('networking_ovn.ovsdb.impl_idl_ovn.get_connection') def _test_main(self, mock_con, mock_conf, mock_log_setup, mock_nb_idl, mock_plugin, mock_manager_init): cmd.main() def test_main_invalid_sync_mode(self): with mock.patch('oslo_config.cfg.CONF') as mock_cfg: self._setup_default_mock_cfg(mock_cfg) mock_cfg.ovn.neutron_sync_mode = 'off' self._test_main() self.cmd_log.error.assert_called_once_with( 'Invalid sync mode : ["%s"]. Should be "log" or "repair"', 'off') def test_main_invalid_core_plugin(self): with mock.patch('oslo_config.cfg.CONF') as mock_cfg: self._setup_default_mock_cfg(mock_cfg) mock_cfg.core_plugin = 'foo' self._test_main() self.cmd_log.error.assert_called_once_with( 'Invalid core plugin : ["%s"].', 'foo') def test_main_no_mechanism_driver(self): with mock.patch('oslo_config.cfg.CONF') as mock_cfg: mock_cfg.ovn.neutron_sync_mode = 'repair' mock_cfg.core_plugin = 'ml2' mock_cfg.ml2.mechanism_drivers = [] self._test_main() self.cmd_log.error.assert_called_once_with( 'please use --config-file to specify ' 'neutron and ml2 configuration file.') def test_main_invalid_mechanism_driver(self): with mock.patch('oslo_config.cfg.CONF') as mock_cfg: self._setup_default_mock_cfg(mock_cfg) mock_cfg.ml2.mechanism_drivers = ['foo'] self._test_main() self.cmd_log.error.assert_called_once_with( 'No "ovn" mechanism driver found : "%s".', ['foo']) def _test_main_sync(self): with mock.patch('networking_ovn.ovn_db_sync.OvnNbSynchronizer', return_value=self.cmd_sync), \ mock.patch('networking_ovn.ovn_db_sync.OvnSbSynchronizer', return_value=self.cmd_sb_sync), \ mock.patch('oslo_config.cfg.CONF') as mock_cfg: self._setup_default_mock_cfg(mock_cfg) self._test_main() def test_main_sync_success(self): self._test_main_sync() self.cmd_sync.do_sync.assert_called_once_with() self.cmd_sb_sync.do_sync.assert_called_once_with() networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/0000775000175100017510000000000013245511554023042 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/test_ovsdb_monitor.py0000666000175100017510000003570013245511145027342 0ustar zuulzuul00000000000000# 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 copy import os import mock from neutron_lib.plugins import constants from neutron_lib.plugins import directory from oslo_utils import uuidutils from ovs.db import idl as ovs_idl from ovs import poller from ovs.stream import Stream from ovsdbapp.backend.ovs_idl import connection from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn.common import config as ovn_config from networking_ovn.ovsdb import ovsdb_monitor from networking_ovn.tests import base from networking_ovn.tests.unit.ml2 import test_mech_driver basedir = os.path.dirname(os.path.abspath(__file__)) schema_files = { 'OVN_Northbound': os.path.join(basedir, 'schemas', 'ovn-nb.ovsschema'), 'OVN_Southbound': os.path.join(basedir, 'schemas', 'ovn-sb.ovsschema'), } OVN_NB_SCHEMA = { "name": "OVN_Northbound", "version": "3.0.0", "tables": { "Logical_Switch_Port": { "columns": { "name": {"type": "string"}, "type": {"type": "string"}, "addresses": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "port_security": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "up": {"type": {"key": "boolean", "min": 0, "max": 1}}}, "indexes": [["name"]], "isRoot": False, }, "Logical_Switch": { "columns": {"name": {"type": "string"}}, "indexes": [["name"]], "isRoot": True, } } } OVN_SB_SCHEMA = { "name": "OVN_Southbound", "version": "1.3.0", "tables": { "Chassis": { "columns": { "name": {"type": "string"}, "hostname": {"type": "string"}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": True, "indexes": [["name"]] } } } class TestOvnNbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase): def setUp(self): super(TestOvnNbIdlNotifyHandler, self).setUp() helper = ovs_idl.SchemaHelper(schema_json=OVN_NB_SCHEMA) helper.register_all() self.idl = ovsdb_monitor.OvnNbIdl(self.driver, "remote", helper) self.idl.lock_name = self.idl.event_lock_name self.idl.has_lock = True self.lp_table = self.idl.tables.get('Logical_Switch_Port') self.driver.set_port_status_up = mock.Mock() self.driver.set_port_status_down = mock.Mock() def _test_lsp_helper(self, event, new_row_json, old_row_json=None, table=None): row_uuid = uuidutils.generate_uuid() if not table: table = self.lp_table lp_row = ovs_idl.Row.from_json(self.idl, table, row_uuid, new_row_json) if old_row_json: old_row = ovs_idl.Row.from_json(self.idl, table, row_uuid, old_row_json) else: old_row = None self.idl.notify(event, lp_row, updates=old_row) # Add a STOP EVENT to the queue self.idl.notify_handler.shutdown() # Execute the notifications queued self.idl.notify_handler.notify_loop() def test_lsp_up_create_event(self): row_data = {"up": True, "name": "foo-name"} self._test_lsp_helper('create', row_data) self.driver.set_port_status_up.assert_called_once_with("foo-name") self.assertFalse(self.driver.set_port_status_down.called) def test_lsp_down_create_event(self): row_data = {"up": False, "name": "foo-name"} self._test_lsp_helper('create', row_data) self.driver.set_port_status_down.assert_called_once_with("foo-name") self.assertFalse(self.driver.set_port_status_up.called) def test_lsp_up_not_set_event(self): row_data = {"up": ['set', []], "name": "foo-name"} self._test_lsp_helper('create', row_data) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_unwatch_logical_switch_port_create_events(self): self.idl.unwatch_logical_switch_port_create_events() row_data = {"up": True, "name": "foo-name"} self._test_lsp_helper('create', row_data) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) row_data["up"] = False self._test_lsp_helper('create', row_data) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_post_connect(self): self.idl.post_connect() self.assertIsNone(self.idl._lsp_create_up_event) self.assertIsNone(self.idl._lsp_create_down_event) def test_lsp_up_update_event(self): new_row_json = {"up": True, "name": "foo-name"} old_row_json = {"up": False} self._test_lsp_helper('update', new_row_json, old_row_json=old_row_json) self.driver.set_port_status_up.assert_called_once_with("foo-name") self.assertFalse(self.driver.set_port_status_down.called) def test_lsp_down_update_event(self): new_row_json = {"up": False, "name": "foo-name"} old_row_json = {"up": True} self._test_lsp_helper('update', new_row_json, old_row_json=old_row_json) self.driver.set_port_status_down.assert_called_once_with("foo-name") self.assertFalse(self.driver.set_port_status_up.called) def test_lsp_up_update_event_no_old_data(self): new_row_json = {"up": True, "name": "foo-name"} self._test_lsp_helper('update', new_row_json, old_row_json=None) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_lsp_down_update_event_no_old_data(self): new_row_json = {"up": False, "name": "foo-name"} self._test_lsp_helper('update', new_row_json, old_row_json=None) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_lsp_other_column_update_event(self): new_row_json = {"up": False, "name": "foo-name", "addresses": ["10.0.0.2"]} old_row_json = {"addresses": ["10.0.0.3"]} self._test_lsp_helper('update', new_row_json, old_row_json=old_row_json) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_notify_other_table(self): new_row_json = {"name": "foo-name"} self._test_lsp_helper('create', new_row_json, table=self.idl.tables.get("Logical_Switch")) self.assertFalse(self.driver.set_port_status_up.called) self.assertFalse(self.driver.set_port_status_down.called) def test_notify_no_ovsdb_lock(self): self.idl.is_lock_contended = True self.idl.notify_handler.notify = mock.Mock() self.idl.notify("create", mock.ANY) self.assertFalse(self.idl.notify_handler.notify.called) def test_notify_ovsdb_lock_not_yet_contended(self): self.idl.is_lock_contended = False self.idl.notify_handler.notify = mock.Mock() self.idl.notify("create", mock.ANY) self.assertTrue(self.idl.notify_handler.notify.called) class TestOvnSbIdlNotifyHandler(test_mech_driver.OVNMechanismDriverTestCase): l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin' def setUp(self): super(TestOvnSbIdlNotifyHandler, self).setUp() sb_helper = ovs_idl.SchemaHelper(schema_json=OVN_SB_SCHEMA) sb_helper.register_table('Chassis') self.sb_idl = ovsdb_monitor.OvnSbIdl(self.driver, "remote", sb_helper) self.sb_idl.lock_name = self.sb_idl.event_lock_name self.sb_idl.has_lock = True self.sb_idl.post_connect() self.chassis_table = self.sb_idl.tables.get('Chassis') self.driver.update_segment_host_mapping = mock.Mock() self.l3_plugin = directory.get_plugin(constants.L3) self.l3_plugin.schedule_unhosted_gateways = mock.Mock() self.row_json = { "name": "fake-name", "hostname": "fake-hostname", "external_ids": ['map', [["ovn-bridge-mappings", "fake-phynet1:fake-br1"]]] } def _test_chassis_helper(self, event, new_row_json, old_row_json=None): row_uuid = uuidutils.generate_uuid() table = self.chassis_table row = ovs_idl.Row.from_json(self.sb_idl, table, row_uuid, new_row_json) if old_row_json: old_row = ovs_idl.Row.from_json(self.sb_idl, table, row_uuid, old_row_json) else: old_row = None self.sb_idl.notify(event, row, updates=old_row) # Add a STOP EVENT to the queue self.sb_idl.notify_handler.shutdown() # Execute the notifications queued self.sb_idl.notify_handler.notify_loop() def test_chassis_create_event(self): self._test_chassis_helper('create', self.row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', ['fake-phynet1']) self.assertEqual( 1, self.l3_plugin.schedule_unhosted_gateways.call_count) def test_chassis_delete_event(self): self._test_chassis_helper('delete', self.row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', []) self.assertEqual( 1, self.l3_plugin.schedule_unhosted_gateways.call_count) def test_chassis_update_event(self): old_row_json = copy.deepcopy(self.row_json) old_row_json['external_ids'][1][0][1] = ( "fake-phynet2:fake-br2") self._test_chassis_helper('update', self.row_json, old_row_json) self.driver.update_segment_host_mapping.assert_called_once_with( 'fake-hostname', ['fake-phynet1']) self.assertEqual( 1, self.l3_plugin.schedule_unhosted_gateways.call_count) class TestOvnDbNotifyHandler(base.TestCase): def setUp(self): super(TestOvnDbNotifyHandler, self).setUp() self.handler = ovsdb_monitor.OvnDbNotifyHandler(mock.ANY) self.watched_events = self.handler._RowEventHandler__watched_events def test_watch_and_unwatch_events(self): expected_events = set() networking_event = mock.Mock() ovn_event = mock.Mock() unknown_event = mock.Mock() self.assertItemsEqual(set(), self.watched_events) expected_events.add(networking_event) self.handler.watch_event(networking_event) self.assertItemsEqual(expected_events, self.watched_events) expected_events.add(ovn_event) self.handler.watch_events([ovn_event]) self.assertItemsEqual(expected_events, self.watched_events) self.handler.unwatch_events([networking_event, ovn_event]) self.handler.unwatch_event(unknown_event) self.handler.unwatch_events([unknown_event]) self.assertItemsEqual(set(), self.watched_events) def test_shutdown(self): self.handler.shutdown() # class TestOvnBaseConnection(base.TestCase): # # Each test is being deleted, but for reviewers sake I wanted to exaplain why: # # @mock.patch.object(idlutils, 'get_schema_helper') # def testget_schema_helper_success(self, mock_gsh): # # 1. OvnBaseConnection and OvnConnection no longer exist # 2. get_schema_helper is no longer a part of the Connection class # # @mock.patch.object(idlutils, 'get_schema_helper') # def testget_schema_helper_initial_exception(self, mock_gsh): # # @mock.patch.object(idlutils, 'get_schema_helper') # def testget_schema_helper_all_exception(self, mock_gsh): # # 3. The only reason get_schema_helper had a retry loop was for Neutron's # use case of trying to set the Manager to listen on ptcp:127.0.0.1:6640 # if it wasn't already set up. Since that code being removed was the whole # reason to re-implement get_schema_helper here,the exception retry is not # needed and therefor is not a part of ovsdbapp's implementation of # idlutils.get_schema_helper which we now use directly in from_server() # 4. These tests now would be testing the various from_server() calls, but # there is almost nothing to test in those except maybe SSL being set up # but that was done below. class TestOvnConnection(base.TestCase): def setUp(self): super(TestOvnConnection, self).setUp() @mock.patch.object(idlutils, 'get_schema_helper') @mock.patch.object(idlutils, 'wait_for_change') def _test_connection_start(self, mock_wfc, mock_gsh, idl_class, schema): mock_gsh.return_value = ovs_idl.SchemaHelper( location=schema_files[schema]) _idl = idl_class.from_server('punix:/tmp/fake', schema, mock.Mock()) self.ovn_connection = connection.Connection(_idl, mock.Mock()) with mock.patch.object(poller, 'Poller'), \ mock.patch('threading.Thread'): self.ovn_connection.start() # A second start attempt shouldn't re-register. self.ovn_connection.start() self.ovn_connection.thread.start.assert_called_once_with() def test_connection_nb_start(self): ovn_config.cfg.CONF.set_override('ovn_nb_private_key', 'foo-key', 'ovn') Stream.ssl_set_private_key_file = mock.Mock() Stream.ssl_set_certificate_file = mock.Mock() Stream.ssl_set_ca_cert_file = mock.Mock() self._test_connection_start(idl_class=ovsdb_monitor.OvnNbIdl, schema='OVN_Northbound') Stream.ssl_set_private_key_file.assert_called_once_with('foo-key') Stream.ssl_set_certificate_file.assert_not_called() Stream.ssl_set_ca_cert_file.assert_not_called() def test_connection_sb_start(self): self._test_connection_start(idl_class=ovsdb_monitor.OvnSbIdl, schema='OVN_Southbound') networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/test_commands.py0000666000175100017510000016103313245511145026256 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 mock from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn.common import acl as ovn_acl from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils as ovn_utils from networking_ovn.ovsdb import commands from networking_ovn.tests import base from networking_ovn.tests.unit import fakes class TestBaseCommandHelpers(base.TestCase): def setUp(self): super(TestBaseCommandHelpers, self).setUp() self.column = 'ovn' self.new_value = '1' self.old_value = '2' def _get_fake_row_mutate(self): return fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={self.column: []}) def test__addvalue_to_list(self): fake_row_mutate = self._get_fake_row_mutate() commands._addvalue_to_list( fake_row_mutate, self.column, self.new_value) fake_row_mutate.addvalue.assert_called_once_with( self.column, self.new_value) fake_row_mutate.verify.assert_not_called() def test__delvalue_from_list(self): fake_row_mutate = self._get_fake_row_mutate() commands._delvalue_from_list( fake_row_mutate, self.column, self.old_value) fake_row_mutate.delvalue.assert_called_once_with( self.column, self.old_value) fake_row_mutate.verify.assert_not_called() def test__updatevalues_in_list_none(self): fake_row_mutate = self._get_fake_row_mutate() commands._updatevalues_in_list(fake_row_mutate, self.column) fake_row_mutate.addvalue.assert_not_called() fake_row_mutate.delvalue.assert_not_called() fake_row_mutate.verify.assert_not_called() def test__updatevalues_in_list_empty(self): fake_row_mutate = self._get_fake_row_mutate() commands._updatevalues_in_list(fake_row_mutate, self.column, [], []) fake_row_mutate.addvalue.assert_not_called() fake_row_mutate.delvalue.assert_not_called() fake_row_mutate.verify.assert_not_called() def test__updatevalues_in_list(self): fake_row_mutate = self._get_fake_row_mutate() commands._updatevalues_in_list( fake_row_mutate, self.column, new_values=[self.new_value], old_values=[self.old_value]) fake_row_mutate.addvalue.assert_called_once_with( self.column, self.new_value) fake_row_mutate.delvalue.assert_called_once_with( self.column, self.old_value) fake_row_mutate.verify.assert_not_called() class TestBaseCommand(base.TestCase): def setUp(self): super(TestBaseCommand, self).setUp() self.ovn_api = fakes.FakeOvsdbNbOvnIdl() self.transaction = fakes.FakeOvsdbTransaction() self.ovn_api.transaction = self.transaction class TestLSwitchSetExternalIdsCommand(TestBaseCommand): def _test_lswitch_extid_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.LSwitchSetExternalIdsCommand( self.ovn_api, 'fake-lswitch', {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'neutron-network'}, if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_no_exist_ignore(self): self._test_lswitch_extid_update_no_exist(if_exists=True) def test_lswitch_no_exist_fail(self): self._test_lswitch_extid_update_no_exist(if_exists=False) def test_lswitch_extid_update(self): network_name = 'private' new_network_name = 'private-new' ext_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network_name} new_ext_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: new_network_name} fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': ext_ids}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): cmd = commands.LSwitchSetExternalIdsCommand( self.ovn_api, fake_lswitch.name, {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: new_network_name}, if_exists=True) cmd.run_idl(self.transaction) self.assertEqual(new_ext_ids, fake_lswitch.external_ids) class TestAddLSwitchPortCommand(TestBaseCommand): def test_lswitch_not_found(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.AddLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lswitch', may_exist=True) self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) self.transaction.insert.assert_not_called() def test_lswitch_port_exists(self): with mock.patch.object(idlutils, 'row_by_value', return_value=mock.ANY): cmd = commands.AddLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lswitch', may_exist=True) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() def test_lswitch_port_add_exists(self): fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ fake_lsp self.transaction.insert.return_value = fake_lsp cmd = commands.AddLSwitchPortCommand( self.ovn_api, fake_lsp.name, fake_lswitch.name, may_exist=False) cmd.run_idl(self.transaction) # NOTE(rtheis): Mocking the transaction allows this insert # to succeed when it normally would fail due the duplicate name. self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Switch_Port']) def _test_lswitch_port_add(self, may_exist=True): lsp_name = 'fake-lsp' fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lswitch, None]): fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'foo': None}) self.transaction.insert.return_value = fake_lsp cmd = commands.AddLSwitchPortCommand( self.ovn_api, lsp_name, fake_lswitch.name, may_exist=may_exist, foo='bar') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Switch_Port']) fake_lswitch.addvalue.assert_called_once_with( 'ports', fake_lsp.uuid) self.assertEqual(lsp_name, fake_lsp.name) self.assertEqual('bar', fake_lsp.foo) def test_lswitch_port_add_may_exist(self): self._test_lswitch_port_add(may_exist=True) def test_lswitch_port_add_ignore_exists(self): self._test_lswitch_port_add(may_exist=False) def _test_lswitch_port_add_with_dhcp(self, dhcpv4_opts, dhcpv6_opts): lsp_name = 'fake-lsp' fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.transaction.insert.return_value = fake_lsp with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lswitch, None]): cmd = commands.AddLSwitchPortCommand( self.ovn_api, lsp_name, fake_lswitch.name, may_exist=True, dhcpv4_options=dhcpv4_opts, dhcpv6_options=dhcpv6_opts) if not isinstance(dhcpv4_opts, list): dhcpv4_opts.result = 'fake-uuid-1' if not isinstance(dhcpv6_opts, list): dhcpv6_opts.result = 'fake-uuid-2' self.transaction.insert.reset_mock() cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api.lsp_table) fake_lswitch.addvalue.assert_called_once_with( 'ports', fake_lsp.uuid) self.assertEqual(lsp_name, fake_lsp.name) if isinstance(dhcpv4_opts, list): self.assertEqual(dhcpv4_opts, fake_lsp.dhcpv4_options) else: self.assertEqual(['fake-uuid-1'], fake_lsp.dhcpv4_options) if isinstance(dhcpv6_opts, list): self.assertEqual(dhcpv6_opts, fake_lsp.dhcpv6_options) else: self.assertEqual(['fake-uuid-2'], fake_lsp.dhcpv6_options) def test_lswitch_port_add_with_dhcp(self): dhcpv4_opts_cmd = commands.AddDHCPOptionsCommand( self.ovn_api, mock.ANY, port_id=mock.ANY) dhcpv6_opts_cmd = commands.AddDHCPOptionsCommand( self.ovn_api, mock.ANY, port_id=mock.ANY) for dhcpv4_opts in ([], ['fake-uuid-1'], dhcpv4_opts_cmd): for dhcpv6_opts in ([], ['fake-uuid-2'], dhcpv6_opts_cmd): self._test_lswitch_port_add_with_dhcp(dhcpv4_opts, dhcpv6_opts) class TestSetLSwitchPortCommand(TestBaseCommand): def _test_lswitch_port_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.SetLSwitchPortCommand( self.ovn_api, 'fake-lsp', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_port_no_exist_ignore(self): self._test_lswitch_port_update_no_exist(if_exists=True) def test_lswitch_port_no_exist_fail(self): self._test_lswitch_port_update_no_exist(if_exists=False) def test_lswitch_port_update(self): ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} new_ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test-new'} fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': ext_ids}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lsp): cmd = commands.SetLSwitchPortCommand( self.ovn_api, fake_lsp.name, if_exists=True, external_ids=new_ext_ids) cmd.run_idl(self.transaction) self.assertEqual(new_ext_ids, fake_lsp.external_ids) def _test_lswitch_port_update_del_dhcp(self, clear_v4_opts, clear_v6_opts, set_v4_opts=False, set_v6_opts=False): ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} dhcp_options_tbl = self.ovn_api._tables['DHCP_Options'] fake_dhcpv4_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': {'port_id': 'fake-lsp'}}) dhcp_options_tbl.rows[fake_dhcpv4_opts.uuid] = fake_dhcpv4_opts fake_dhcpv6_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': {'port_id': 'fake-lsp'}}) dhcp_options_tbl.rows[fake_dhcpv6_opts.uuid] = fake_dhcpv6_opts fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': 'fake-lsp', 'external_ids': ext_ids, 'dhcpv4_options': [fake_dhcpv4_opts], 'dhcpv6_options': [fake_dhcpv6_opts]}) columns = {} if clear_v4_opts: columns['dhcpv4_options'] = [] elif set_v4_opts: columns['dhcpv4_options'] = [fake_dhcpv4_opts.uuid] if clear_v6_opts: columns['dhcpv6_options'] = [] elif set_v6_opts: columns['dhcpv6_options'] = [fake_dhcpv6_opts.uuid] with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lsp): cmd = commands.SetLSwitchPortCommand( self.ovn_api, fake_lsp.name, if_exists=True, **columns) cmd.run_idl(self.transaction) if clear_v4_opts and clear_v6_opts: fake_dhcpv4_opts.delete.assert_called_once_with() fake_dhcpv6_opts.delete.assert_called_once_with() elif clear_v4_opts: # not clear_v6_opts and set_v6_opts is any fake_dhcpv4_opts.delete.assert_called_once_with() fake_dhcpv6_opts.delete.assert_not_called() elif clear_v6_opts: # not clear_v4_opts and set_v6_opts is any fake_dhcpv4_opts.delete.assert_not_called() fake_dhcpv6_opts.delete.assert_called_once_with() else: # not clear_v4_opts and not clear_v6_opts and # set_v4_opts is any and set_v6_opts is any fake_dhcpv4_opts.delete.assert_not_called() fake_dhcpv6_opts.delete.assert_not_called() def test_lswitch_port_update_del_port_dhcpv4_options(self): self._test_lswitch_port_update_del_dhcp(True, False) def test_lswitch_port_update_del_port_dhcpv6_options(self): self._test_lswitch_port_update_del_dhcp(False, True) def test_lswitch_port_update_del_all_port_dhcp_options(self): self._test_lswitch_port_update_del_dhcp(True, True) def test_lswitch_port_update_del_no_port_dhcp_options(self): self._test_lswitch_port_update_del_dhcp(False, False) def test_lswitch_port_update_set_port_dhcpv4_options(self): self._test_lswitch_port_update_del_dhcp(False, True, set_v4_opts=True) def test_lswitch_port_update_set_port_dhcpv6_options(self): self._test_lswitch_port_update_del_dhcp(True, False, set_v6_opts=True) def test_lswitch_port_update_set_all_port_dhcp_options(self): self._test_lswitch_port_update_del_dhcp(False, False, set_v4_opts=True, set_v6_opts=True) def _test_lswitch_port_update_with_dhcp(self, dhcpv4_opts, dhcpv6_opts): ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': 'fake-lsp', 'external_ids': ext_ids, 'dhcpv4_options': ['fake-v4-subnet-dhcp-opt'], 'dhcpv6_options': ['fake-v6-subnet-dhcp-opt']}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lsp): cmd = commands.SetLSwitchPortCommand( self.ovn_api, fake_lsp.name, if_exists=True, external_ids=ext_ids, dhcpv4_options=dhcpv4_opts, dhcpv6_options=dhcpv6_opts) if not isinstance(dhcpv4_opts, list): dhcpv4_opts.result = 'fake-uuid-1' if not isinstance(dhcpv6_opts, list): dhcpv6_opts.result = 'fake-uuid-2' cmd.run_idl(self.transaction) if isinstance(dhcpv4_opts, list): self.assertEqual(dhcpv4_opts, fake_lsp.dhcpv4_options) else: self.assertEqual(['fake-uuid-1'], fake_lsp.dhcpv4_options) if isinstance(dhcpv6_opts, list): self.assertEqual(dhcpv6_opts, fake_lsp.dhcpv6_options) else: self.assertEqual(['fake-uuid-2'], fake_lsp.dhcpv6_options) def test_lswitch_port_update_with_dhcp(self): v4_dhcp_cmd = commands.AddDHCPOptionsCommand(self.ovn_api, mock.ANY, port_id=mock.ANY) v6_dhcp_cmd = commands.AddDHCPOptionsCommand(self.ovn_api, mock.ANY, port_id=mock.ANY) for dhcpv4_opts in ([], ['fake-v4-subnet-dhcp-opt'], v4_dhcp_cmd): for dhcpv6_opts in ([], ['fake-v6-subnet-dhcp-opt'], v6_dhcp_cmd): self._test_lswitch_port_update_with_dhcp( dhcpv4_opts, dhcpv6_opts) class TestDelLSwitchPortCommand(TestBaseCommand): def _test_lswitch_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=['fake-lsp', idlutils.RowNotFound]): cmd = commands.DelLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lswitch', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_no_exist_ignore(self): self._test_lswitch_no_exist(if_exists=True) def test_lswitch_no_exist_fail(self): self._test_lswitch_no_exist(if_exists=False) def _test_lswitch_port_del_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lswitch', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_port_no_exist_ignore(self): self._test_lswitch_port_del_no_exist(if_exists=True) def test_lswitch_port_no_exist_fail(self): self._test_lswitch_port_del_no_exist(if_exists=False) def test_lswitch_port_del(self): fake_lsp = mock.MagicMock() fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ports': [fake_lsp]}) self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ fake_lsp with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lsp, fake_lswitch]): cmd = commands.DelLSwitchPortCommand( self.ovn_api, fake_lsp.name, fake_lswitch.name, if_exists=True) cmd.run_idl(self.transaction) fake_lswitch.delvalue.assert_called_once_with('ports', fake_lsp) fake_lsp.delete.assert_called_once_with() def _test_lswitch_port_del_delete_dhcp_opt(self, dhcpv4_opt_ext_ids, dhcpv6_opt_ext_ids): ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} fake_dhcpv4_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': dhcpv4_opt_ext_ids}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcpv4_options.uuid] = \ fake_dhcpv4_options fake_dhcpv6_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': dhcpv6_opt_ext_ids}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcpv6_options.uuid] = \ fake_dhcpv6_options fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': 'lsp', 'external_ids': ext_ids, 'dhcpv4_options': [fake_dhcpv4_options], 'dhcpv6_options': [fake_dhcpv6_options]}) self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ fake_lsp fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ports': [fake_lsp]}) with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lsp, fake_lswitch]): cmd = commands.DelLSwitchPortCommand( self.ovn_api, fake_lsp.name, fake_lswitch.name, if_exists=True) cmd.run_idl(self.transaction) fake_lswitch.delvalue.assert_called_once_with('ports', fake_lsp) fake_lsp.delete.assert_called_once_with() if 'port_id' in dhcpv4_opt_ext_ids: fake_dhcpv4_options.delete.assert_called_once_with() else: fake_dhcpv4_options.delete.assert_not_called() if 'port_id' in dhcpv6_opt_ext_ids: fake_dhcpv6_options.delete.assert_called_once_with() else: fake_dhcpv6_options.delete.assert_not_called() def test_lswitch_port_del_delete_dhcp_opt(self): for v4_ext_ids in ({'subnet_id': 'fake-ls0'}, {'subnet_id': 'fake-ls0', 'port_id': 'lsp'}): for v6_ext_ids in ({'subnet_id': 'fake-ls1'}, {'subnet_id': 'fake-ls1', 'port_id': 'lsp'}): self._test_lswitch_port_del_delete_dhcp_opt( v4_ext_ids, v6_ext_ids) class TestAddLRouterCommand(TestBaseCommand): def test_lrouter_exists(self): with mock.patch.object(idlutils, 'row_by_value', return_value=mock.ANY): cmd = commands.AddLRouterCommand( self.ovn_api, 'fake-lrouter', may_exist=True, a='1', b='2') cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() def test_lrouter_add_exists(self): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api._tables['Logical_Router'].rows[fake_lrouter.uuid] = \ fake_lrouter self.transaction.insert.return_value = fake_lrouter cmd = commands.AddLRouterCommand( self.ovn_api, fake_lrouter.name, may_exist=False) cmd.run_idl(self.transaction) # NOTE(rtheis): Mocking the transaction allows this insert # to succeed when it normally would fail due the duplicate name. self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Router']) def _test_lrouter_add(self, may_exist=True): with mock.patch.object(idlutils, 'row_by_value', return_value=None): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.transaction.insert.return_value = fake_lrouter cmd = commands.AddLRouterCommand( self.ovn_api, 'fake-lrouter', may_exist=may_exist, a='1', b='2') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Router']) self.assertEqual('fake-lrouter', fake_lrouter.name) self.assertEqual('1', fake_lrouter.a) self.assertEqual('2', fake_lrouter.b) def test_lrouter_add_may_exist(self): self._test_lrouter_add(may_exist=True) def test_lrouter_add_ignore_exists(self): self._test_lrouter_add(may_exist=False) class TestUpdateLRouterCommand(TestBaseCommand): def _test_lrouter_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.UpdateLRouterCommand( self.ovn_api, 'fake-lrouter', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_no_exist_ignore(self): self._test_lrouter_update_no_exist(if_exists=True) def test_lrouter_no_exist_fail(self): self._test_lrouter_update_no_exist(if_exists=False) def test_lrouter_update(self): ext_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'richard'} new_ext_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'richard-new'} fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': ext_ids}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): cmd = commands.UpdateLRouterCommand( self.ovn_api, fake_lrouter.name, if_exists=True, external_ids=new_ext_ids) cmd.run_idl(self.transaction) self.assertEqual(new_ext_ids, fake_lrouter.external_ids) class TestDelLRouterCommand(TestBaseCommand): def _test_lrouter_del_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelLRouterCommand( self.ovn_api, 'fake-lrouter', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_no_exist_ignore(self): self._test_lrouter_del_no_exist(if_exists=True) def test_lrouter_no_exist_fail(self): self._test_lrouter_del_no_exist(if_exists=False) def test_lrouter_del(self): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api._tables['Logical_Router'].rows[fake_lrouter.uuid] = \ fake_lrouter with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): cmd = commands.DelLRouterCommand( self.ovn_api, fake_lrouter.name, if_exists=True) cmd.run_idl(self.transaction) fake_lrouter.delete.assert_called_once_with() class TestAddLRouterPortCommand(TestBaseCommand): def test_lrouter_not_found(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.AddLRouterPortCommand( self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=False) self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) self.transaction.insert.assert_not_called() def test_lrouter_port_exists(self): with mock.patch.object(idlutils, 'row_by_value', return_value=mock.ANY): cmd = commands.AddLRouterPortCommand( self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=False) self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) self.transaction.insert.assert_not_called() def test_lrouter_port_may_exist(self): with mock.patch.object(idlutils, 'row_by_value', return_value=mock.ANY): cmd = commands.AddLRouterPortCommand( self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=True) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() def test_lrouter_port_add(self): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lrouter, idlutils.RowNotFound]): fake_lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'foo': None}) self.transaction.insert.return_value = fake_lrp cmd = commands.AddLRouterPortCommand( self.ovn_api, 'fake-lrp', fake_lrouter.name, may_exist=False, foo='bar') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Router_Port']) self.assertEqual('fake-lrp', fake_lrp.name) fake_lrouter.addvalue.assert_called_once_with('ports', fake_lrp) self.assertEqual('bar', fake_lrp.foo) class TestUpdateLRouterPortCommand(TestBaseCommand): def _test_lrouter_port_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.UpdateLRouterPortCommand( self.ovn_api, 'fake-lrp', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_port_no_exist_ignore(self): self._test_lrouter_port_update_no_exist(if_exists=True) def test_lrouter_port_no_exist_fail(self): self._test_lrouter_port_update_no_exist(if_exists=False) def test_lrouter_port_update(self): old_networks = [] new_networks = ['10.1.0.0/24'] fake_lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'networks': old_networks}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrp): cmd = commands.UpdateLRouterPortCommand( self.ovn_api, fake_lrp.name, if_exists=True, networks=new_networks) cmd.run_idl(self.transaction) self.assertEqual(new_networks, fake_lrp.networks) class TestDelLRouterPortCommand(TestBaseCommand): def _test_lrouter_port_del_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelLRouterPortCommand( self.ovn_api, 'fake-lrp', 'fake-lrouter', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_port_no_exist_ignore(self): self._test_lrouter_port_del_no_exist(if_exists=True) def test_lrouter_port_no_exist_fail(self): self._test_lrouter_port_del_no_exist(if_exists=False) def test_lrouter_no_exist(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=[mock.ANY, idlutils.RowNotFound]): cmd = commands.DelLRouterPortCommand( self.ovn_api, 'fake-lrp', 'fake-lrouter', if_exists=True) self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_port_del(self): fake_lrp = mock.MagicMock() fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ports': [fake_lrp]}) self.ovn_api._tables['Logical_Router_Port'].rows[fake_lrp.uuid] = \ fake_lrp with mock.patch.object(idlutils, 'row_by_value', side_effect=[fake_lrp, fake_lrouter]): cmd = commands.DelLRouterPortCommand( self.ovn_api, fake_lrp.name, fake_lrouter.name, if_exists=True) cmd.run_idl(self.transaction) fake_lrouter.delvalue.assert_called_once_with('ports', fake_lrp) class TestSetLRouterPortInLSwitchPortCommand(TestBaseCommand): def test_lswitch_port_no_exist_fail(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.SetLRouterPortInLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lrp', False, if_exists=False) self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_port_no_exist_do_not_fail(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.SetLRouterPortInLSwitchPortCommand( self.ovn_api, 'fake-lsp', 'fake-lrp', False, if_exists=True) cmd.run_idl(self.transaction) def test_lswitch_port_router_update(self): lrp_name = 'fake-lrp' fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lsp): cmd = commands.SetLRouterPortInLSwitchPortCommand( self.ovn_api, fake_lsp.name, lrp_name, True, if_exists=True) cmd.run_idl(self.transaction) self.assertEqual({'router-port': lrp_name, 'nat-addresses': 'router'}, fake_lsp.options) self.assertEqual('router', fake_lsp.type) self.assertEqual('router', fake_lsp.addresses) class TestAddACLCommand(TestBaseCommand): def test_lswitch_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.AddACLCommand( self.ovn_api, 'fake-lswitch', 'fake-lsp') self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_acl_add(self): fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): fake_acl = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.transaction.insert.return_value = fake_acl cmd = commands.AddACLCommand( self.ovn_api, fake_lswitch.name, 'fake-lsp', match='*') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['ACL']) fake_lswitch.addvalue.assert_called_once_with( 'acls', fake_acl.uuid) self.assertEqual('*', fake_acl.match) class TestDelACLCommand(TestBaseCommand): def _test_lswitch_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelACLCommand( self.ovn_api, 'fake-lswitch', 'fake-lsp', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lswitch_no_exist_ignore(self): self._test_lswitch_no_exist(if_exists=True) def test_lswitch_no_exist_fail(self): self._test_lswitch_no_exist(if_exists=False) def test_acl_del(self): fake_lsp_name = 'fake-lsp' fake_acl_del = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': {'neutron:lport': fake_lsp_name}}) fake_acl_save = mock.ANY fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'acls': [fake_acl_del, fake_acl_save]}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): cmd = commands.DelACLCommand( self.ovn_api, fake_lswitch.name, fake_lsp_name, if_exists=True) cmd.run_idl(self.transaction) fake_lswitch.delvalue.assert_called_once_with('acls', mock.ANY) class TestUpdateACLsCommand(TestBaseCommand): def test_lswitch_no_exist(self): fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api.get_acls_for_lswitches.return_value = ({}, {}, {}) cmd = commands.UpdateACLsCommand( self.ovn_api, [fake_lswitch.name], port_list=[], acl_new_values_dict={}, need_compare=True) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() fake_lswitch.addvalue.assert_not_called() fake_lswitch.delvalue.assert_not_called() def _test_acl_update_no_acls(self, need_compare): fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api.get_acls_for_lswitches.return_value = ( {}, {}, {fake_lswitch.name: fake_lswitch}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): cmd = commands.UpdateACLsCommand( self.ovn_api, [fake_lswitch.name], port_list=[], acl_new_values_dict={}, need_compare=need_compare) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() fake_lswitch.addvalue.assert_not_called() fake_lswitch.delvalue.assert_not_called() def test_acl_update_compare_no_acls(self): self._test_acl_update_no_acls(need_compare=True) def test_acl_update_no_compare_no_acls(self): self._test_acl_update_no_acls(need_compare=False) def test_acl_update_compare_acls(self): fake_sg_rule = \ fakes.FakeSecurityGroupRule.create_one_security_group_rule().info() fake_port = fakes.FakePort.create_one_port().info() fake_add_acl = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'match': 'add_acl'}) fake_del_acl = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'match': 'del_acl'}) fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': ovn_utils.ovn_name(fake_port['network_id']), 'acls': []}) add_acl = ovn_acl.add_sg_rule_acl_for_port( fake_port, fake_sg_rule, 'add_acl') self.ovn_api.get_acls_for_lswitches.return_value = ( {fake_port['id']: [fake_del_acl.match]}, {fake_del_acl.match: fake_del_acl}, {fake_lswitch.name.replace('neutron-', ''): fake_lswitch}) cmd = commands.UpdateACLsCommand( self.ovn_api, [fake_port['network_id']], [fake_port], {fake_port['id']: [add_acl]}, need_compare=True) self.transaction.insert.return_value = fake_add_acl cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['ACL']) fake_lswitch.addvalue.assert_called_with('acls', fake_add_acl.uuid) def test_acl_update_no_compare_add_acls(self): fake_sg_rule = \ fakes.FakeSecurityGroupRule.create_one_security_group_rule().info() fake_port = fakes.FakePort.create_one_port().info() fake_acl = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'match': '*'}) fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': ovn_utils.ovn_name(fake_port['network_id'])}) add_acl = ovn_acl.add_sg_rule_acl_for_port( fake_port, fake_sg_rule, '*') with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): self.transaction.insert.return_value = fake_acl cmd = commands.UpdateACLsCommand( self.ovn_api, [fake_port['network_id']], [fake_port], {fake_port['id']: add_acl}, need_compare=False, is_add_acl=True) cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['ACL']) fake_lswitch.addvalue.assert_called_once_with( 'acls', fake_acl.uuid) def test_acl_update_no_compare_del_acls(self): fake_sg_rule = \ fakes.FakeSecurityGroupRule.create_one_security_group_rule().info() fake_port = fakes.FakePort.create_one_port().info() fake_acl = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'match': '*', 'external_ids': {'neutron:lport': fake_port['id'], 'neutron:security_group_rule_id': fake_sg_rule['id']}}) fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'name': ovn_utils.ovn_name(fake_port['network_id']), 'acls': [fake_acl]}) del_acl = ovn_acl.add_sg_rule_acl_for_port( fake_port, fake_sg_rule, '*') with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lswitch): cmd = commands.UpdateACLsCommand( self.ovn_api, [fake_port['network_id']], [fake_port], {fake_port['id']: del_acl}, need_compare=False, is_add_acl=False) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() fake_lswitch.delvalue.assert_called_with('acls', mock.ANY) class TestAddStaticRouteCommand(TestBaseCommand): def test_lrouter_not_found(self): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.AddStaticRouteCommand(self.ovn_api, 'fake-lrouter') self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) self.transaction.insert.assert_not_called() def test_static_route_add(self): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): fake_static_route = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.transaction.insert.return_value = fake_static_route cmd = commands.AddStaticRouteCommand( self.ovn_api, fake_lrouter.name, nexthop='40.0.0.100', ip_prefix='30.0.0.0/24') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Logical_Router_Static_Route']) self.assertEqual('40.0.0.100', fake_static_route.nexthop) self.assertEqual('30.0.0.0/24', fake_static_route.ip_prefix) fake_lrouter.addvalue.assert_called_once_with( 'static_routes', fake_static_route.uuid) class TestDelStaticRouteCommand(TestBaseCommand): def _test_lrouter_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelStaticRouteCommand( self.ovn_api, 'fake-lrouter', '30.0.0.0/24', '40.0.0.100', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_lrouter_no_exist_ignore(self): self._test_lrouter_no_exist(if_exists=True) def test_lrouter_no_exist_fail(self): self._test_lrouter_no_exist(if_exists=False) def test_static_route_del(self): fake_static_route = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ip_prefix': '50.0.0.0/24', 'nexthop': '40.0.0.101'}) fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'static_routes': [fake_static_route]}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): cmd = commands.DelStaticRouteCommand( self.ovn_api, fake_lrouter.name, fake_static_route.ip_prefix, fake_static_route.nexthop, if_exists=True) cmd.run_idl(self.transaction) fake_lrouter.delvalue.assert_called_once_with( 'static_routes', mock.ANY) def test_static_route_del_not_found(self): fake_static_route1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ip_prefix': '50.0.0.0/24', 'nexthop': '40.0.0.101'}) fake_static_route2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'ip_prefix': '60.0.0.0/24', 'nexthop': '70.0.0.101'}) fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'static_routes': [fake_static_route2]}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): cmd = commands.DelStaticRouteCommand( self.ovn_api, fake_lrouter.name, fake_static_route1.ip_prefix, fake_static_route1.nexthop, if_exists=True) cmd.run_idl(self.transaction) fake_lrouter.delvalue.assert_not_called() self.assertEqual([mock.ANY], fake_lrouter.static_routes) class TestAddAddrSetCommand(TestBaseCommand): def test_addrset_exists(self): with mock.patch.object(idlutils, 'row_by_value', return_value=mock.ANY): cmd = commands.AddAddrSetCommand( self.ovn_api, 'fake-addrset', may_exist=True) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() def test_addrset_add_exists(self): fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api._tables['Address_Set'].rows[fake_addrset.uuid] = \ fake_addrset self.transaction.insert.return_value = fake_addrset cmd = commands.AddAddrSetCommand( self.ovn_api, fake_addrset.name, may_exist=False) cmd.run_idl(self.transaction) # NOTE(rtheis): Mocking the transaction allows this insert # to succeed when it normally would fail due the duplicate name. self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Address_Set']) def _test_addrset_add(self, may_exist=True): with mock.patch.object(idlutils, 'row_by_value', return_value=None): fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'foo': ''}) self.transaction.insert.return_value = fake_addrset cmd = commands.AddAddrSetCommand( self.ovn_api, 'fake-addrset', may_exist=may_exist, foo='bar') cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['Address_Set']) self.assertEqual('fake-addrset', fake_addrset.name) self.assertEqual('bar', fake_addrset.foo) def test_addrset_add_may_exist(self): self._test_addrset_add(may_exist=True) def test_addrset_add_ignore_exists(self): self._test_addrset_add(may_exist=False) class TestDelAddrSetCommand(TestBaseCommand): def _test_addrset_del_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.DelAddrSetCommand( self.ovn_api, 'fake-addrset', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_addrset_no_exist_ignore(self): self._test_addrset_del_no_exist(if_exists=True) def test_addrset_no_exist_fail(self): self._test_addrset_del_no_exist(if_exists=False) def test_addrset_del(self): fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row() self.ovn_api._tables['Address_Set'].rows[fake_addrset.uuid] = \ fake_addrset with mock.patch.object(idlutils, 'row_by_value', return_value=fake_addrset): cmd = commands.DelAddrSetCommand( self.ovn_api, fake_addrset.name, if_exists=True) cmd.run_idl(self.transaction) fake_addrset.delete.assert_called_once_with() class TestUpdateAddrSetCommand(TestBaseCommand): def _test_addrset_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.UpdateAddrSetCommand( self.ovn_api, 'fake-addrset', addrs_add=[], addrs_remove=[], if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_addrset_no_exist_ignore(self): self._test_addrset_update_no_exist(if_exists=True) def test_addrset_no_exist_fail(self): self._test_addrset_update_no_exist(if_exists=False) def _test_addrset_update(self, addrs_add=None, addrs_del=None): save_address = '10.0.0.1' initial_addresses = [save_address] final_addresses = [save_address] expected_addvalue_calls = [] expected_delvalue_calls = [] if addrs_add: for addr_add in addrs_add: final_addresses.append(addr_add) expected_addvalue_calls.append( mock.call('addresses', addr_add)) if addrs_del: for addr_del in addrs_del: initial_addresses.append(addr_del) expected_delvalue_calls.append( mock.call('addresses', addr_del)) fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'addresses': initial_addresses}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_addrset): cmd = commands.UpdateAddrSetCommand( self.ovn_api, fake_addrset.name, addrs_add=addrs_add, addrs_remove=addrs_del, if_exists=True) cmd.run_idl(self.transaction) fake_addrset.addvalue.assert_has_calls(expected_addvalue_calls) fake_addrset.delvalue.assert_has_calls(expected_delvalue_calls) def test_addrset_update_add(self): self._test_addrset_update(addrs_add=['10.0.0.4']) def test_addrset_update_del(self): self._test_addrset_update(addrs_del=['10.0.0.2']) class TestUpdateAddrSetExtIdsCommand(TestBaseCommand): def setUp(self): super(TestUpdateAddrSetExtIdsCommand, self).setUp() self.ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default'} def _test_addrset_extids_update_no_exist(self, if_exists=True): with mock.patch.object(idlutils, 'row_by_value', side_effect=idlutils.RowNotFound): cmd = commands.UpdateAddrSetExtIdsCommand( self.ovn_api, 'fake-addrset', self.ext_ids, if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_addrset_no_exist_ignore(self): self._test_addrset_extids_update_no_exist(if_exists=True) def test_addrset_no_exist_fail(self): self._test_addrset_extids_update_no_exist(if_exists=False) def test_addrset_extids_update(self): new_ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default-new'} fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': self.ext_ids}) with mock.patch.object(idlutils, 'row_by_value', return_value=fake_addrset): cmd = commands.UpdateAddrSetExtIdsCommand( self.ovn_api, fake_addrset.name, new_ext_ids, if_exists=True) cmd.run_idl(self.transaction) self.assertEqual(new_ext_ids, fake_addrset.external_ids) class TestAddDHCPOptionsCommand(TestBaseCommand): def test_dhcp_options_exists(self): fake_ext_ids = {'subnet_id': 'fake-subnet-id', 'port_id': 'fake-port-id'} fake_dhcp_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': fake_ext_ids}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options.uuid] = \ fake_dhcp_options cmd = commands.AddDHCPOptionsCommand( self.ovn_api, fake_ext_ids['subnet_id'], fake_ext_ids['port_id'], may_exist=True, external_ids=fake_ext_ids) cmd.run_idl(self.transaction) self.transaction.insert.assert_not_called() self.assertEqual(fake_ext_ids, fake_dhcp_options.external_ids) def _test_dhcp_options_add(self, may_exist=True): fake_subnet_id = 'fake-subnet-id-' + str(may_exist) fake_port_id = 'fake-port-id-' + str(may_exist) fake_ext_ids1 = {'subnet_id': fake_subnet_id, 'port_id': fake_port_id} fake_dhcp_options1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': fake_ext_ids1}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options1.uuid] = \ fake_dhcp_options1 fake_ext_ids2 = {'subnet_id': fake_subnet_id} fake_dhcp_options2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': fake_ext_ids2}) fake_dhcp_options3 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': {'subnet_id': 'nomatch'}}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options3.uuid] = \ fake_dhcp_options3 self.transaction.insert.return_value = fake_dhcp_options2 cmd = commands.AddDHCPOptionsCommand( self.ovn_api, fake_ext_ids2['subnet_id'], may_exist=may_exist, external_ids=fake_ext_ids2) cmd.run_idl(self.transaction) self.transaction.insert.assert_called_once_with( self.ovn_api._tables['DHCP_Options']) self.assertEqual(fake_ext_ids2, fake_dhcp_options2.external_ids) def test_dhcp_options_add_may_exist(self): self._test_dhcp_options_add(may_exist=True) def test_dhcp_options_add_ignore_exists(self): self._test_dhcp_options_add(may_exist=False) def _test_dhcp_options_update_result(self, new_insert=False): fake_ext_ids = {'subnet_id': 'fake_subnet', 'port_id': 'fake_port'} fake_dhcp_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': fake_ext_ids}) if new_insert: self.transaction.insert.return_value = fake_dhcp_opts self.transaction.get_insert_uuid = mock.Mock( return_value='fake-uuid') else: self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_opts.uuid] = \ fake_dhcp_opts self.transaction.get_insert_uuid = mock.Mock( return_value=None) cmd = commands.AddDHCPOptionsCommand( self.ovn_api, fake_ext_ids['subnet_id'], port_id=fake_ext_ids['port_id'], may_exist=True, external_ids=fake_ext_ids) cmd.run_idl(self.transaction) cmd.post_commit(self.transaction) if new_insert: self.assertEqual('fake-uuid', cmd.result) else: self.assertEqual(fake_dhcp_opts.uuid, cmd.result) def test_dhcp_options_update_result_with_exist_row(self): self._test_dhcp_options_update_result(new_insert=False) def test_dhcp_options_update_result_with_new_row(self): self._test_dhcp_options_update_result(new_insert=True) class TestDelDHCPOptionsCommand(TestBaseCommand): def _test_dhcp_options_del_no_exist(self, if_exists=True): cmd = commands.DelDHCPOptionsCommand( self.ovn_api, 'fake-dhcp-options', if_exists=if_exists) if if_exists: cmd.run_idl(self.transaction) else: self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) def test_dhcp_options_no_exist_ignore(self): self._test_dhcp_options_del_no_exist(if_exists=True) def test_dhcp_options_no_exist_fail(self): self._test_dhcp_options_del_no_exist(if_exists=False) def test_dhcp_options_del(self): fake_dhcp_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ids': {'subnet_id': 'fake-subnet-id'}}) self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options.uuid] = \ fake_dhcp_options cmd = commands.DelDHCPOptionsCommand( self.ovn_api, fake_dhcp_options.uuid, if_exists=True) cmd.run_idl(self.transaction) fake_dhcp_options.delete.assert_called_once_with() class TestSetNATRuleInLRouterCommand(TestBaseCommand): def test_set_nat_rule(self): fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() with mock.patch.object(idlutils, 'row_by_value', return_value=fake_lrouter): fake_nat_rule_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ip': '192.168.1.10', 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat'}) fake_nat_rule_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs={'external_ip': '192.168.1.8', 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat'}) fake_lrouter.nat = [fake_nat_rule_1, fake_nat_rule_2] self.ovn_api._tables['NAT'].rows[fake_nat_rule_1.uuid] = \ fake_nat_rule_1 self.ovn_api._tables['NAT'].rows[fake_nat_rule_2.uuid] = \ fake_nat_rule_2 cmd = commands.SetNATRuleInLRouterCommand( self.ovn_api, fake_lrouter.name, fake_nat_rule_1.uuid, logical_ip='10.0.0.10') cmd.run_idl(self.transaction) self.assertEqual('10.0.0.10', fake_nat_rule_1.logical_ip) self.assertEqual('10.0.0.5', fake_nat_rule_2.logical_ip) networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/__init__.py0000666000175100017510000000000013245511145025137 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/schemas/0000775000175100017510000000000013245511554024465 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/schemas/ovn-nb.ovsschema0000666000175100017510000003367513245511145027612 0ustar zuulzuul00000000000000{ "name": "OVN_Northbound", "version": "5.5.0", "cksum": "2099428463 14236", "tables": { "NB_Global": { "columns": { "nb_cfg": {"type": {"key": "integer"}}, "sb_cfg": {"type": {"key": "integer"}}, "hv_cfg": {"type": {"key": "integer"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "connections": { "type": {"key": {"type": "uuid", "refTable": "Connection"}, "min": 0, "max": "unlimited"}}, "ssl": { "type": {"key": {"type": "uuid", "refTable": "SSL"}, "min": 0, "max": 1}}}, "maxRows": 1, "isRoot": true}, "Logical_Switch": { "columns": { "name": {"type": "string"}, "ports": {"type": {"key": {"type": "uuid", "refTable": "Logical_Switch_Port", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "acls": {"type": {"key": {"type": "uuid", "refTable": "ACL", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "qos_rules": {"type": {"key": {"type": "uuid", "refTable": "QoS", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "load_balancer": {"type": {"key": {"type": "uuid", "refTable": "Load_Balancer", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "other_config": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true}, "Logical_Switch_Port": { "columns": { "name": {"type": "string"}, "type": {"type": "string"}, "options": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "parent_name": {"type": {"key": "string", "min": 0, "max": 1}}, "tag_request": { "type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 4095}, "min": 0, "max": 1}}, "tag": { "type": {"key": {"type": "integer", "minInteger": 1, "maxInteger": 4095}, "min": 0, "max": 1}}, "addresses": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "dynamic_addresses": {"type": {"key": "string", "min": 0, "max": 1}}, "port_security": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "up": {"type": {"key": "boolean", "min": 0, "max": 1}}, "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, "dhcpv4_options": {"type": {"key": {"type": "uuid", "refTable": "DHCP_Options", "refType": "weak"}, "min": 0, "max": 1}}, "dhcpv6_options": {"type": {"key": {"type": "uuid", "refTable": "DHCP_Options", "refType": "weak"}, "min": 0, "max": 1}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["name"]], "isRoot": false}, "Address_Set": { "columns": { "name": {"type": "string"}, "addresses": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["name"]], "isRoot": true}, "Load_Balancer": { "columns": { "name": {"type": "string"}, "vips": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "protocol": { "type": {"key": {"type": "string", "enum": ["set", ["tcp", "udp"]]}, "min": 0, "max": 1}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true}, "ACL": { "columns": { "priority": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 32767}}}, "direction": {"type": {"key": {"type": "string", "enum": ["set", ["from-lport", "to-lport"]]}}}, "match": {"type": "string"}, "action": {"type": {"key": {"type": "string", "enum": ["set", ["allow", "allow-related", "drop", "reject"]]}}}, "log": {"type": "boolean"}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": false}, "QoS": { "columns": { "priority": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 32767}}}, "direction": {"type": {"key": {"type": "string", "enum": ["set", ["from-lport", "to-lport"]]}}}, "match": {"type": "string"}, "action": {"type": {"key": {"type": "string", "enum": ["set", ["dscp"]]}, "value": {"type": "integer", "minInteger": 0, "maxInteger": 63}}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": false}, "Logical_Router": { "columns": { "name": {"type": "string"}, "ports": {"type": {"key": {"type": "uuid", "refTable": "Logical_Router_Port", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "static_routes": {"type": {"key": {"type": "uuid", "refTable": "Logical_Router_Static_Route", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, "nat": {"type": {"key": {"type": "uuid", "refTable": "NAT", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "load_balancer": {"type": {"key": {"type": "uuid", "refTable": "Load_Balancer", "refType": "strong"}, "min": 0, "max": "unlimited"}}, "options": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true}, "Logical_Router_Port": { "columns": { "name": {"type": "string"}, "options": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "networks": {"type": {"key": "string", "min": 1, "max": "unlimited"}}, "mac": {"type": "string"}, "peer": {"type": {"key": "string", "min": 0, "max": 1}}, "enabled": {"type": {"key": "boolean", "min": 0, "max": 1}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["name"]], "isRoot": false}, "Logical_Router_Static_Route": { "columns": { "ip_prefix": {"type": "string"}, "policy": {"type": {"key": {"type": "string", "enum": ["set", ["src-ip", "dst-ip"]]}, "min": 0, "max": 1}}, "nexthop": {"type": "string"}, "output_port": {"type": {"key": "string", "min": 0, "max": 1}}}, "isRoot": false}, "NAT": { "columns": { "external_ip": {"type": "string"}, "external_mac": {"type": {"key": "string", "min": 0, "max": 1}}, "logical_ip": {"type": "string"}, "logical_port": {"type": {"key": "string", "min": 0, "max": 1}}, "type": {"type": {"key": {"type": "string", "enum": ["set", ["dnat", "snat", "dnat_and_snat" ]]}}}}, "isRoot": false}, "DHCP_Options": { "columns": { "cidr": {"type": "string"}, "options": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true}, "Connection": { "columns": { "target": {"type": "string"}, "max_backoff": {"type": {"key": {"type": "integer", "minInteger": 1000}, "min": 0, "max": 1}}, "inactivity_probe": {"type": {"key": "integer", "min": 0, "max": 1}}, "other_config": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "is_connected": {"type": "boolean", "ephemeral": true}, "status": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}, "ephemeral": true}}, "indexes": [["target"]]}, "SSL": { "columns": { "private_key": {"type": "string"}, "certificate": {"type": "string"}, "ca_cert": {"type": "string"}, "bootstrap_ca_cert": {"type": "boolean"}, "external_ids": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "maxRows": 1}}} networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/schemas/ovn-sb.ovsschema0000666000175100017510000002331113245511145027601 0ustar zuulzuul00000000000000{ "name": "OVN_Southbound", "version": "1.10.0", "cksum": "860871483 9898", "tables": { "SB_Global": { "columns": { "nb_cfg": {"type": {"key": "integer"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "connections": { "type": {"key": {"type": "uuid", "refTable": "Connection"}, "min": 0, "max": "unlimited"}}, "ssl": { "type": {"key": {"type": "uuid", "refTable": "SSL"}, "min": 0, "max": 1}}}, "maxRows": 1, "isRoot": true}, "Chassis": { "columns": { "name": {"type": "string"}, "hostname": {"type": "string"}, "encaps": {"type": {"key": {"type": "uuid", "refTable": "Encap"}, "min": 1, "max": "unlimited"}}, "vtep_logical_switches" : {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "nb_cfg": {"type": {"key": "integer"}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true, "indexes": [["name"]]}, "Encap": { "columns": { "type": {"type": {"key": { "type": "string", "enum": ["set", ["geneve", "stt", "vxlan"]]}}}, "options": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "ip": {"type": "string"}}}, "Address_Set": { "columns": { "name": {"type": "string"}, "addresses": {"type": {"key": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["name"]], "isRoot": true}, "Logical_Flow": { "columns": { "logical_datapath": {"type": {"key": {"type": "uuid", "refTable": "Datapath_Binding"}}}, "pipeline": {"type": {"key": {"type": "string", "enum": ["set", ["ingress", "egress"]]}}}, "table_id": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 15}}}, "priority": {"type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 65535}}}, "match": {"type": "string"}, "actions": {"type": "string"}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "isRoot": true}, "Multicast_Group": { "columns": { "datapath": {"type": {"key": {"type": "uuid", "refTable": "Datapath_Binding"}}}, "name": {"type": "string"}, "tunnel_key": { "type": {"key": {"type": "integer", "minInteger": 32768, "maxInteger": 65535}}}, "ports": {"type": {"key": {"type": "uuid", "refTable": "Port_Binding", "refType": "weak"}, "min": 1, "max": "unlimited"}}}, "indexes": [["datapath", "tunnel_key"], ["datapath", "name"]], "isRoot": true}, "Datapath_Binding": { "columns": { "tunnel_key": { "type": {"key": {"type": "integer", "minInteger": 1, "maxInteger": 16777215}}}, "external_ids": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["tunnel_key"]], "isRoot": true}, "Port_Binding": { "columns": { "logical_port": {"type": "string"}, "type": {"type": "string"}, "options": { "type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "datapath": {"type": {"key": {"type": "uuid", "refTable": "Datapath_Binding"}}}, "tunnel_key": { "type": {"key": {"type": "integer", "minInteger": 1, "maxInteger": 32767}}}, "parent_port": {"type": {"key": "string", "min": 0, "max": 1}}, "tag": { "type": {"key": {"type": "integer", "minInteger": 1, "maxInteger": 4095}, "min": 0, "max": 1}}, "chassis": {"type": {"key": {"type": "uuid", "refTable": "Chassis", "refType": "weak"}, "min": 0, "max": 1}}, "mac": {"type": {"key": "string", "min": 0, "max": "unlimited"}}, "nat_addresses": {"type": {"key": "string", "min": 0, "max": "unlimited"}}}, "indexes": [["datapath", "tunnel_key"], ["logical_port"]], "isRoot": true}, "MAC_Binding": { "columns": { "logical_port": {"type": "string"}, "ip": {"type": "string"}, "mac": {"type": "string"}, "datapath": {"type": {"key": {"type": "uuid", "refTable": "Datapath_Binding"}}}}, "indexes": [["logical_port", "ip"]], "isRoot": true}, "DHCP_Options": { "columns": { "name": {"type": "string"}, "code": { "type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 254}}}, "type": { "type": {"key": { "type": "string", "enum": ["set", ["bool", "uint8", "uint16", "uint32", "ipv4", "static_routes", "str"]]}}}}, "isRoot": true}, "DHCPv6_Options": { "columns": { "name": {"type": "string"}, "code": { "type": {"key": {"type": "integer", "minInteger": 0, "maxInteger": 254}}}, "type": { "type": {"key": { "type": "string", "enum": ["set", ["ipv6", "str", "mac"]]}}}}, "isRoot": true}, "Connection": { "columns": { "target": {"type": "string"}, "max_backoff": {"type": {"key": {"type": "integer", "minInteger": 1000}, "min": 0, "max": 1}}, "inactivity_probe": {"type": {"key": "integer", "min": 0, "max": 1}}, "read_only": {"type": "boolean"}, "other_config": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "external_ids": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}, "is_connected": {"type": "boolean", "ephemeral": true}, "status": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}, "ephemeral": true}}, "indexes": [["target"]]}, "SSL": { "columns": { "private_key": {"type": "string"}, "certificate": {"type": "string"}, "ca_cert": {"type": "string"}, "bootstrap_ca_cert": {"type": "boolean"}, "external_ids": {"type": {"key": "string", "value": "string", "min": 0, "max": "unlimited"}}}, "maxRows": 1}}} networking-ovn-4.0.0/networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py0000666000175100017510000011041513245511145027126 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import mock from networking_ovn.common import config from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils from networking_ovn.ovsdb import impl_idl_ovn from networking_ovn.tests import base from networking_ovn.tests.unit import fakes class TestDBImplIdlOvn(base.TestCase): def _load_ovsdb_fake_rows(self, table, fake_attrs): for fake_attr in fake_attrs: fake_row = fakes.FakeOvsdbRow.create_one_ovsdb_row( attrs=fake_attr) # Pre-populate ovs idl "._data" fake_data = copy.deepcopy(fake_attr) try: del fake_data["unit_test_id"] except KeyError: pass setattr(fake_row, "_data", fake_data) table.rows[fake_row.uuid] = fake_row def _find_ovsdb_fake_row(self, table, key, value): for fake_row in table.rows.values(): if getattr(fake_row, key) == value: return fake_row return None def _construct_ovsdb_references(self, fake_associations, parent_table, child_table, parent_key, child_key, reference_column_name): for p_name, c_names in fake_associations.items(): p_row = self._find_ovsdb_fake_row(parent_table, parent_key, p_name) c_uuids = [] for c_name in c_names: c_row = self._find_ovsdb_fake_row(child_table, child_key, c_name) if not c_row: continue # Fake IDL processing (uuid -> row) c_uuids.append(c_row) setattr(p_row, reference_column_name, c_uuids) class TestNBImplIdlOvn(TestDBImplIdlOvn): fake_set = { 'lswitches': [ {'name': utils.ovn_name('ls-id-1'), 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'ls-name-1'}}, {'name': utils.ovn_name('ls-id-2'), 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'ls-name-2'}}, {'name': utils.ovn_name('ls-id-3'), 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'ls-name-3'}}, {'name': 'ls-id-4', 'external_ids': {'not-neutron:network_name': 'ls-name-4'}}, {'name': utils.ovn_name('ls-id-5'), 'external_ids': {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'ls-name-5'}}], 'lswitch_ports': [ {'name': 'lsp-id-11', 'addresses': ['10.0.1.1'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-11'}}, {'name': 'lsp-id-12', 'addresses': ['10.0.1.2'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-12'}}, {'name': 'lsp-rp-id-1', 'addresses': ['10.0.1.254'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-rp-name-1'}, 'options': {'router-port': utils.ovn_lrouter_port_name('orp-id-a1')}}, {'name': 'provnet-ls-id-1', 'addresses': ['unknown'], 'external_ids': {}, 'options': {'network_name': 'physnet1'}}, {'name': 'lsp-id-21', 'addresses': ['10.0.2.1'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-21'}}, {'name': 'lsp-id-22', 'addresses': ['10.0.2.2'], 'external_ids': {}}, {'name': 'lsp-id-23', 'addresses': ['10.0.2.3'], 'external_ids': {'not-neutron:port_name': 'lsp-name-23'}}, {'name': 'lsp-rp-id-2', 'addresses': ['10.0.2.254'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-rp-name-2'}, 'options': {'router-port': utils.ovn_lrouter_port_name('orp-id-a2')}}, {'name': 'provnet-ls-id-2', 'addresses': ['unknown'], 'external_ids': {}, 'options': {'network_name': 'physnet2'}}, {'name': 'lsp-id-31', 'addresses': ['10.0.3.1'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-31'}}, {'name': 'lsp-id-32', 'addresses': ['10.0.3.2'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-32'}}, {'name': 'lsp-rp-id-3', 'addresses': ['10.0.3.254'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-rp-name-3'}, 'options': {'router-port': utils.ovn_lrouter_port_name('orp-id-a3')}}, {'name': 'lsp-vpn-id-3', 'addresses': ['10.0.3.253'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-vpn-name-3'}}, {'name': 'lsp-id-41', 'addresses': ['20.0.1.1'], 'external_ids': {'not-neutron:port_name': 'lsp-name-41'}}, {'name': 'lsp-rp-id-4', 'addresses': ['20.0.1.254'], 'external_ids': {}, 'options': {'router-port': 'xrp-id-b1'}}, {'name': 'lsp-id-51', 'addresses': ['20.0.2.1'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-51'}}, {'name': 'lsp-id-52', 'addresses': ['20.0.2.2'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-name-52'}}, {'name': 'lsp-rp-id-5', 'addresses': ['20.0.2.254'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-rp-name-5'}, 'options': {'router-port': utils.ovn_lrouter_port_name('orp-id-b2')}}, {'name': 'lsp-vpn-id-5', 'addresses': ['20.0.2.253'], 'external_ids': {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'lsp-vpn-name-5'}}], 'lrouters': [ {'name': utils.ovn_name('lr-id-a'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-a'}}, {'name': utils.ovn_name('lr-id-b'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-b'}}, {'name': utils.ovn_name('lr-id-c'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-c'}}, {'name': utils.ovn_name('lr-id-d'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-d'}}, {'name': utils.ovn_name('lr-id-e'), 'external_ids': {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'lr-name-e'}}], 'lrouter_ports': [ {'name': utils.ovn_lrouter_port_name('orp-id-a1'), 'external_ids': {}, 'networks': ['10.0.1.0/24'], 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-1'}}, {'name': utils.ovn_lrouter_port_name('orp-id-a2'), 'external_ids': {}, 'networks': ['10.0.2.0/24'], 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-1'}}, {'name': utils.ovn_lrouter_port_name('orp-id-a3'), 'external_ids': {}, 'networks': ['10.0.3.0/24'], 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: ovn_const.OVN_GATEWAY_INVALID_CHASSIS}}, {'name': 'xrp-id-b1', 'external_ids': {}, 'networks': ['20.0.1.0/24']}, {'name': utils.ovn_lrouter_port_name('orp-id-b2'), 'external_ids': {}, 'networks': ['20.0.2.0/24'], 'options': {ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}}, {'name': utils.ovn_lrouter_port_name('orp-id-b3'), 'external_ids': {}, 'networks': ['20.0.3.0/24'], 'options': {}}], 'static_routes': [{'ip_prefix': '20.0.0.0/16', 'nexthop': '10.0.3.253'}, {'ip_prefix': '10.0.0.0/16', 'nexthop': '20.0.2.253'}], 'nats': [{'external_ip': '10.0.3.1', 'logical_ip': '20.0.0.0/16', 'type': 'snat'}, {'external_ip': '20.0.2.1', 'logical_ip': '10.0.0.0/24', 'type': 'snat'}, {'external_ip': '20.0.2.4', 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat', 'external_mac': [], 'logical_port': []}, {'external_ip': '20.0.2.5', 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat', 'external_mac': ['00:01:02:03:04:05'], 'logical_port': ['lsp-id-001']}], 'acls': [ {'unit_test_id': 1, 'action': 'allow-related', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'lsp-id-11'}, 'match': 'inport == "lsp-id-11" && ip4'}, {'unit_test_id': 2, 'action': 'allow-related', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'lsp-id-11'}, 'match': 'outport == "lsp-id-11" && ip4.src == $as_ip4_id_1'}, {'unit_test_id': 3, 'action': 'allow-related', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'lsp-id-12'}, 'match': 'inport == "lsp-id-12" && ip4'}, {'unit_test_id': 4, 'action': 'allow-related', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'lsp-id-12'}, 'match': 'outport == "lsp-id-12" && ip4.src == $as_ip4_id_1'}, {'unit_test_id': 5, 'action': 'allow-related', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'lsp-id-21'}, 'match': 'inport == "lsp-id-21" && ip4'}, {'unit_test_id': 6, 'action': 'allow-related', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'lsp-id-21'}, 'match': 'outport == "lsp-id-21" && ip4.src == $as_ip4_id_2'}, {'unit_test_id': 7, 'action': 'allow-related', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'lsp-id-41'}, 'match': 'inport == "lsp-id-41" && ip4'}, {'unit_test_id': 8, 'action': 'allow-related', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'lsp-id-41'}, 'match': 'outport == "lsp-id-41" && ip4.src == $as_ip4_id_4'}, {'unit_test_id': 9, 'action': 'allow-related', 'direction': 'from-lport', 'external_ids': {'neutron:lport': 'lsp-id-52'}, 'match': 'inport == "lsp-id-52" && ip4'}, {'unit_test_id': 10, 'action': 'allow-related', 'direction': 'to-lport', 'external_ids': {'neutron:lport': 'lsp-id-52'}, 'match': 'outport == "lsp-id-52" && ip4.src == $as_ip4_id_5'}], 'dhcp_options': [ {'cidr': '10.0.1.0/24', 'external_ids': {'subnet_id': 'subnet-id-10-0-1-0'}, 'options': {'mtu': '1442', 'router': '10.0.1.254'}}, {'cidr': '10.0.2.0/24', 'external_ids': {'subnet_id': 'subnet-id-10-0-2-0'}, 'options': {'mtu': '1442', 'router': '10.0.2.254'}}, {'cidr': '10.0.1.0/26', 'external_ids': {'subnet_id': 'subnet-id-10-0-1-0', 'port_id': 'lsp-vpn-id-3'}, 'options': {'mtu': '1442', 'router': '10.0.1.1'}}, {'cidr': '20.0.1.0/24', 'external_ids': {'subnet_id': 'subnet-id-20-0-1-0'}, 'options': {'mtu': '1442', 'router': '20.0.1.254'}}, {'cidr': '20.0.2.0/24', 'external_ids': {'subnet_id': 'subnet-id-20-0-2-0', 'port_id': 'lsp-vpn-id-5'}, 'options': {'mtu': '1442', 'router': '20.0.2.254'}}, {'cidr': '2001:dba::/64', 'external_ids': {'subnet_id': 'subnet-id-2001-dba', 'port_id': 'lsp-vpn-id-5'}, 'options': {'server_id': '12:34:56:78:9a:bc'}}, {'cidr': '30.0.1.0/24', 'external_ids': {'port_id': 'port-id-30-0-1-0'}, 'options': {'mtu': '1442', 'router': '30.0.2.254'}}, {'cidr': '30.0.2.0/24', 'external_ids': {}, 'options': {}}], 'address_sets': [ {'name': '$as_ip4_id_1', 'addresses': ['10.0.1.1', '10.0.1.2'], 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_1'}}, {'name': '$as_ip4_id_2', 'addresses': ['10.0.2.1'], 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_2'}}, {'name': '$as_ip4_id_3', 'addresses': ['10.0.3.1', '10.0.3.2'], 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_3'}}, {'name': '$as_ip4_id_4', 'addresses': ['20.0.1.1', '20.0.1.2'], 'external_ids': {}}, {'name': '$as_ip4_id_5', 'addresses': ['20.0.2.1', '20.0.2.2'], 'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'id_5'}}, ]} fake_associations = { 'lstolsp': { utils.ovn_name('ls-id-1'): [ 'lsp-id-11', 'lsp-id-12', 'lsp-rp-id-1', 'provnet-ls-id-1'], utils.ovn_name('ls-id-2'): [ 'lsp-id-21', 'lsp-id-22', 'lsp-id-23', 'lsp-rp-id-2', 'provnet-ls-id-2'], utils.ovn_name('ls-id-3'): [ 'lsp-id-31', 'lsp-id-32', 'lsp-rp-id-3', 'lsp-vpn-id-3'], 'ls-id-4': [ 'lsp-id-41', 'lsp-rp-id-4'], utils.ovn_name('ls-id-5'): [ 'lsp-id-51', 'lsp-id-52', 'lsp-rp-id-5', 'lsp-vpn-id-5']}, 'lrtolrp': { utils.ovn_name('lr-id-a'): [ utils.ovn_lrouter_port_name('orp-id-a1'), utils.ovn_lrouter_port_name('orp-id-a2'), utils.ovn_lrouter_port_name('orp-id-a3')], utils.ovn_name('lr-id-b'): [ 'xrp-id-b1', utils.ovn_lrouter_port_name('orp-id-b2')]}, 'lrtosroute': { utils.ovn_name('lr-id-a'): ['20.0.0.0/16'], utils.ovn_name('lr-id-b'): ['10.0.0.0/16'] }, 'lrtonat': { utils.ovn_name('lr-id-a'): ['10.0.3.1'], utils.ovn_name('lr-id-b'): ['20.0.2.1', '20.0.2.4', '20.0.2.5'], }, 'lstoacl': { utils.ovn_name('ls-id-1'): [1, 2, 3, 4], utils.ovn_name('ls-id-2'): [5, 6], 'ls-id-4': [7, 8], utils.ovn_name('ls-id-5'): [9, 10]} } def setUp(self): super(TestNBImplIdlOvn, self).setUp() self.lswitch_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.lsp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.lrouter_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.lrp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.sroute_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.nat_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.acl_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.dhcp_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self.address_set_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self._tables = {} self._tables['Logical_Switch'] = self.lswitch_table self._tables['Logical_Switch_Port'] = self.lsp_table self._tables['Logical_Router'] = self.lrouter_table self._tables['Logical_Router_Port'] = self.lrp_table self._tables['Logical_Router_Static_Route'] = self.sroute_table self._tables['ACL'] = self.acl_table self._tables['DHCP_Options'] = self.dhcp_table self._tables['Address_Set'] = self.address_set_table with mock.patch.object(impl_idl_ovn, 'get_connection', return_value=mock.Mock()): impl_idl_ovn.OvsdbNbOvnIdl.ovsdb_connection = None self.nb_ovn_idl = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) self.nb_ovn_idl.idl.tables = self._tables def _load_nb_db(self): # Load Switches and Switch Ports fake_lswitches = TestNBImplIdlOvn.fake_set['lswitches'] self._load_ovsdb_fake_rows(self.lswitch_table, fake_lswitches) fake_lsps = TestNBImplIdlOvn.fake_set['lswitch_ports'] self._load_ovsdb_fake_rows(self.lsp_table, fake_lsps) # Associate switches and ports self._construct_ovsdb_references( TestNBImplIdlOvn.fake_associations['lstolsp'], self.lswitch_table, self.lsp_table, 'name', 'name', 'ports') # Load Routers and Router Ports fake_lrouters = TestNBImplIdlOvn.fake_set['lrouters'] self._load_ovsdb_fake_rows(self.lrouter_table, fake_lrouters) fake_lrps = TestNBImplIdlOvn.fake_set['lrouter_ports'] self._load_ovsdb_fake_rows(self.lrp_table, fake_lrps) # Associate routers and router ports self._construct_ovsdb_references( TestNBImplIdlOvn.fake_associations['lrtolrp'], self.lrouter_table, self.lrp_table, 'name', 'name', 'ports') # Load static routes fake_sroutes = TestNBImplIdlOvn.fake_set['static_routes'] self._load_ovsdb_fake_rows(self.sroute_table, fake_sroutes) # Associate routers and static routes self._construct_ovsdb_references( TestNBImplIdlOvn.fake_associations['lrtosroute'], self.lrouter_table, self.sroute_table, 'name', 'ip_prefix', 'static_routes') # Load nats fake_nats = TestNBImplIdlOvn.fake_set['nats'] self._load_ovsdb_fake_rows(self.nat_table, fake_nats) # Associate routers and nats self._construct_ovsdb_references( TestNBImplIdlOvn.fake_associations['lrtonat'], self.lrouter_table, self.nat_table, 'name', 'external_ip', 'nat') # Load acls fake_acls = TestNBImplIdlOvn.fake_set['acls'] self._load_ovsdb_fake_rows(self.acl_table, fake_acls) # Associate switches and acls self._construct_ovsdb_references( TestNBImplIdlOvn.fake_associations['lstoacl'], self.lswitch_table, self.acl_table, 'name', 'unit_test_id', 'acls') # Load dhcp options fake_dhcp_options = TestNBImplIdlOvn.fake_set['dhcp_options'] self._load_ovsdb_fake_rows(self.dhcp_table, fake_dhcp_options) # Load address sets fake_address_sets = TestNBImplIdlOvn.fake_set['address_sets'] self._load_ovsdb_fake_rows(self.address_set_table, fake_address_sets) @mock.patch.object(impl_idl_ovn.OvsdbNbOvnIdl, 'ovsdb_connection', None) @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) def test_setting_ovsdb_probe_timeout_default_value(self): inst = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) inst.idl._session.reconnect.set_probe_interval.assert_called_with(0) @mock.patch.object(impl_idl_ovn.OvsdbNbOvnIdl, 'ovsdb_connection', None) @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) @mock.patch.object(config, 'get_ovn_ovsdb_probe_interval') def test_setting_ovsdb_probe_timeout(self, mock_get_probe_interval): mock_get_probe_interval.return_value = 5000 inst = impl_idl_ovn.OvsdbNbOvnIdl(mock.Mock()) inst.idl._session.reconnect.set_probe_interval.assert_called_with(5000) def test_get_all_logical_switches_with_ports(self): # Test empty mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() self.assertItemsEqual(mapping, {}) # Test loaded values self._load_nb_db() mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() expected = [{'name': utils.ovn_name('ls-id-1'), 'ports': ['lsp-id-11', 'lsp-id-12', 'lsp-rp-id-1'], 'provnet_port': 'provnet-ls-id-1'}, {'name': utils.ovn_name('ls-id-2'), 'ports': ['lsp-id-21', 'lsp-rp-id-2'], 'provnet_port': 'provnet-ls-id-2'}, {'name': utils.ovn_name('ls-id-3'), 'ports': ['lsp-id-31', 'lsp-id-32', 'lsp-rp-id-3', 'lsp-vpn-id-3'], 'provnet_port': None}, {'name': utils.ovn_name('ls-id-5'), 'ports': ['lsp-id-51', 'lsp-id-52', 'lsp-rp-id-5', 'lsp-vpn-id-5'], 'provnet_port': None}] self.assertItemsEqual(mapping, expected) def test_get_all_logical_routers_with_rports(self): # Test empty mapping = self.nb_ovn_idl.get_all_logical_switches_with_ports() self.assertItemsEqual(mapping, {}) # Test loaded values self._load_nb_db() mapping = self.nb_ovn_idl.get_all_logical_routers_with_rports() expected = [{'name': 'lr-id-a', 'ports': {'orp-id-a1': ['10.0.1.0/24'], 'orp-id-a2': ['10.0.2.0/24'], 'orp-id-a3': ['10.0.3.0/24']}, 'static_routes': [{'destination': '20.0.0.0/16', 'nexthop': '10.0.3.253'}], 'snats': [{'external_ip': '10.0.3.1', 'logical_ip': '20.0.0.0/16', 'type': 'snat'}], 'dnat_and_snats': []}, {'name': 'lr-id-b', 'ports': {'xrp-id-b1': ['20.0.1.0/24'], 'orp-id-b2': ['20.0.2.0/24']}, 'static_routes': [{'destination': '10.0.0.0/16', 'nexthop': '20.0.2.253'}], 'snats': [{'external_ip': '20.0.2.1', 'logical_ip': '10.0.0.0/24', 'type': 'snat'}], 'dnat_and_snats': [{'external_ip': '20.0.2.4', 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat'}, {'external_ip': '20.0.2.5', 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat', 'external_mac': '00:01:02:03:04:05', 'logical_port': 'lsp-id-001'}]}, {'name': 'lr-id-c', 'ports': {}, 'static_routes': [], 'snats': [], 'dnat_and_snats': []}, {'name': 'lr-id-d', 'ports': {}, 'static_routes': [], 'snats': [], 'dnat_and_snats': []}, {'name': 'lr-id-e', 'ports': {}, 'static_routes': [], 'snats': [], 'dnat_and_snats': []}] self.assertItemsEqual(mapping, expected) def test_get_acls_for_lswitches(self): self._load_nb_db() # Test neutron switches lswitches = ['ls-id-1', 'ls-id-2', 'ls-id-3', 'ls-id-5'] acl_values, acl_objs, lswitch_ovsdb_dict = \ self.nb_ovn_idl.get_acls_for_lswitches(lswitches) excepted_acl_values = { 'lsp-id-11': [ {'action': 'allow-related', 'lport': 'lsp-id-11', 'lswitch': 'neutron-ls-id-1', 'external_ids': {'neutron:lport': 'lsp-id-11'}, 'direction': 'from-lport', 'match': 'inport == "lsp-id-11" && ip4'}, {'action': 'allow-related', 'lport': 'lsp-id-11', 'lswitch': 'neutron-ls-id-1', 'external_ids': {'neutron:lport': 'lsp-id-11'}, 'direction': 'to-lport', 'match': 'outport == "lsp-id-11" && ip4.src == $as_ip4_id_1'} ], 'lsp-id-12': [ {'action': 'allow-related', 'lport': 'lsp-id-12', 'lswitch': 'neutron-ls-id-1', 'external_ids': {'neutron:lport': 'lsp-id-12'}, 'direction': 'from-lport', 'match': 'inport == "lsp-id-12" && ip4'}, {'action': 'allow-related', 'lport': 'lsp-id-12', 'lswitch': 'neutron-ls-id-1', 'external_ids': {'neutron:lport': 'lsp-id-12'}, 'direction': 'to-lport', 'match': 'outport == "lsp-id-12" && ip4.src == $as_ip4_id_1'} ], 'lsp-id-21': [ {'action': 'allow-related', 'lport': 'lsp-id-21', 'lswitch': 'neutron-ls-id-2', 'external_ids': {'neutron:lport': 'lsp-id-21'}, 'direction': 'from-lport', 'match': 'inport == "lsp-id-21" && ip4'}, {'action': 'allow-related', 'lport': 'lsp-id-21', 'lswitch': 'neutron-ls-id-2', 'external_ids': {'neutron:lport': 'lsp-id-21'}, 'direction': 'to-lport', 'match': 'outport == "lsp-id-21" && ip4.src == $as_ip4_id_2'} ], 'lsp-id-52': [ {'action': 'allow-related', 'lport': 'lsp-id-52', 'lswitch': 'neutron-ls-id-5', 'external_ids': {'neutron:lport': 'lsp-id-52'}, 'direction': 'from-lport', 'match': 'inport == "lsp-id-52" && ip4'}, {'action': 'allow-related', 'lport': 'lsp-id-52', 'lswitch': 'neutron-ls-id-5', 'external_ids': {'neutron:lport': 'lsp-id-52'}, 'direction': 'to-lport', 'match': 'outport == "lsp-id-52" && ip4.src == $as_ip4_id_5'} ]} self.assertItemsEqual(acl_values, excepted_acl_values) self.assertEqual(len(acl_objs), 8) self.assertEqual(len(lswitch_ovsdb_dict), len(lswitches)) # Test non-neutron switches lswitches = ['ls-id-4'] acl_values, acl_objs, lswitch_ovsdb_dict = \ self.nb_ovn_idl.get_acls_for_lswitches(lswitches) self.assertItemsEqual(acl_values, {}) self.assertEqual(len(acl_objs), 0) self.assertEqual(len(lswitch_ovsdb_dict), 0) def test_get_all_chassis_gateway_bindings(self): self._load_nb_db() bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings() expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'), utils.ovn_lrouter_port_name('orp-id-a2')], 'host-2': [utils.ovn_lrouter_port_name('orp-id-b2')], ovn_const.OVN_GATEWAY_INVALID_CHASSIS: [ utils.ovn_name('orp-id-a3')]} self.assertItemsEqual(bindings, expected) bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings([]) self.assertItemsEqual(bindings, expected) bindings = self.nb_ovn_idl.get_all_chassis_gateway_bindings(['host-1']) expected = {'host-1': [utils.ovn_lrouter_port_name('orp-id-a1'), utils.ovn_lrouter_port_name('orp-id-a2')]} self.assertItemsEqual(bindings, expected) def test_get_gateway_chassis_binding(self): self._load_nb_db() chassis = self.nb_ovn_idl.get_gateway_chassis_binding( utils.ovn_lrouter_port_name('orp-id-a1')) self.assertEqual(chassis, ['host-1']) chassis = self.nb_ovn_idl.get_gateway_chassis_binding( utils.ovn_lrouter_port_name('orp-id-b2')) self.assertEqual(chassis, ['host-2']) chassis = self.nb_ovn_idl.get_gateway_chassis_binding( utils.ovn_lrouter_port_name('orp-id-a3')) self.assertEqual(chassis, ['neutron-ovn-invalid-chassis']) chassis = self.nb_ovn_idl.get_gateway_chassis_binding( utils.ovn_lrouter_port_name('orp-id-b3')) self.assertEqual(chassis, []) chassis = self.nb_ovn_idl.get_gateway_chassis_binding('bad') self.assertEqual(chassis, []) def test_get_unhosted_gateways(self): self._load_nb_db() # Test only host-1 in the valid list unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( {}, {'host-1': 'physnet1'}) expected = { utils.ovn_lrouter_port_name('orp-id-b2'): { ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}, utils.ovn_lrouter_port_name('orp-id-a3'): { ovn_const.OVN_GATEWAY_CHASSIS_KEY: ovn_const.OVN_GATEWAY_INVALID_CHASSIS}} self.assertItemsEqual(unhosted_gateways, expected) # Test both host-1, host-2 in valid list unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}) expected = {utils.ovn_lrouter_port_name('orp-id-a3'): { ovn_const.OVN_GATEWAY_CHASSIS_KEY: ovn_const.OVN_GATEWAY_INVALID_CHASSIS}} self.assertItemsEqual(unhosted_gateways, expected) # Schedule unhosted_gateways on host-2 for unhosted_gateway in unhosted_gateways: router_row = self._find_ovsdb_fake_row(self.lrp_table, 'name', unhosted_gateway) setattr(router_row, 'options', { ovn_const.OVN_GATEWAY_CHASSIS_KEY: 'host-2'}) unhosted_gateways = self.nb_ovn_idl.get_unhosted_gateways( {}, {'host-1': 'physnet1', 'host-2': 'physnet2'}) self.assertItemsEqual(unhosted_gateways, {}) def test_get_subnet_dhcp_options(self): self._load_nb_db() subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-10-0-2-0') expected_row = self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.2.0/24') self.assertEqual({ 'subnet': {'cidr': expected_row.cidr, 'external_ids': expected_row.external_ids, 'options': expected_row.options, 'uuid': expected_row.uuid}, 'ports': []}, subnet_options) subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-11-0-2-0')['subnet'] self.assertIsNone(subnet_options) subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'port-id-30-0-1-0')['subnet'] self.assertIsNone(subnet_options) def test_get_subnet_dhcp_options_with_ports(self): # Test empty subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-10-0-1-0', with_ports=True) self.assertItemsEqual({'subnet': None, 'ports': []}, subnet_options) # Test loaded values self._load_nb_db() # Test getting both subnet and port dhcp options subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-10-0-1-0', with_ports=True) dhcp_rows = [ self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.1.0/24'), self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.1.0/26')] expected_rows = [{'cidr': dhcp_row.cidr, 'external_ids': dhcp_row.external_ids, 'options': dhcp_row.options, 'uuid': dhcp_row.uuid} for dhcp_row in dhcp_rows] self.assertItemsEqual(expected_rows, [ subnet_options['subnet']] + subnet_options['ports']) # Test getting only subnet dhcp options subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-10-0-2-0', with_ports=True) dhcp_rows = [ self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '10.0.2.0/24')] expected_rows = [{'cidr': dhcp_row.cidr, 'external_ids': dhcp_row.external_ids, 'options': dhcp_row.options, 'uuid': dhcp_row.uuid} for dhcp_row in dhcp_rows] self.assertItemsEqual(expected_rows, [ subnet_options['subnet']] + subnet_options['ports']) # Test getting no dhcp options subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options( 'subnet-id-11-0-2-0', with_ports=True) self.assertItemsEqual({'subnet': None, 'ports': []}, subnet_options) def test_get_subnets_dhcp_options(self): self._load_nb_db() get_row_dict = lambda row: { 'cidr': row.cidr, 'external_ids': row.external_ids, 'options': row.options, 'uuid': row.uuid} subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( ['subnet-id-10-0-1-0', 'subnet-id-10-0-2-0']) expected_rows = [ get_row_dict( self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', cidr)) for cidr in ('10.0.1.0/24', '10.0.2.0/24')] self.assertItemsEqual(expected_rows, subnets_options) subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( ['subnet-id-11-0-2-0', 'subnet-id-20-0-1-0']) expected_row = get_row_dict( self._find_ovsdb_fake_row(self.dhcp_table, 'cidr', '20.0.1.0/24')) self.assertItemsEqual([expected_row], subnets_options) subnets_options = self.nb_ovn_idl.get_subnets_dhcp_options( ['port-id-30-0-1-0', 'fake-not-exist']) self.assertEqual([], subnets_options) def test_get_all_dhcp_options(self): self._load_nb_db() dhcp_options = self.nb_ovn_idl.get_all_dhcp_options() self.assertEqual(len(dhcp_options['subnets']), 3) self.assertEqual(len(dhcp_options['ports_v4']), 2) def test_get_address_sets(self): self._load_nb_db() address_sets = self.nb_ovn_idl.get_address_sets() self.assertEqual(len(address_sets), 4) class TestSBImplIdlOvn(TestDBImplIdlOvn): fake_set = { 'chassis': [ {'name': 'host-1', 'hostname': 'host-1.localdomain.com', 'external_ids': {'ovn-bridge-mappings': 'public:br-ex,private:br-0'}}, {'name': 'host-2', 'hostname': 'host-2.localdomain.com', 'external_ids': {'ovn-bridge-mappings': 'public:br-ex,public2:br-ex'}}, {'name': 'host-3', 'hostname': 'host-3.localdomain.com', 'external_ids': {'ovn-bridge-mappings': 'public:br-ex'}}, ] } def setUp(self): super(TestSBImplIdlOvn, self).setUp() self.chassis_table = fakes.FakeOvsdbTable.create_one_ovsdb_table() self._tables = {} self._tables['Chassis'] = self.chassis_table with mock.patch.object(impl_idl_ovn, 'get_connection', return_value=mock.Mock()): impl_idl_ovn.OvsdbSbOvnIdl.ovsdb_connection = None self.sb_ovn_idl = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) self.sb_ovn_idl.idl.tables = self._tables def _load_sb_db(self): # Load Chassis fake_chassis = TestSBImplIdlOvn.fake_set['chassis'] self._load_ovsdb_fake_rows(self.chassis_table, fake_chassis) @mock.patch.object(impl_idl_ovn.OvsdbSbOvnIdl, 'ovsdb_connection', None) @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) def test_setting_ovsdb_probe_timeout_default_value(self): inst = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) inst.idl._session.reconnect.set_probe_interval.assert_called_with(0) @mock.patch.object(impl_idl_ovn.OvsdbSbOvnIdl, 'ovsdb_connection', None) @mock.patch.object(impl_idl_ovn, 'get_connection', mock.Mock()) @mock.patch.object(config, 'get_ovn_ovsdb_probe_interval') def test_setting_ovsdb_probe_timeout(self, mock_get_probe_interval): mock_get_probe_interval.return_value = 5000 inst = impl_idl_ovn.OvsdbSbOvnIdl(mock.Mock()) inst.idl._session.reconnect.set_probe_interval.assert_called_with(5000) networking-ovn-4.0.0/networking_ovn/tests/unit/test_ovn_vtep.py0000666000175100017510000000777013245511145025207 0ustar zuulzuul00000000000000# Copyright (c) 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. from networking_ovn.common import constants as ovn_const from networking_ovn.tests.unit.ml2 import test_mech_driver OVN_PROFILE = ovn_const.OVN_PORT_BINDING_PROFILE class TestOVNVtepPortBinding(test_mech_driver.OVNMechanismDriverTestCase): def test_create_port_with_vtep_options(self): binding = {OVN_PROFILE: {"vtep-physical-switch": 'psw1', "vtep-logical-switch": 'lsw1'}} with self.network() as n: with self.subnet(n): res = self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), **binding) port = self.deserialize(self.fmt, res) self.assertEqual(binding[OVN_PROFILE], port['port'][OVN_PROFILE]) def test_create_port_with_only_vtep_physical_switch(self): binding = {OVN_PROFILE: {"vtep-physical-switch": 'psw'}} with self.network() as n: with self.subnet(n): self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), expected_res_status=400, **binding) def test_create_port_with_only_vtep_logical_switch(self): binding = {OVN_PROFILE: {"vtep-logical-switch": 'lsw1'}} with self.network() as n: with self.subnet(n): self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), expected_res_status=400, **binding) def test_create_port_with_invalid_vtep_logical_switch(self): binding = {OVN_PROFILE: {"vtep-logical-switch": 1234, "vtep-physical-switch": "psw1"}} with self.network() as n: with self.subnet(n): self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), expected_res_status=400, **binding) def test_create_port_with_vtep_options_and_parent_name_tag(self): binding = {OVN_PROFILE: {"vtep-logical-switch": "lsw1", "vtep-physical-switch": "psw1", "parent_name": "pname", "tag": 22}} with self.network() as n: with self.subnet(n): self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), expected_res_status=400, **binding) def test_create_port_with_vtep_options_and_check_vtep_keys(self): port = { 'id': 'foo-port', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'foo-subnet', 'ip_address': '10.0.0.11'}], OVN_PROFILE: {"vtep-logical-switch": "lsw1", "vtep-physical-switch": "psw1"} } ovn_port_info = ( self.mech_driver._ovn_client._get_port_options(port)) self.assertEqual(port[OVN_PROFILE]["vtep-physical-switch"], ovn_port_info.options["vtep-physical-switch"]) self.assertEqual(port[OVN_PROFILE]["vtep-logical-switch"], ovn_port_info.options["vtep-logical-switch"]) networking-ovn-4.0.0/networking_ovn/tests/unit/__init__.py0000666000175100017510000000000013245511145024022 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/test_ovn_db_sync.py0000666000175100017510000013016713245511145025647 0ustar zuulzuul00000000000000# 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 mock from networking_ovn.common import constants as ovn_const from networking_ovn.common import ovn_client from networking_ovn import ovn_db_sync from networking_ovn.tests.unit.ml2 import test_mech_driver @mock.patch('networking_ovn.l3.l3_ovn.OVNL3RouterPlugin._sb_ovn', mock.Mock()) class TestOvnNbSyncML2(test_mech_driver.OVNMechanismDriverTestCase): l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin' def setUp(self): super(TestOvnNbSyncML2, self).setUp() self.subnet = {'cidr': '10.0.0.0/24', 'id': 'subnet1', 'subnetpool_id': None, 'name': 'private-subnet', 'enable_dhcp': True, 'network_id': 'n1', 'tenant_id': 'tenant1', 'gateway_ip': '10.0.0.1', 'ip_version': 4, 'shared': False} self.matches = ["", "", "", ""] self.networks = [{'id': 'n1', 'mtu': 1450, 'provider:physical_network': 'physnet1', 'provider:segmentation_id': 1000}, {'id': 'n2', 'mtu': 1450}, {'id': 'n4', 'mtu': 1450, 'provider:physical_network': 'physnet2'}] self.subnets = [{'id': 'n1-s1', 'network_id': 'n1', 'enable_dhcp': True, 'cidr': '10.0.0.0/24', 'tenant_id': 'tenant1', 'gateway_ip': '10.0.0.1', 'dns_nameservers': [], 'host_routes': [], 'ip_version': 4}, {'id': 'n1-s2', 'network_id': 'n1', 'enable_dhcp': True, 'cidr': 'fd79:e1c:a55::/64', 'tenant_id': 'tenant1', 'gateway_ip': 'fd79:e1c:a55::1', 'dns_nameservers': [], 'host_routes': [], 'ip_version': 6}, {'id': 'n2', 'network_id': 'n2', 'enable_dhcp': True, 'cidr': '20.0.0.0/24', 'tenant_id': 'tenant1', 'gateway_ip': '20.0.0.1', 'dns_nameservers': [], 'host_routes': [], 'ip_version': 4}] self.security_groups = [ {'id': 'sg1', 'tenant_id': 'tenant1', 'security_group_rules': [{'remote_group_id': None, 'direction': 'ingress', 'remote_ip_prefix': '0.0.0.0/0', 'protocol': 'tcp', 'ethertype': 'IPv4', 'tenant_id': 'tenant1', 'port_range_max': 65535, 'port_range_min': 1, 'id': 'ruleid1', 'security_group_id': 'sg1'}], 'name': 'all-tcp'}, {'id': 'sg2', 'tenant_id': 'tenant1', 'security_group_rules': [{'remote_group_id': 'sg2', 'direction': 'egress', 'remote_ip_prefix': '0.0.0.0/0', 'protocol': 'tcp', 'ethertype': 'IPv4', 'tenant_id': 'tenant1', 'port_range_max': 65535, 'port_range_min': 1, 'id': 'ruleid1', 'security_group_id': 'sg2'}], 'name': 'all-tcpe'}] self.ports = [ {'id': 'p1n1', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40', 'ip_address': '10.0.0.4'}, {'subnet_id': 'subnet1', 'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}], 'security_groups': ['sg1'], 'network_id': 'n1'}, {'id': 'p2n1', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40', 'ip_address': '10.0.0.4'}, {'subnet_id': 'subnet1', 'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}], 'security_groups': ['sg2'], 'network_id': 'n1', 'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'foo-domain'}]}, {'id': 'p1n2', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40', 'ip_address': '10.0.0.4'}, {'subnet_id': 'subnet1', 'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}], 'security_groups': ['sg1'], 'network_id': 'n2', 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': '20.0.0.20'}, {'ip_version': 4, 'opt_name': 'dns-server', 'opt_value': '8.8.8.8'}, {'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'foo-domain'}]}, {'id': 'p2n2', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'b142f5e3-d434-4740-8e88-75e8e5322a40', 'ip_address': '10.0.0.4'}, {'subnet_id': 'subnet1', 'ip_address': 'fd79:e1c:a55::816:eff:eff:ff2'}], 'security_groups': ['sg2'], 'network_id': 'n2'}, {'id': 'fp1', 'device_owner': 'network:floatingip', 'fixed_ips': [{'subnet_id': 'ext-subnet', 'ip_address': '90.0.0.10'}], 'network_id': 'ext-net'}] self.acls_ovn = { 'lport1': # ACLs need to be removed by the sync tool [{'id': 'acl1', 'priority': 00, 'policy': 'allow', 'lswitch': 'lswitch1', 'lport': 'lport1'}], 'lport2': [{'id': 'acl2', 'priority': 00, 'policy': 'drop', 'lswitch': 'lswitch2', 'lport': 'lport2'}], # ACLs need to be kept as-is by the sync tool 'p2n2': [{'lport': 'p2n2', 'direction': 'to-lport', 'log': False, 'lswitch': 'neutron-n2', 'priority': 1001, 'action': 'drop', 'external_ids': {'neutron:lport': 'p2n2'}, 'match': 'outport == "p2n2" && ip'}, {'lport': 'p2n2', 'direction': 'to-lport', 'log': False, 'lswitch': 'neutron-n2', 'priority': 1002, 'action': 'allow', 'external_ids': {'neutron:lport': 'p2n2'}, 'match': 'outport == "p2n2" && ip4 && ' 'ip4.src == 10.0.0.0/24 && udp && ' 'udp.src == 67 && udp.dst == 68'}]} self.address_sets_ovn = { 'as_ip4_sg1': {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'all-tcp'}, 'name': 'as_ip4_sg1', 'addresses': ['10.0.0.4']}, 'as_ip4_sg2': {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'all-tcpe'}, 'name': 'as_ip4_sg2', 'addresses': []}, 'as_ip6_sg2': {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'all-tcpe'}, 'name': 'as_ip6_sg2', 'addresses': ['fd79:e1c:a55::816:eff:eff:ff2', 'fd79:e1c:a55::816:eff:eff:ff3']}, 'as_ip4_del': {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'all-delete'}, 'name': 'as_ip4_delete', 'addresses': ['10.0.0.4']}, } self.routers = [{'id': 'r1', 'routes': [{'nexthop': '20.0.0.100', 'destination': '11.0.0.0/24'}, { 'nexthop': '20.0.0.101', 'destination': '12.0.0.0/24'}], 'gw_port_id': 'gpr1', 'external_gateway_info': { 'network_id': "ext-net", 'enable_snat': True, 'external_fixed_ips': [ {'subnet_id': 'ext-subnet', 'ip_address': '90.0.0.2'}]}}, {'id': 'r2', 'routes': [{'nexthop': '40.0.0.100', 'destination': '30.0.0.0/24'}], 'gw_port_id': 'gpr2', 'external_gateway_info': { 'network_id': "ext-net", 'enable_snat': True, 'external_fixed_ips': [ {'subnet_id': 'ext-subnet', 'ip_address': '100.0.0.2'}]}}, {'id': 'r4', 'routes': []}] self.get_sync_router_ports = [ {'fixed_ips': [{'subnet_id': 'subnet1', 'ip_address': '192.168.1.1'}], 'id': 'p1r1', 'device_id': 'r1', 'mac_address': 'fa:16:3e:d7:fd:5f'}, {'fixed_ips': [{'subnet_id': 'subnet2', 'ip_address': '192.168.2.1'}], 'id': 'p1r2', 'device_id': 'r2', 'mac_address': 'fa:16:3e:d6:8b:ce'}, {'fixed_ips': [{'subnet_id': 'subnet4', 'ip_address': '192.168.4.1'}], 'id': 'p1r4', 'device_id': 'r4', 'mac_address': 'fa:16:3e:12:34:56'}] self.floating_ips = [{'id': 'fip1', 'router_id': 'r1', 'floating_ip_address': '90.0.0.10', 'fixed_ip_address': '172.16.0.10'}, {'id': 'fip2', 'router_id': 'r1', 'floating_ip_address': '90.0.0.12', 'fixed_ip_address': '172.16.2.12'}, {'id': 'fip3', 'router_id': 'r2', 'floating_ip_address': '100.0.0.10', 'fixed_ip_address': '192.168.2.10'}, {'id': 'fip4', 'router_id': 'r2', 'floating_ip_address': '100.0.0.11', 'fixed_ip_address': '192.168.2.11'}] self.lrouters_with_rports = [{'name': 'r3', 'ports': {'p1r3': ['fake']}, 'static_routes': [], 'snats': [], 'dnat_and_snats': []}, {'name': 'r4', 'ports': {'p1r4': ['fdad:123:456::1/64', 'fdad:789:abc::1/64']}, 'static_routes': [], 'snats': [], 'dnat_and_snats': []}, {'name': 'r1', 'ports': {'p3r1': ['fake']}, 'static_routes': [{'nexthop': '20.0.0.100', 'destination': '11.0.0.0/24'}, {'nexthop': '20.0.0.100', 'destination': '10.0.0.0/24'}], 'snats': [{'logical_ip': '172.16.0.0/24', 'external_ip': '90.0.0.2', 'type': 'snat'}, {'logical_ip': '172.16.1.0/24', 'external_ip': '90.0.0.2', 'type': 'snat'}], 'dnat_and_snats': [{'logical_ip': '172.16.0.10', 'external_ip': '90.0.0.10', 'type': 'dnat_and_snat'}, {'logical_ip': '172.16.1.11', 'external_ip': '90.0.0.11', 'type': 'dnat_and_snat'}, {'logical_ip': '192.168.2.11', 'external_ip': '100.0.0.11', 'type': 'dnat_and_snat', 'external_mac': '01:02:03:04:05:06', 'logical_port': 'vm1'}]}] self.lswitches_with_ports = [{'name': 'neutron-n1', 'ports': ['p1n1', 'p3n1'], 'provnet_port': None}, {'name': 'neutron-n3', 'ports': ['p1n3', 'p2n3'], 'provnet_port': None}, {'name': 'neutron-n4', 'ports': [], 'provnet_port': 'provnet-n4'}] self.lrport_networks = ['fdad:123:456::1/64', 'fdad:cafe:a1b2::1/64'] def _fake_get_ovn_dhcp_options(self, subnet, network, server_mac=None): if subnet['id'] == 'n1-s1': return {'cidr': '10.0.0.0/24', 'options': {'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1450', 'router': '10.0.0.1'}, 'external_ids': {'subnet_id': 'n1-s1'}} return {'cidr': '', 'options': '', 'external_ids': {}} def _fake_get_gw_info(self, ctx, router): return { 'r1': ovn_client.GW_INFO(router_ip='90.0.0.2', gateway_ip='90.0.0.1', network_id='', subnet_id=''), 'r2': ovn_client.GW_INFO(router_ip='100.0.0.2', gateway_ip='100.0.0.1', network_id='', subnet_id='') }.get(router['id'], ovn_client.GW_INFO('', '', '', '')) def _fake_get_v4_network_of_all_router_ports(self, ctx, router_id): return {'r1': ['172.16.0.0/24', '172.16.2.0/24'], 'r2': ['192.168.2.0/24']}.get(router_id, []) def _test_mocks_helper(self, ovn_nb_synchronizer): core_plugin = ovn_nb_synchronizer.core_plugin ovn_api = ovn_nb_synchronizer.ovn_api ovn_driver = ovn_nb_synchronizer.ovn_driver l3_plugin = ovn_nb_synchronizer.l3_plugin core_plugin.get_networks = mock.Mock() core_plugin.get_networks.return_value = self.networks core_plugin.get_subnets = mock.Mock() core_plugin.get_subnets.return_value = self.subnets # following block is used for acl syncing unit-test # With the given set of values in the unit testing, # 19 neutron acls should have been there, # 4 acls are returned as current ovn acls, # two of which will match with neutron. # So, in this example 17 will be added, 2 removed core_plugin.get_ports = mock.Mock() core_plugin.get_ports.return_value = self.ports mock.patch( "networking_ovn.common.acl._get_subnet_from_cache", return_value=self.subnet ).start() mock.patch( "networking_ovn.common.acl.acl_remote_group_id", side_effect=self.matches ).start() core_plugin.get_security_group = mock.MagicMock( side_effect=self.security_groups) ovn_nb_synchronizer.get_acls = mock.Mock() ovn_nb_synchronizer.get_acls.return_value = self.acls_ovn core_plugin.get_security_groups = mock.MagicMock( return_value=self.security_groups) ovn_nb_synchronizer.get_address_sets = mock.Mock() ovn_nb_synchronizer.get_address_sets.return_value =\ self.address_sets_ovn # end of acl-sync block # The following block is used for router and router port syncing tests # With the give set of values in the unit test, # The Neutron db has Routers r1 and r2 present. # The OVN db has Routers r1 and r3 present. # During the sync r2 will need to be created and r3 will need # to be deleted from the OVN db. When Router r3 is deleted, all LRouter # ports associated with r3 is deleted too. # # Neutron db has Router ports p1r1 in Router r1 and p1r2 in Router r2 # OVN db has p1r3 in Router 3 and p3r1 in Router 1. # During the sync p1r1 and p1r2 will be added and p1r3 and p3r1 # will be deleted from the OVN db l3_plugin.get_routers = mock.Mock() l3_plugin.get_routers.return_value = self.routers l3_plugin._get_sync_interfaces = mock.Mock() l3_plugin._get_sync_interfaces.return_value = ( self.get_sync_router_ports) ovn_nb_synchronizer._ovn_client = mock.Mock() ovn_nb_synchronizer._ovn_client.\ _get_nets_and_ipv6_ra_confs_for_router_port.return_value = ( self.lrport_networks, {}) ovn_nb_synchronizer._ovn_client._get_v4_network_of_all_router_ports. \ side_effect = self._fake_get_v4_network_of_all_router_ports ovn_nb_synchronizer._ovn_client._get_gw_info = mock.Mock() ovn_nb_synchronizer._ovn_client._get_gw_info.side_effect = ( self._fake_get_gw_info) # end of router-sync block l3_plugin.get_floatingips = mock.Mock() l3_plugin.get_floatingips.return_value = self.floating_ips ovn_api.get_all_logical_switches_with_ports = mock.Mock() ovn_api.get_all_logical_switches_with_ports.return_value = ( self.lswitches_with_ports) ovn_api.get_all_logical_routers_with_rports = mock.Mock() ovn_api.get_all_logical_routers_with_rports.return_value = ( self.lrouters_with_rports) ovn_api.transaction = mock.MagicMock() ovn_nb_synchronizer._ovn_client.create_network = mock.Mock() ovn_nb_synchronizer._ovn_client.create_port = mock.Mock() ovn_driver.validate_and_get_data_from_binding_profile = mock.Mock() ovn_nb_synchronizer._ovn_client.create_port = mock.Mock() ovn_nb_synchronizer._ovn_client.create_port.return_value = mock.ANY ovn_nb_synchronizer._ovn_client._create_provnet_port = mock.Mock() ovn_api.ls_del = mock.Mock() ovn_api.delete_lswitch_port = mock.Mock() ovn_api.delete_lrouter = mock.Mock() ovn_api.delete_lrouter_port = mock.Mock() ovn_api.add_static_route = mock.Mock() ovn_api.delete_static_route = mock.Mock() ovn_api.get_all_dhcp_options.return_value = { 'subnets': {'n1-s1': {'cidr': '10.0.0.0/24', 'options': {'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1450', 'router': '10.0.0.1'}, 'external_ids': {'subnet_id': 'n1-s1'}, 'uuid': 'UUID1'}, 'n1-s3': {'cidr': '30.0.0.0/24', 'options': {'server_id': '30.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1450', 'router': '30.0.0.1'}, 'external_ids': {'subnet_id': 'n1-s3'}, 'uuid': 'UUID2'}}, 'ports_v4': {'p1n2': {'cidr': '10.0.0.0/24', 'options': {'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': '1000', 'mtu': '1400', 'router': '10.0.0.1'}, 'external_ids': {'subnet_id': 'n1-s1', 'port_id': 'p1n2'}, 'uuid': 'UUID3'}, 'p5n2': {'cidr': '10.0.0.0/24', 'options': {'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': '1000', 'mtu': '1400', 'router': '10.0.0.1'}, 'external_ids': {'subnet_id': 'n1-s1', 'port_id': 'p5n2'}, 'uuid': 'UUID4'}}, 'ports_v6': {'p1n1': {'cidr': 'fd79:e1c:a55::/64', 'options': {'server_id': '01:02:03:04:05:06', 'mtu': '1450'}, 'external_ids': {'subnet_id': 'fake', 'port_id': 'p1n1'}, 'uuid': 'UUID5'}, 'p1n2': {'cidr': 'fd79:e1c:a55::/64', 'options': {'server_id': '01:02:03:04:05:06', 'mtu': '1450'}, 'external_ids': {'subnet_id': 'fake', 'port_id': 'p1n2'}, 'uuid': 'UUID6'}}} ovn_api.create_address_set = mock.Mock() ovn_api.delete_address_set = mock.Mock() ovn_api.update_address_set = mock.Mock() ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options = mock.Mock() ovn_nb_synchronizer._ovn_client._get_ovn_dhcp_options = mock.Mock() ovn_nb_synchronizer._ovn_client._get_ovn_dhcp_options.side_effect = ( self._fake_get_ovn_dhcp_options) ovn_api.delete_dhcp_options = mock.Mock() ovn_nb_synchronizer._ovn_client.get_port_dns_records = mock.Mock() ovn_nb_synchronizer._ovn_client.get_port_dns_records.return_value = {} def _test_ovn_nb_sync_helper(self, ovn_nb_synchronizer, networks, ports, routers, router_ports, create_router_list, create_router_port_list, update_router_port_list, del_router_list, del_router_port_list, create_network_list, create_port_list, create_provnet_port_list, del_network_list, del_port_list, add_static_route_list, del_static_route_list, add_snat_list, del_snat_list, add_floating_ip_list, del_floating_ip_list, add_address_set_list, del_address_set_list, update_address_set_list, add_subnet_dhcp_options_list, delete_dhcp_options_list): self._test_mocks_helper(ovn_nb_synchronizer) core_plugin = ovn_nb_synchronizer.core_plugin ovn_api = ovn_nb_synchronizer.ovn_api mock.patch("networking_ovn.ovsdb.impl_idl_ovn.get_connection").start() ovn_nb_synchronizer.do_sync() get_security_group_calls = [mock.call(mock.ANY, sg['id']) for sg in self.security_groups] self.assertEqual(len(self.security_groups), core_plugin.get_security_group.call_count) core_plugin.get_security_group.assert_has_calls( get_security_group_calls, any_order=True) self.assertEqual( len(create_network_list), ovn_nb_synchronizer._ovn_client.create_network.call_count) create_network_calls = [mock.call(net['net']) for net in create_network_list] ovn_nb_synchronizer._ovn_client.create_network.assert_has_calls( create_network_calls, any_order=True) self.assertEqual( len(create_port_list), ovn_nb_synchronizer._ovn_client.create_port.call_count) create_port_calls = [mock.call(port) for port in create_port_list] ovn_nb_synchronizer._ovn_client.create_port.assert_has_calls( create_port_calls, any_order=True) create_provnet_port_calls = [ mock.call(mock.ANY, mock.ANY, network['provider:physical_network'], network['provider:segmentation_id']) for network in create_provnet_port_list] self.assertEqual( len(create_provnet_port_list), ovn_nb_synchronizer._ovn_client._create_provnet_port.call_count) ovn_nb_synchronizer._ovn_client._create_provnet_port.assert_has_calls( create_provnet_port_calls, any_order=True) self.assertEqual(len(del_network_list), ovn_api.ls_del.call_count) ls_del_calls = [mock.call(net_name) for net_name in del_network_list] ovn_api.ls_del.assert_has_calls( ls_del_calls, any_order=True) self.assertEqual(len(del_port_list), ovn_api.delete_lswitch_port.call_count) delete_lswitch_port_calls = [mock.call(lport_name=port['id'], lswitch_name=port['lswitch']) for port in del_port_list] ovn_api.delete_lswitch_port.assert_has_calls( delete_lswitch_port_calls, any_order=True) add_route_calls = [mock.call(mock.ANY, ip_prefix=route['destination'], nexthop=route['nexthop']) for route in add_static_route_list] ovn_api.add_static_route.assert_has_calls(add_route_calls, any_order=True) self.assertEqual(len(add_static_route_list), ovn_api.add_static_route.call_count) del_route_calls = [mock.call(mock.ANY, ip_prefix=route['destination'], nexthop=route['nexthop']) for route in del_static_route_list] ovn_api.delete_static_route.assert_has_calls(del_route_calls, any_order=True) self.assertEqual(len(del_static_route_list), ovn_api.delete_static_route.call_count) add_nat_calls = [mock.call(mock.ANY, **nat) for nat in add_snat_list] ovn_api.add_nat_rule_in_lrouter.assert_has_calls(add_nat_calls, any_order=True) self.assertEqual(len(add_snat_list), ovn_api.add_nat_rule_in_lrouter.call_count) add_fip_calls = [mock.call(nat, txn=mock.ANY) for nat in add_floating_ip_list] (ovn_nb_synchronizer._ovn_client._create_or_update_floatingip. assert_has_calls(add_fip_calls)) self.assertEqual( len(add_floating_ip_list), ovn_nb_synchronizer._ovn_client._create_or_update_floatingip. call_count) del_nat_calls = [mock.call(mock.ANY, **nat) for nat in del_snat_list] ovn_api.delete_nat_rule_in_lrouter.assert_has_calls(del_nat_calls, any_order=True) self.assertEqual(len(del_snat_list), ovn_api.delete_nat_rule_in_lrouter.call_count) del_fip_calls = [mock.call(nat, mock.ANY, txn=mock.ANY) for nat in del_floating_ip_list] ovn_nb_synchronizer._ovn_client._delete_floatingip.assert_has_calls( del_fip_calls, any_order=True) self.assertEqual( len(del_floating_ip_list), ovn_nb_synchronizer._ovn_client._delete_floatingip.call_count) create_router_calls = [mock.call(r, add_external_gateway=False) for r in create_router_list] self.assertEqual( len(create_router_list), ovn_nb_synchronizer._ovn_client.create_router.call_count) ovn_nb_synchronizer._ovn_client.create_router.assert_has_calls( create_router_calls, any_order=True) create_router_port_calls = [mock.call(p['device_id'], mock.ANY) for p in create_router_port_list] self.assertEqual( len(create_router_port_list), ovn_nb_synchronizer._ovn_client.create_router_port.call_count) ovn_nb_synchronizer._ovn_client.create_router_port.assert_has_calls( create_router_port_calls, any_order=True) self.assertEqual(len(del_router_list), ovn_api.delete_lrouter.call_count) update_router_port_calls = [mock.call(p) for p in update_router_port_list] self.assertEqual( len(update_router_port_list), ovn_nb_synchronizer._ovn_client.update_router_port.call_count) ovn_nb_synchronizer._ovn_client.update_router_port.assert_has_calls( update_router_port_calls, any_order=True) delete_lrouter_calls = [mock.call(r['router']) for r in del_router_list] ovn_api.delete_lrouter.assert_has_calls( delete_lrouter_calls, any_order=True) self.assertEqual( len(del_router_port_list), ovn_api.delete_lrouter_port.call_count) delete_lrouter_port_calls = [mock.call(port['id'], port['router'], if_exists=False) for port in del_router_port_list] ovn_api.delete_lrouter_port.assert_has_calls( delete_lrouter_port_calls, any_order=True) create_address_set_calls = [mock.call(**a) for a in add_address_set_list] self.assertEqual( len(add_address_set_list), ovn_api.create_address_set.call_count) ovn_api.create_address_set.assert_has_calls( create_address_set_calls, any_order=True) del_address_set_calls = [mock.call(**d) for d in del_address_set_list] self.assertEqual( len(del_address_set_list), ovn_api.delete_address_set.call_count) ovn_api.delete_address_set.assert_has_calls( del_address_set_calls, any_order=True) update_address_set_calls = [mock.call(**u) for u in update_address_set_list] self.assertEqual( len(update_address_set_list), ovn_api.update_address_set.call_count) ovn_api.update_address_set.assert_has_calls( update_address_set_calls, any_order=True) self.assertEqual( len(add_subnet_dhcp_options_list), ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options. call_count) add_subnet_dhcp_options_calls = [ mock.call(subnet, net, mock.ANY) for (subnet, net) in add_subnet_dhcp_options_list] ovn_nb_synchronizer._ovn_client._add_subnet_dhcp_options. \ assert_has_calls(add_subnet_dhcp_options_calls, any_order=True) self.assertEqual(ovn_api.delete_dhcp_options.call_count, len(delete_dhcp_options_list)) delete_dhcp_options_calls = [ mock.call(dhcp_opt_uuid) for dhcp_opt_uuid in delete_dhcp_options_list] ovn_api.delete_dhcp_options.assert_has_calls( delete_dhcp_options_calls, any_order=True) def test_ovn_nb_sync_mode_repair(self): create_network_list = [{'net': {'id': 'n2', 'mtu': 1450}, 'ext_ids': {}}] del_network_list = ['neutron-n3'] del_port_list = [{'id': 'p3n1', 'lswitch': 'neutron-n1'}, {'id': 'p1n1', 'lswitch': 'neutron-n1'}] create_port_list = self.ports for port in create_port_list: if port['id'] in ['p1n1', 'fp1']: # this will be skipped by the logic, # because p1n1 is already in lswitch-port list # and fp1 is a floating IP port create_port_list.remove(port) create_provnet_port_list = [{'id': 'n1', 'mtu': 1450, 'provider:physical_network': 'physnet1', 'provider:segmentation_id': 1000}] create_router_list = [{ 'id': 'r2', 'routes': [ {'nexthop': '40.0.0.100', 'destination': '30.0.0.0/24'}], 'gw_port_id': 'gpr2', 'external_gateway_info': { 'network_id': "ext-net", 'enable_snat': True, 'external_fixed_ips': [{ 'subnet_id': 'ext-subnet', 'ip_address': '100.0.0.2'}]}}] # Test adding and deleting routes snats fips behaviors for router r1 # existing in both neutron DB and OVN DB. # Test adding behaviors for router r2 only existing in neutron DB. # Static routes with destination 0.0.0.0/0 are default gateway routes add_static_route_list = [{'nexthop': '20.0.0.101', 'destination': '12.0.0.0/24'}, {'nexthop': '90.0.0.1', 'destination': '0.0.0.0/0'}, {'nexthop': '40.0.0.100', 'destination': '30.0.0.0/24'}, {'nexthop': '100.0.0.1', 'destination': '0.0.0.0/0'}] del_static_route_list = [{'nexthop': '20.0.0.100', 'destination': '10.0.0.0/24'}] add_snat_list = [{'logical_ip': '172.16.2.0/24', 'external_ip': '90.0.0.2', 'type': 'snat'}, {'logical_ip': '192.168.2.0/24', 'external_ip': '100.0.0.2', 'type': 'snat'}] del_snat_list = [{'logical_ip': '172.16.1.0/24', 'external_ip': '90.0.0.2', 'type': 'snat'}] # fip 100.0.0.11 exists in OVN with distributed type and in Neutron # with centralized type. This fip is used to test # enable_distributed_floating_ip switch and migration add_floating_ip_list = [{'id': 'fip2', 'router_id': 'r1', 'floating_ip_address': '90.0.0.12', 'fixed_ip_address': '172.16.2.12'}, {'id': 'fip3', 'router_id': 'r2', 'floating_ip_address': '100.0.0.10', 'fixed_ip_address': '192.168.2.10'}, {'id': 'fip4', 'router_id': 'r2', 'floating_ip_address': '100.0.0.11', 'fixed_ip_address': '192.168.2.11'}] del_floating_ip_list = [{'logical_ip': '172.16.1.11', 'external_ip': '90.0.0.11', 'type': 'dnat_and_snat'}, {'logical_ip': '192.168.2.11', 'external_ip': '100.0.0.11', 'type': 'dnat_and_snat', 'external_mac': '01:02:03:04:05:06', 'logical_port': 'vm1'}] del_router_list = [{'router': 'neutron-r3'}] del_router_port_list = [{'id': 'lrp-p3r1', 'router': 'neutron-r1'}] create_router_port_list = self.get_sync_router_ports[:2] update_router_port_list = [self.get_sync_router_ports[2]] update_router_port_list[0].update( {'networks': self.lrport_networks}) add_address_set_list = [ {'external_ids': {ovn_const.OVN_SG_EXT_ID_KEY: 'sg1'}, 'name': 'as_ip6_sg1', 'addresses': ['fd79:e1c:a55::816:eff:eff:ff2']}] del_address_set_list = [{'name': 'as_ip4_del'}] update_address_set_list = [ {'addrs_remove': [], 'addrs_add': ['10.0.0.4'], 'name': 'as_ip4_sg2'}, {'addrs_remove': ['fd79:e1c:a55::816:eff:eff:ff3'], 'addrs_add': [], 'name': 'as_ip6_sg2'}] add_subnet_dhcp_options_list = [(self.subnets[2], self.networks[1]), (self.subnets[1], self.networks[0])] delete_dhcp_options_list = ['UUID2', 'UUID4', 'UUID5'] ovn_nb_synchronizer = ovn_db_sync.OvnNbSynchronizer( self.plugin, self.mech_driver._nb_ovn, self.mech_driver._sb_ovn, 'repair', self.mech_driver) self._test_ovn_nb_sync_helper(ovn_nb_synchronizer, self.networks, self.ports, self.routers, self.get_sync_router_ports, create_router_list, create_router_port_list, update_router_port_list, del_router_list, del_router_port_list, create_network_list, create_port_list, create_provnet_port_list, del_network_list, del_port_list, add_static_route_list, del_static_route_list, add_snat_list, del_snat_list, add_floating_ip_list, del_floating_ip_list, add_address_set_list, del_address_set_list, update_address_set_list, add_subnet_dhcp_options_list, delete_dhcp_options_list) def test_ovn_nb_sync_mode_log(self): create_network_list = [] create_port_list = [] create_provnet_port_list = [] del_network_list = [] del_port_list = [] create_router_list = [] create_router_port_list = [] update_router_port_list = [] del_router_list = [] del_router_port_list = [] add_static_route_list = [] del_static_route_list = [] add_snat_list = [] del_snat_list = [] add_floating_ip_list = [] del_floating_ip_list = [] add_address_set_list = [] del_address_set_list = [] update_address_set_list = [] add_subnet_dhcp_options_list = [] delete_dhcp_options_list = [] ovn_nb_synchronizer = ovn_db_sync.OvnNbSynchronizer( self.plugin, self.mech_driver._nb_ovn, self.mech_driver._sb_ovn, 'log', self.mech_driver) self._test_ovn_nb_sync_helper(ovn_nb_synchronizer, self.networks, self.ports, self.routers, self.get_sync_router_ports, create_router_list, create_router_port_list, update_router_port_list, del_router_list, del_router_port_list, create_network_list, create_port_list, create_provnet_port_list, del_network_list, del_port_list, add_static_route_list, del_static_route_list, add_snat_list, del_snat_list, add_floating_ip_list, del_floating_ip_list, add_address_set_list, del_address_set_list, update_address_set_list, add_subnet_dhcp_options_list, delete_dhcp_options_list) class TestOvnSbSyncML2(test_mech_driver.OVNMechanismDriverTestCase): def test_ovn_sb_sync(self): ovn_sb_synchronizer = ovn_db_sync.OvnSbSynchronizer( self.plugin, self.mech_driver._sb_ovn, self.mech_driver) ovn_api = ovn_sb_synchronizer.ovn_api hostname_with_physnets = {'hostname1': ['physnet1', 'physnet2'], 'hostname2': ['physnet1']} ovn_api.get_chassis_hostname_and_physnets.return_value = ( hostname_with_physnets) ovn_driver = ovn_sb_synchronizer.ovn_driver ovn_driver.update_segment_host_mapping = mock.Mock() hosts_in_neutron = {'hostname2', 'hostname3'} with mock.patch.object(ovn_db_sync.segments_db, 'get_hosts_mapped_with_segments', return_value=hosts_in_neutron): ovn_sb_synchronizer.sync_hostname_and_physical_networks(mock.ANY) all_hosts = set(hostname_with_physnets.keys()) | hosts_in_neutron self.assertEqual( len(all_hosts), ovn_driver.update_segment_host_mapping.call_count) update_segment_host_mapping_calls = [mock.call( host, hostname_with_physnets[host]) for host in hostname_with_physnets] update_segment_host_mapping_calls += [ mock.call(host, []) for host in hosts_in_neutron - set(hostname_with_physnets.keys())] ovn_driver.update_segment_host_mapping.assert_has_calls( update_segment_host_mapping_calls, any_order=True) networking-ovn-4.0.0/networking_ovn/tests/unit/ml2/0000775000175100017510000000000013245511554022417 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/ml2/test_mech_driver.py0000666000175100017510000032167013245511145026326 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 mock from webob import exc from neutron.services.revisions import revision_plugin from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as const from neutron_lib import context from neutron_lib import exceptions as n_exc from neutron_lib.plugins import directory from neutron_lib.utils import net as n_net from oslo_config import cfg from oslo_db import exception as os_db_exc from oslo_serialization import jsonutils from neutron.db import provisioning_blocks from neutron.plugins.ml2.drivers import type_geneve # noqa from neutron.tests import tools from neutron.tests.unit.extensions import test_segment from neutron.tests.unit.plugins.ml2 import test_ext_portsecurity from neutron.tests.unit.plugins.ml2 import test_plugin from neutron.tests.unit.plugins.ml2 import test_security_group from networking_ovn.common import acl as ovn_acl from networking_ovn.common import config as ovn_config from networking_ovn.common import constants as ovn_const from networking_ovn.common import ovn_client from networking_ovn.common import utils as ovn_utils from networking_ovn.db import revision as db_rev from networking_ovn.ml2 import mech_driver from networking_ovn.tests.unit import fakes class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase): _mechanism_drivers = ['logger', 'ovn'] _extension_drivers = ['port_security', 'dns'] def setUp(self): cfg.CONF.set_override('extension_drivers', self._extension_drivers, group='ml2') cfg.CONF.set_override('tenant_network_types', ['geneve'], group='ml2') cfg.CONF.set_override('vni_ranges', ['1:65536'], group='ml2_type_geneve') ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', False, group='ovn') super(TestOVNMechanismDriver, self).setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj self.mech_driver._nb_ovn = fakes.FakeOvsdbNbOvnIdl() self.mech_driver._sb_ovn = fakes.FakeOvsdbSbOvnIdl() self.nb_ovn = self.mech_driver._nb_ovn self.sb_ovn = self.mech_driver._sb_ovn self.fake_subnet = fakes.FakeSubnet.create_one_subnet().info() self.fake_port_no_sg = fakes.FakePort.create_one_port().info() self.fake_sg_rule = \ fakes.FakeSecurityGroupRule.create_one_security_group_rule().info() self.fake_sg = fakes.FakeSecurityGroup.create_one_security_group( attrs={'security_group_rules': [self.fake_sg_rule]} ).info() self.sg_cache = {self.fake_sg['id']: self.fake_sg} self.subnet_cache = {self.fake_subnet['id']: self.fake_subnet} mock.patch( "networking_ovn.common.acl._acl_columns_name_severity_supported", return_value=True ).start() revision_plugin.RevisionPlugin() p = mock.patch.object(ovn_utils, 'get_revision_number', return_value=1) p.start() self.addCleanup(p.stop) p = mock.patch.object(db_rev, 'bump_revision') p.start() self.addCleanup(p.stop) @mock.patch.object(db_rev, 'bump_revision') def test__create_security_group(self, mock_bump): self.mech_driver._create_security_group( resources.SECURITY_GROUP, events.AFTER_CREATE, {}, security_group=self.fake_sg) external_ids = {ovn_const.OVN_SG_EXT_ID_KEY: self.fake_sg['id']} ip4_name = ovn_utils.ovn_addrset_name(self.fake_sg['id'], 'ip4') ip6_name = ovn_utils.ovn_addrset_name(self.fake_sg['id'], 'ip6') create_address_set_calls = [mock.call(name=name, external_ids=external_ids) for name in [ip4_name, ip6_name]] self.nb_ovn.create_address_set.assert_has_calls( create_address_set_calls, any_order=True) mock_bump.assert_called_once_with( self.fake_sg, ovn_const.TYPE_SECURITY_GROUPS) def test__delete_security_group(self): self.mech_driver._delete_security_group( resources.SECURITY_GROUP, events.AFTER_CREATE, {}, security_group_id=self.fake_sg['id']) ip4_name = ovn_utils.ovn_addrset_name(self.fake_sg['id'], 'ip4') ip6_name = ovn_utils.ovn_addrset_name(self.fake_sg['id'], 'ip6') delete_address_set_calls = [mock.call(name=name) for name in [ip4_name, ip6_name]] self.nb_ovn.delete_address_set.assert_has_calls( delete_address_set_calls, any_order=True) @mock.patch.object(db_rev, 'bump_revision') def test__process_sg_rule_notifications_sgr_create(self, mock_bump): with mock.patch( 'networking_ovn.common.acl.update_acls_for_security_group' ) as ovn_acl_up: rule = {'security_group_id': 'sg_id'} self.mech_driver._process_sg_rule_notification( resources.SECURITY_GROUP_RULE, events.AFTER_CREATE, {}, security_group_rule=rule) ovn_acl_up.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, 'sg_id', rule, is_add_acl=True) mock_bump.assert_called_once_with( rule, ovn_const.TYPE_SECURITY_GROUP_RULES) @mock.patch.object(db_rev, 'delete_revision') def test_process_sg_rule_notifications_sgr_delete(self, mock_delrev): rule = {'id': 'sgr_id', 'security_group_id': 'sg_id'} with mock.patch( 'networking_ovn.common.acl.update_acls_for_security_group' ) as ovn_acl_up: with mock.patch( 'neutron.db.securitygroups_db.' 'SecurityGroupDbMixin.get_security_group_rule', return_value=rule ): self.mech_driver._process_sg_rule_notification( resources.SECURITY_GROUP_RULE, events.BEFORE_DELETE, {}, security_group_rule=rule) ovn_acl_up.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, 'sg_id', rule, is_add_acl=False) mock_delrev.assert_called_once_with( rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES) def test_add_acls_no_sec_group(self): acls = ovn_acl.add_acls(self.mech_driver._plugin, mock.Mock(), self.fake_port_no_sg, {}, {}, self.mech_driver._nb_ovn) self.assertEqual([], acls) def _test_add_acls_with_sec_group_helper(self, native_dhcp=True): fake_port_sg = fakes.FakePort.create_one_port( attrs={'security_groups': [self.fake_sg['id']], 'fixed_ips': [{'subnet_id': self.fake_subnet['id'], 'ip_address': '10.10.10.20'}]} ).info() expected_acls = [] expected_acls += ovn_acl.drop_all_ip_traffic_for_port( fake_port_sg) expected_acls += ovn_acl.add_acl_dhcp( fake_port_sg, self.fake_subnet, native_dhcp) sg_rule_acl = ovn_acl.add_sg_rule_acl_for_port( fake_port_sg, self.fake_sg_rule, 'outport == "' + fake_port_sg['id'] + '" ' + '&& ip4 && ip4.src == 0.0.0.0/0 ' + '&& tcp && tcp.dst == 22') expected_acls.append(sg_rule_acl) # Test with caches acls = ovn_acl.add_acls(self.mech_driver._plugin, mock.Mock(), fake_port_sg, self.sg_cache, self.subnet_cache, self.mech_driver._nb_ovn) self.assertEqual(expected_acls, acls) # Test without caches with mock.patch('neutron.db.db_base_plugin_v2.' 'NeutronDbPluginV2.get_subnet', return_value=self.fake_subnet), \ mock.patch('neutron.db.securitygroups_db.' 'SecurityGroupDbMixin.get_security_group', return_value=self.fake_sg): acls = ovn_acl.add_acls(self.mech_driver._plugin, mock.Mock(), fake_port_sg, {}, {}, self.mech_driver._nb_ovn) self.assertEqual(expected_acls, acls) # Test with security groups disabled with mock.patch('networking_ovn.common.acl.is_sg_enabled', return_value=False): acls = ovn_acl.add_acls(self.mech_driver._plugin, mock.Mock(), fake_port_sg, self.sg_cache, self.subnet_cache, self.mech_driver._nb_ovn) self.assertEqual([], acls) # Test with multiple fixed IPs on the same subnet. fake_port_sg['fixed_ips'].append({'subnet_id': self.fake_subnet['id'], 'ip_address': '10.10.10.21'}) acls = ovn_acl.add_acls(self.mech_driver._plugin, mock.Mock(), fake_port_sg, self.sg_cache, self.subnet_cache, self.mech_driver._nb_ovn) self.assertEqual(expected_acls, acls) def test_add_acls_with_sec_group_native_dhcp_enabled(self): self._test_add_acls_with_sec_group_helper() def test_port_invalid_binding_profile(self): invalid_binding_profiles = [ {'tag': 0, 'parent_name': 'fakename'}, {'tag': 1024}, {'tag': 1024, 'parent_name': 1024}, {'parent_name': 'test'}, {'tag': 'test'}, {'vtep-physical-switch': 'psw1'}, {'vtep-logical-switch': 'lsw1'}, {'vtep-physical-switch': 'psw1', 'vtep-logical-switch': 1234}, {'vtep-physical-switch': 1234, 'vtep-logical-switch': 'lsw1'}, {'vtep-physical-switch': 'psw1', 'vtep-logical-switch': 'lsw1', 'tag': 1024}, {'vtep-physical-switch': 'psw1', 'vtep-logical-switch': 'lsw1', 'parent_name': 'fakename'}, {'vtep-physical-switch': 'psw1', 'vtep-logical-switch': 'lsw1', 'tag': 1024, 'parent_name': 'fakename'}, ] with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: # succeed without binding:profile with self.port(subnet=subnet1, set_context=True, tenant_id='test'): pass # fail with invalid binding profiles for invalid_profile in invalid_binding_profiles: try: kwargs = {ovn_const.OVN_PORT_BINDING_PROFILE: invalid_profile} with self.port( subnet=subnet1, expected_res_status=403, arg_list=( ovn_const.OVN_PORT_BINDING_PROFILE,), set_context=True, tenant_id='test', **kwargs): pass except exc.HTTPClientError: pass def test__validate_ignored_port_update_from_fip_port(self): p = {'id': 'id', 'device_owner': 'test'} ori_p = {'id': 'id', 'device_owner': const.DEVICE_OWNER_FLOATINGIP} self.assertRaises(mech_driver.OVNPortUpdateError, self.mech_driver._validate_ignored_port, p, ori_p) def test__validate_ignored_port_update_to_fip_port(self): p = {'id': 'id', 'device_owner': const.DEVICE_OWNER_FLOATINGIP} ori_p = {'id': 'port-id', 'device_owner': 'test'} self.assertRaises(mech_driver.OVNPortUpdateError, self.mech_driver._validate_ignored_port, p, ori_p) def test_create_and_update_ignored_fip_port(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, device_owner=const.DEVICE_OWNER_FLOATINGIP, set_context=True, tenant_id='test') as port: self.nb_ovn.create_lswitch_port.assert_not_called() data = {'port': {'name': 'new'}} req = self.new_update_request('ports', data, port['port']['id']) res = req.get_response(self.api) self.assertEqual(exc.HTTPOk.code, res.status_int) self.nb_ovn.set_lswitch_port.assert_not_called() def test_update_ignored_port_from_fip_device_owner(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, device_owner=const.DEVICE_OWNER_FLOATINGIP, set_context=True, tenant_id='test') as port: self.nb_ovn.create_lswitch_port.assert_not_called() data = {'port': {'device_owner': 'test'}} req = self.new_update_request('ports', data, port['port']['id']) res = req.get_response(self.api) self.assertEqual(exc.HTTPBadRequest.code, res.status_int) msg = jsonutils.loads(res.body)['NeutronError']['message'] expect_msg = ('Bad port request: Updating device_owner for' ' port %s owned by network:floatingip is' ' not supported.' % port['port']['id']) self.assertEqual(msg, expect_msg) self.nb_ovn.set_lswitch_port.assert_not_called() def test_update_ignored_port_to_fip_device_owner(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, device_owner='test', set_context=True, tenant_id='test') as port: self.assertEqual( 1, self.nb_ovn.create_lswitch_port.call_count) data = {'port': {'device_owner': const.DEVICE_OWNER_FLOATINGIP}} req = self.new_update_request('ports', data, port['port']['id']) res = req.get_response(self.api) self.assertEqual(exc.HTTPBadRequest.code, res.status_int) msg = jsonutils.loads(res.body)['NeutronError']['message'] expect_msg = ('Bad port request: Updating device_owner to' ' network:floatingip for port %s is' ' not supported.' % port['port']['id']) self.assertEqual(msg, expect_msg) self.nb_ovn.set_lswitch_port.assert_not_called() def test_create_port_security(self): kwargs = {'mac_address': '00:00:00:00:00:01', 'fixed_ips': [{'ip_address': '10.0.0.2'}, {'ip_address': '10.0.0.4'}]} with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, arg_list=('mac_address', 'fixed_ips'), set_context=True, tenant_id='test', **kwargs) as port: self.assertTrue(self.nb_ovn.create_lswitch_port.called) called_args_dict = ( (self.nb_ovn.create_lswitch_port ).call_args_list[0][1]) self.assertEqual(['00:00:00:00:00:01 10.0.0.2 10.0.0.4'], called_args_dict.get('port_security')) data = {'port': {'mac_address': '00:00:00:00:00:02'}} req = self.new_update_request( 'ports', data, port['port']['id']) req.get_response(self.api) self.assertTrue(self.nb_ovn.set_lswitch_port.called) called_args_dict = ( (self.nb_ovn.set_lswitch_port ).call_args_list[0][1]) self.assertEqual(['00:00:00:00:00:02 10.0.0.2 10.0.0.4'], called_args_dict.get('port_security')) def test_create_port_with_disabled_security(self): kwargs = {'port_security_enabled': False} with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, arg_list=('port_security_enabled',), set_context=True, tenant_id='test', **kwargs) as port: self.assertTrue(self.nb_ovn.create_lswitch_port.called) called_args_dict = ( (self.nb_ovn.create_lswitch_port ).call_args_list[0][1]) self.assertEqual([], called_args_dict.get('port_security')) data = {'port': {'mac_address': '00:00:00:00:00:01'}} req = self.new_update_request( 'ports', data, port['port']['id']) req.get_response(self.api) self.assertTrue(self.nb_ovn.set_lswitch_port.called) called_args_dict = ( (self.nb_ovn.set_lswitch_port ).call_args_list[0][1]) self.assertEqual([], called_args_dict.get('port_security')) def test_create_port_security_allowed_address_pairs(self): kwargs = {'allowed_address_pairs': [{"ip_address": "1.1.1.1"}, {"ip_address": "2.2.2.2", "mac_address": "22:22:22:22:22:22"}]} with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, arg_list=('allowed_address_pairs',), set_context=True, tenant_id='test', **kwargs) as port: port_ip = port['port'].get('fixed_ips')[0]['ip_address'] self.assertTrue(self.nb_ovn.create_lswitch_port.called) called_args_dict = ( (self.nb_ovn.create_lswitch_port ).call_args_list[0][1]) self.assertEqual( tools.UnorderedList( ["22:22:22:22:22:22 2.2.2.2", port['port']['mac_address'] + ' ' + port_ip + ' ' + '1.1.1.1']), called_args_dict.get('port_security')) self.assertEqual( tools.UnorderedList( ["22:22:22:22:22:22", port['port']['mac_address'] + ' ' + port_ip]), called_args_dict.get('addresses')) old_mac = port['port']['mac_address'] # we are updating only the port mac address. So the # mac address of the allowed address pair ip 1.1.1.1 # will have old mac address data = {'port': {'mac_address': '00:00:00:00:00:01'}} req = self.new_update_request( 'ports', data, port['port']['id']) req.get_response(self.api) self.assertTrue(self.nb_ovn.set_lswitch_port.called) called_args_dict = ( (self.nb_ovn.set_lswitch_port ).call_args_list[0][1]) self.assertEqual(tools.UnorderedList( ["22:22:22:22:22:22 2.2.2.2", "00:00:00:00:00:01 " + port_ip, old_mac + " 1.1.1.1"]), called_args_dict.get('port_security')) self.assertEqual( tools.UnorderedList( ["22:22:22:22:22:22", "00:00:00:00:00:01 " + port_ip, old_mac]), called_args_dict.get('addresses')) def _create_fake_network_context(self, network_type, physical_network=None, segmentation_id=None): network_attrs = {'provider:network_type': network_type, 'provider:physical_network': physical_network, 'provider:segmentation_id': segmentation_id} segment_attrs = {'network_type': network_type, 'physical_network': physical_network, 'segmentation_id': segmentation_id} fake_network = \ fakes.FakeNetwork.create_one_network(attrs=network_attrs).info() fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] return fakes.FakeNetworkContext(fake_network, fake_segments) def _create_fake_mp_network_context(self): network_type = 'flat' network_attrs = {'segments': []} fake_segments = [] for physical_network in ['physnet1', 'physnet2']: network_attrs['segments'].append( {'provider:network_type': network_type, 'provider:physical_network': physical_network}) segment_attrs = {'network_type': network_type, 'physical_network': physical_network} fake_segments.append( fakes.FakeSegment.create_one_segment( attrs=segment_attrs).info()) fake_network = \ fakes.FakeNetwork.create_one_network(attrs=network_attrs).info() fake_network.pop('provider:network_type') fake_network.pop('provider:physical_network') fake_network.pop('provider:segmentation_id') return fakes.FakeNetworkContext(fake_network, fake_segments) def test_network_precommit(self): # Test supported network types. fake_network_context = self._create_fake_network_context('local') self.mech_driver.create_network_precommit(fake_network_context) fake_network_context = self._create_fake_network_context( 'flat', physical_network='physnet') self.mech_driver.update_network_precommit(fake_network_context) fake_network_context = self._create_fake_network_context( 'geneve', segmentation_id=10) self.mech_driver.create_network_precommit(fake_network_context) fake_network_context = self._create_fake_network_context( 'vlan', physical_network='physnet', segmentation_id=11) self.mech_driver.update_network_precommit(fake_network_context) fake_mp_network_context = self._create_fake_mp_network_context() self.mech_driver.create_network_precommit(fake_mp_network_context) # Test unsupported network types. fake_network_context = self._create_fake_network_context( 'vxlan', segmentation_id=12) self.assertRaises(n_exc.InvalidInput, self.mech_driver.create_network_precommit, fake_network_context) fake_network_context = self._create_fake_network_context( 'gre', segmentation_id=13) self.assertRaises(n_exc.InvalidInput, self.mech_driver.update_network_precommit, fake_network_context) def test_create_port_without_security_groups(self): kwargs = {'security_groups': []} with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, arg_list=('security_groups',), set_context=True, tenant_id='test', **kwargs): self.assertEqual( 1, self.nb_ovn.create_lswitch_port.call_count) self.nb_ovn.add_acl.assert_not_called() self.nb_ovn.update_address_set.assert_not_called() def _test_create_port_with_security_groups_helper(self, add_acl_call_count): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, set_context=True, tenant_id='test'): self.assertEqual( 1, self.nb_ovn.create_lswitch_port.call_count) self.assertEqual( add_acl_call_count, self.nb_ovn.add_acl.call_count) self.assertEqual( 1, self.nb_ovn.update_address_set.call_count) def test_create_port_with_security_groups_native_dhcp_enabled(self): self._test_create_port_with_security_groups_helper(7) def test_update_port_changed_security_groups(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1: sg_id = port1['port']['security_groups'][0] fake_lsp = ( fakes.FakeOVNPort.from_neutron_port( port1['port'])) self.nb_ovn.lookup.return_value = fake_lsp # Remove the default security group. self.nb_ovn.set_lswitch_port.reset_mock() self.nb_ovn.update_acls.reset_mock() self.nb_ovn.update_address_set.reset_mock() data = {'port': {'security_groups': []}} self._update('ports', port1['port']['id'], data) self.assertEqual( 1, self.nb_ovn.set_lswitch_port.call_count) self.assertEqual( 1, self.nb_ovn.update_acls.call_count) self.assertEqual( 1, self.nb_ovn.update_address_set.call_count) # Add the default security group. self.nb_ovn.set_lswitch_port.reset_mock() self.nb_ovn.update_acls.reset_mock() self.nb_ovn.update_address_set.reset_mock() fake_lsp.external_ids.pop(ovn_const.OVN_SG_IDS_EXT_ID_KEY) data = {'port': {'security_groups': [sg_id]}} self._update('ports', port1['port']['id'], data) self.assertEqual( 1, self.nb_ovn.set_lswitch_port.call_count) self.assertEqual( 1, self.nb_ovn.update_acls.call_count) self.assertEqual( 1, self.nb_ovn.update_address_set.call_count) def test_update_port_unchanged_security_groups(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1: fake_lsp = ( fakes.FakeOVNPort.from_neutron_port( port1['port'])) self.nb_ovn.lookup.return_value = fake_lsp # Update the port name. self.nb_ovn.set_lswitch_port.reset_mock() self.nb_ovn.update_acls.reset_mock() self.nb_ovn.update_address_set.reset_mock() data = {'port': {'name': 'rtheis'}} self._update('ports', port1['port']['id'], data) self.assertEqual( 1, self.nb_ovn.set_lswitch_port.call_count) self.nb_ovn.update_acls.assert_not_called() self.nb_ovn.update_address_set.assert_not_called() # Update the port fixed IPs self.nb_ovn.set_lswitch_port.reset_mock() self.nb_ovn.update_acls.reset_mock() self.nb_ovn.update_address_set.reset_mock() data = {'port': {'fixed_ips': []}} self._update('ports', port1['port']['id'], data) self.assertEqual( 1, self.nb_ovn.set_lswitch_port.call_count) self.assertEqual( 1, self.nb_ovn.update_acls.call_count) self.assertEqual( 1, self.nb_ovn.update_address_set.call_count) def test_delete_port_without_security_groups(self): kwargs = {'security_groups': []} with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, arg_list=('security_groups',), set_context=True, tenant_id='test', **kwargs) as port1: fake_lsp = ( fakes.FakeOVNPort.from_neutron_port( port1['port'])) self.nb_ovn.lookup.return_value = fake_lsp self.nb_ovn.delete_lswitch_port.reset_mock() self.nb_ovn.delete_acl.reset_mock() self.nb_ovn.update_address_set.reset_mock() self._delete('ports', port1['port']['id']) self.assertEqual( 1, self.nb_ovn.delete_lswitch_port.call_count) self.assertEqual( 1, self.nb_ovn.delete_acl.call_count) self.nb_ovn.update_address_set.assert_not_called() def test_delete_port_with_security_groups(self): with self.network(set_context=True, tenant_id='test') as net1: with self.subnet(network=net1) as subnet1: with self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1: fake_lsp = ( fakes.FakeOVNPort.from_neutron_port( port1['port'])) self.nb_ovn.lookup.return_value = fake_lsp self.nb_ovn.delete_lswitch_port.reset_mock() self.nb_ovn.delete_acl.reset_mock() self.nb_ovn.update_address_set.reset_mock() self._delete('ports', port1['port']['id']) self.assertEqual( 1, self.nb_ovn.delete_lswitch_port.call_count) self.assertEqual( 1, self.nb_ovn.delete_acl.call_count) self.assertEqual( 1, self.nb_ovn.update_address_set.call_count) def test_set_port_status_up(self): with self.network(set_context=True, tenant_id='test') as net1, \ self.subnet(network=net1) as subnet1, \ self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1, \ mock.patch('neutron.db.provisioning_blocks.' 'provisioning_complete') as pc, \ mock.patch.object(self.mech_driver, '_update_subport_host_if_needed') as upd_subport: self.mech_driver.set_port_status_up(port1['port']['id']) pc.assert_called_once_with( mock.ANY, port1['port']['id'], resources.PORT, provisioning_blocks.L2_AGENT_ENTITY ) upd_subport.assert_called_once_with(port1['port']['id']) def test_set_port_status_down(self): with self.network(set_context=True, tenant_id='test') as net1, \ self.subnet(network=net1) as subnet1, \ self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1, \ mock.patch('neutron.db.provisioning_blocks.' 'add_provisioning_component') as apc: self.mech_driver.set_port_status_down(port1['port']['id']) apc.assert_called_once_with( mock.ANY, port1['port']['id'], resources.PORT, provisioning_blocks.L2_AGENT_ENTITY ) def test_set_port_status_down_not_found(self): with mock.patch('neutron.db.provisioning_blocks.' 'add_provisioning_component') as apc: self.mech_driver.set_port_status_down('foo') apc.assert_not_called() def test_set_port_status_concurrent_delete(self): exc = os_db_exc.DBReferenceError('', '', '', '') with self.network(set_context=True, tenant_id='test') as net1, \ self.subnet(network=net1) as subnet1, \ self.port(subnet=subnet1, set_context=True, tenant_id='test') as port1, \ mock.patch('neutron.db.provisioning_blocks.' 'add_provisioning_component', side_effect=exc) as apc: self.mech_driver.set_port_status_down(port1['port']['id']) apc.assert_called_once_with( mock.ANY, port1['port']['id'], resources.PORT, provisioning_blocks.L2_AGENT_ENTITY ) def test__update_subport_host_if_needed(self): """Check that a subport is updated with parent's host_id.""" binding_host_id = {'binding:host_id': 'hostname'} with mock.patch.object(self.mech_driver._ovn_client, 'get_parent_port', return_value='parent'), \ mock.patch.object(self.mech_driver._plugin, 'get_port', return_value=binding_host_id) as get_port, \ mock.patch.object(self.mech_driver._plugin, 'update_port') as upd: self.mech_driver._update_subport_host_if_needed('subport') get_port.assert_called_once_with(mock.ANY, 'parent') upd.assert_called_once_with(mock.ANY, 'subport', {'port': binding_host_id}) def test_bind_port_unsupported_vnic_type(self): fake_port = fakes.FakePort.create_one_port( attrs={'binding:vnic_type': 'unknown'}).info() fake_port_context = fakes.FakePortContext(fake_port, 'host', []) self.mech_driver.bind_port(fake_port_context) self.sb_ovn.get_chassis_data_for_ml2_bind_port.assert_not_called() fake_port_context.set_binding.assert_not_called() def _test_bind_port_failed(self, fake_segments): fake_port = fakes.FakePort.create_one_port().info() fake_host = 'host' fake_port_context = fakes.FakePortContext( fake_port, fake_host, fake_segments) self.mech_driver.bind_port(fake_port_context) self.sb_ovn.get_chassis_data_for_ml2_bind_port.assert_called_once_with( fake_host) fake_port_context.set_binding.assert_not_called() def test_bind_port_host_not_found(self): self.sb_ovn.get_chassis_data_for_ml2_bind_port.side_effect = \ RuntimeError self._test_bind_port_failed([]) def test_bind_port_no_segments_to_bind(self): self._test_bind_port_failed([]) def test_bind_port_physnet_not_found(self): segment_attrs = {'network_type': 'vlan', 'physical_network': 'unknown-physnet', 'segmentation_id': 23} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port_failed(fake_segments) def _test_bind_port(self, fake_segments): fake_port = fakes.FakePort.create_one_port().info() fake_host = 'host' fake_port_context = fakes.FakePortContext( fake_port, fake_host, fake_segments) self.mech_driver.bind_port(fake_port_context) self.sb_ovn.get_chassis_data_for_ml2_bind_port.assert_called_once_with( fake_host) fake_port_context.set_binding.assert_called_once_with( fake_segments[0]['id'], portbindings.VIF_TYPE_OVS, self.mech_driver.vif_details[portbindings.VIF_TYPE_OVS]) def _test_bind_port_sriov(self, fake_segments): fake_port = fakes.FakePort.create_one_port( attrs={'binding:vnic_type': 'direct', 'binding:profile': {'capabilities': ['switchdev']}}).info() fake_host = 'host' fake_port_context = fakes.FakePortContext( fake_port, fake_host, fake_segments) self.mech_driver.bind_port(fake_port_context) self.sb_ovn.get_chassis_data_for_ml2_bind_port.assert_called_once_with( fake_host) fake_port_context.set_binding.assert_called_once_with( fake_segments[0]['id'], portbindings.VIF_TYPE_OVS, self.mech_driver.vif_details[portbindings.VIF_TYPE_OVS]) def test_bind_port_geneve(self): segment_attrs = {'network_type': 'geneve', 'physical_network': None, 'segmentation_id': 1023} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port(fake_segments) def test_bind_sriov_port_geneve(self): """Test binding a SR-IOV port to a geneve segment.""" segment_attrs = {'network_type': 'geneve', 'physical_network': None, 'segmentation_id': 1023} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port_sriov(fake_segments) def test_bind_port_vlan(self): segment_attrs = {'network_type': 'vlan', 'physical_network': 'fake-physnet', 'segmentation_id': 23} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port(fake_segments) def test_bind_port_flat(self): segment_attrs = {'network_type': 'flat', 'physical_network': 'fake-physnet', 'segmentation_id': None} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port(fake_segments) def test_bind_port_vxlan(self): segment_attrs = {'network_type': 'vxlan', 'physical_network': None, 'segmentation_id': 1024} fake_segments = \ [fakes.FakeSegment.create_one_segment(attrs=segment_attrs).info()] self._test_bind_port(fake_segments) def test__is_port_provisioning_required(self): fake_port = fakes.FakePort.create_one_port( attrs={'binding:vnic_type': 'normal', 'status': const.PORT_STATUS_DOWN}).info() fake_host = 'fake-physnet' # Test host not changed self.assertFalse(self.mech_driver._is_port_provisioning_required( fake_port, fake_host, fake_host)) # Test invalid vnic type. fake_port['binding:vnic_type'] = 'unknown' self.assertFalse(self.mech_driver._is_port_provisioning_required( fake_port, fake_host, None)) fake_port['binding:vnic_type'] = 'normal' # Test invalid status. fake_port['status'] = const.PORT_STATUS_ACTIVE self.assertFalse(self.mech_driver._is_port_provisioning_required( fake_port, fake_host, None)) fake_port['status'] = const.PORT_STATUS_DOWN # Test no host. self.assertFalse(self.mech_driver._is_port_provisioning_required( fake_port, None, None)) # Test invalid host. self.sb_ovn.chassis_exists.return_value = False self.assertFalse(self.mech_driver._is_port_provisioning_required( fake_port, fake_host, None)) self.sb_ovn.chassis_exists.return_value = True # Test port provisioning required. self.assertTrue(self.mech_driver._is_port_provisioning_required( fake_port, fake_host, None)) def _test_add_subnet_dhcp_options_in_ovn(self, subnet, ovn_dhcp_opts=None, call_get_dhcp_opts=True, call_add_dhcp_opts=True): subnet['id'] = 'fake_id' with mock.patch.object(self.mech_driver._ovn_client, '_get_ovn_dhcp_options') as get_opts: self.mech_driver._ovn_client._add_subnet_dhcp_options( subnet, mock.ANY, ovn_dhcp_opts) self.assertEqual(call_get_dhcp_opts, get_opts.called) self.assertEqual( call_add_dhcp_opts, self.mech_driver._nb_ovn.add_dhcp_options.called) def test_add_subnet_dhcp_options_in_ovn(self): subnet = {'ip_version': const.IP_VERSION_4} self._test_add_subnet_dhcp_options_in_ovn(subnet) def test_add_subnet_dhcp_options_in_ovn_with_given_ovn_dhcp_opts(self): subnet = {'ip_version': const.IP_VERSION_4} self._test_add_subnet_dhcp_options_in_ovn( subnet, ovn_dhcp_opts={'foo': 'bar', 'external_ids': {}}, call_get_dhcp_opts=False) def test_add_subnet_dhcp_options_in_ovn_with_slaac_v6_subnet(self): subnet = {'ip_version': const.IP_VERSION_6, 'ipv6_address_mode': const.IPV6_SLAAC} self._test_add_subnet_dhcp_options_in_ovn( subnet, call_get_dhcp_opts=False, call_add_dhcp_opts=False) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_ports') @mock.patch('neutron_lib.utils.net.get_random_mac') def test_enable_subnet_dhcp_options_in_ovn_ipv4(self, grm, gps): grm.return_value = '01:02:03:04:05:06' gps.return_value = [ {'id': 'port-id-1', 'device_owner': 'nova:compute'}, {'id': 'port-id-2', 'device_owner': 'nova:compute', 'extra_dhcp_opts': [ {'opt_value': '10.0.0.33', 'ip_version': 4, 'opt_name': 'router'}]}, {'id': 'port-id-3', 'device_owner': 'nova:compute', 'extra_dhcp_opts': [ {'opt_value': '1200', 'ip_version': 4, 'opt_name': 'mtu'}]}, {'id': 'port-id-10', 'device_owner': 'network:foo'}] subnet = {'id': 'subnet-id', 'ip_version': 4, 'cidr': '10.0.0.0/24', 'network_id': 'network-id', 'gateway_ip': '10.0.0.1', 'enable_dhcp': True, 'dns_nameservers': [], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} txn = self.mech_driver._nb_ovn.transaction().__enter__.return_value dhcp_option_command = mock.Mock() txn.add.return_value = dhcp_option_command self.mech_driver._ovn_client._enable_subnet_dhcp_options( subnet, network, txn) # Check adding DHCP_Options rows subnet_dhcp_options = { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'cidr': subnet['cidr'], 'options': { 'router': subnet['gateway_ip'], 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1000)}} ports_dhcp_options = [{ 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', 'port_id': 'port-id-2'}, 'cidr': subnet['cidr'], 'options': { 'router': '10.0.0.33', 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1000)}}, { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', 'port_id': 'port-id-3'}, 'cidr': subnet['cidr'], 'options': { 'router': subnet['gateway_ip'], 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1200)}}] add_dhcp_calls = [mock.call('subnet-id', **subnet_dhcp_options)] add_dhcp_calls.extend([mock.call( 'subnet-id', port_id=port_dhcp_options['external_ids']['port_id'], **port_dhcp_options) for port_dhcp_options in ports_dhcp_options]) self.assertEqual(len(add_dhcp_calls), self.mech_driver._nb_ovn.add_dhcp_options.call_count) self.mech_driver._nb_ovn.add_dhcp_options.assert_has_calls( add_dhcp_calls, any_order=True) # Check setting lport rows set_lsp_calls = [mock.call(lport_name='port-id-1', dhcpv4_options=dhcp_option_command), mock.call(lport_name='port-id-2', dhcpv4_options=dhcp_option_command), mock.call(lport_name='port-id-3', dhcpv4_options=dhcp_option_command)] self.assertEqual(len(set_lsp_calls), self.mech_driver._nb_ovn.set_lswitch_port.call_count) self.mech_driver._nb_ovn.set_lswitch_port.assert_has_calls( set_lsp_calls, any_order=True) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_ports') @mock.patch('neutron_lib.utils.net.get_random_mac') def test_enable_subnet_dhcp_options_in_ovn_ipv6(self, grm, gps): grm.return_value = '01:02:03:04:05:06' gps.return_value = [ {'id': 'port-id-1', 'device_owner': 'nova:compute'}, {'id': 'port-id-2', 'device_owner': 'nova:compute', 'extra_dhcp_opts': [ {'opt_value': '11:22:33:44:55:66', 'ip_version': 6, 'opt_name': 'server-id'}]}, {'id': 'port-id-3', 'device_owner': 'nova:compute', 'extra_dhcp_opts': [ {'opt_value': '10::34', 'ip_version': 6, 'opt_name': 'dns-server'}]}, {'id': 'port-id-10', 'device_owner': 'network:foo'}] subnet = {'id': 'subnet-id', 'ip_version': 6, 'cidr': '10::0/64', 'gateway_ip': '10::1', 'enable_dhcp': True, 'ipv6_address_mode': 'dhcpv6-stateless', 'dns_nameservers': [], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} txn = self.mech_driver._nb_ovn.transaction().__enter__.return_value dhcp_option_command = mock.Mock() txn.add.return_value = dhcp_option_command self.mech_driver._ovn_client._enable_subnet_dhcp_options( subnet, network, txn) # Check adding DHCP_Options rows subnet_dhcp_options = { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'server_id': '01:02:03:04:05:06'}} ports_dhcp_options = [{ 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', 'port_id': 'port-id-2'}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'server_id': '11:22:33:44:55:66'}}, { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1', 'port_id': 'port-id-3'}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'server_id': '01:02:03:04:05:06', 'dns_server': '10::34'}}] add_dhcp_calls = [mock.call('subnet-id', **subnet_dhcp_options)] add_dhcp_calls.extend([mock.call( 'subnet-id', port_id=port_dhcp_options['external_ids']['port_id'], **port_dhcp_options) for port_dhcp_options in ports_dhcp_options]) self.assertEqual(len(add_dhcp_calls), self.mech_driver._nb_ovn.add_dhcp_options.call_count) self.mech_driver._nb_ovn.add_dhcp_options.assert_has_calls( add_dhcp_calls, any_order=True) # Check setting lport rows set_lsp_calls = [mock.call(lport_name='port-id-1', dhcpv6_options=dhcp_option_command), mock.call(lport_name='port-id-2', dhcpv6_options=dhcp_option_command), mock.call(lport_name='port-id-3', dhcpv6_options=dhcp_option_command)] self.assertEqual(len(set_lsp_calls), self.mech_driver._nb_ovn.set_lswitch_port.call_count) self.mech_driver._nb_ovn.set_lswitch_port.assert_has_calls( set_lsp_calls, any_order=True) def test_enable_subnet_dhcp_options_in_ovn_ipv6_slaac(self): subnet = {'id': 'subnet-id', 'ip_version': 6, 'enable_dhcp': True, 'ipv6_address_mode': 'slaac'} network = {'id': 'network-id'} self.mech_driver._ovn_client._enable_subnet_dhcp_options( subnet, network, mock.Mock()) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() self.mech_driver._nb_ovn.set_lswitch_port.assert_not_called() def _test_remove_subnet_dhcp_options_in_ovn(self, ip_version): opts = {'subnet': {'uuid': 'subnet-uuid'}, 'ports': [{'uuid': 'port1-uuid'}]} self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value = opts self.mech_driver._ovn_client._remove_subnet_dhcp_options( 'subnet-id', mock.Mock()) # Check deleting DHCP_Options rows delete_dhcp_calls = [mock.call('subnet-uuid'), mock.call('port1-uuid')] self.assertEqual( len(delete_dhcp_calls), self.mech_driver._nb_ovn.delete_dhcp_options.call_count) self.mech_driver._nb_ovn.delete_dhcp_options.assert_has_calls( delete_dhcp_calls, any_order=True) def test_remove_subnet_dhcp_options_in_ovn_ipv4(self): self._test_remove_subnet_dhcp_options_in_ovn(4) def test_remove_subnet_dhcp_options_in_ovn_ipv6(self): self._test_remove_subnet_dhcp_options_in_ovn(6) def test_update_subnet_dhcp_options_in_ovn_ipv4(self): subnet = {'id': 'subnet-id', 'ip_version': 4, 'cidr': '10.0.0.0/24', 'network_id': 'network-id', 'gateway_ip': '10.0.0.1', 'enable_dhcp': True, 'dns_nameservers': [], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} orignal_options = {'subnet': { 'external_ids': {'subnet_id': subnet['id']}, 'cidr': subnet['cidr'], 'options': { 'router': '10.0.0.2', 'server_id': '10.0.0.2', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1000)}}, 'ports': []} self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value =\ orignal_options self.mech_driver._ovn_client._update_subnet_dhcp_options( subnet, network, mock.Mock()) new_options = { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'cidr': subnet['cidr'], 'options': { 'router': subnet['gateway_ip'], 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1000)}} self.mech_driver._nb_ovn.add_dhcp_options.assert_called_once_with( subnet['id'], **new_options) def test_update_subnet_dhcp_options_in_ovn_ipv4_not_change(self): subnet = {'id': 'subnet-id', 'ip_version': 4, 'cidr': '10.0.0.0/24', 'network_id': 'network-id', 'gateway_ip': '10.0.0.1', 'enable_dhcp': True, 'dns_nameservers': [], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} orignal_options = {'subnet': { 'external_ids': {'subnet_id': subnet['id']}, 'cidr': subnet['cidr'], 'options': { 'router': subnet['gateway_ip'], 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(1000)}}, 'ports': []} self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value =\ orignal_options self.mech_driver._ovn_client._update_subnet_dhcp_options( subnet, network, mock.Mock()) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() def test_update_subnet_dhcp_options_in_ovn_ipv6(self): subnet = {'id': 'subnet-id', 'ip_version': 6, 'cidr': '10::0/64', 'network_id': 'network-id', 'gateway_ip': '10::1', 'enable_dhcp': True, 'ipv6_address_mode': 'dhcpv6-stateless', 'dns_nameservers': ['10::3'], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} orignal_options = {'subnet': { 'external_ids': {'subnet_id': subnet['id']}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'server_id': '01:02:03:04:05:06'}}, 'ports': []} self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value =\ orignal_options self.mech_driver._ovn_client._update_subnet_dhcp_options( subnet, network, mock.Mock()) new_options = { 'external_ids': {'subnet_id': subnet['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'dns_server': '{10::3}', 'server_id': '01:02:03:04:05:06'}} self.mech_driver._nb_ovn.add_dhcp_options.assert_called_once_with( subnet['id'], **new_options) def test_update_subnet_dhcp_options_in_ovn_ipv6_not_change(self): subnet = {'id': 'subnet-id', 'ip_version': 6, 'cidr': '10::0/64', 'gateway_ip': '10::1', 'enable_dhcp': True, 'ipv6_address_mode': 'dhcpv6-stateless', 'dns_nameservers': [], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1000} orignal_options = {'subnet': { 'external_ids': {'subnet_id': subnet['id']}, 'cidr': subnet['cidr'], 'options': { 'dhcpv6_stateless': 'true', 'server_id': '01:02:03:04:05:06'}}, 'ports': []} self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value =\ orignal_options self.mech_driver._ovn_client._update_subnet_dhcp_options( subnet, network, mock.Mock()) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() def test_update_subnet_dhcp_options_in_ovn_ipv6_slaac(self): subnet = {'id': 'subnet-id', 'ip_version': 6, 'enable_dhcp': True, 'ipv6_address_mode': 'slaac'} network = {'id': 'network-id'} self.mech_driver._ovn_client._update_subnet_dhcp_options( subnet, network, mock.Mock()) self.mech_driver._nb_ovn.get_subnet_dhcp_options.assert_not_called() self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() def test_update_subnet_postcommit_ovn_do_nothing(self): context = fakes.FakeSubnetContext( subnet={'enable_dhcp': False, 'ip_version': 4, 'network_id': 'id', 'id': 'subnet_id'}, network={'id': 'id'}) with mock.patch.object( self.mech_driver._ovn_client, '_enable_subnet_dhcp_options') as esd,\ mock.patch.object( self.mech_driver._ovn_client, '_remove_subnet_dhcp_options') as dsd,\ mock.patch.object( self.mech_driver._ovn_client, '_update_subnet_dhcp_options') as usd,\ mock.patch.object( self.mech_driver._ovn_client, '_find_metadata_port') as fmd,\ mock.patch.object( self.mech_driver._ovn_client, 'update_metadata_port') as umd: self.mech_driver.update_subnet_postcommit(context) esd.assert_not_called() dsd.assert_not_called() usd.assert_not_called() fmd.assert_not_called() umd.assert_not_called() def test_update_subnet_postcommit_enable_dhcp(self): context = fakes.FakeSubnetContext( subnet={'enable_dhcp': True, 'ip_version': 4, 'network_id': 'id', 'id': 'subnet_id'}, network={'id': 'id'}) with mock.patch.object( self.mech_driver._ovn_client, '_enable_subnet_dhcp_options') as esd,\ mock.patch.object( self.mech_driver._ovn_client, 'update_metadata_port') as umd: self.mech_driver.update_subnet_postcommit(context) esd.assert_called_once_with( context.current, context.network.current, mock.ANY) umd.assert_called_once_with(mock.ANY, 'id') def test_update_subnet_postcommit_disable_dhcp(self): self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value = { 'subnet': mock.sentinel.subnet, 'ports': []} context = fakes.FakeSubnetContext( subnet={'enable_dhcp': False, 'id': 'fake_id', 'ip_version': 4, 'network_id': 'id'}, network={'id': 'id'}) with mock.patch.object( self.mech_driver._ovn_client, '_remove_subnet_dhcp_options') as dsd,\ mock.patch.object( self.mech_driver._ovn_client, 'update_metadata_port') as umd: self.mech_driver.update_subnet_postcommit(context) dsd.assert_called_once_with(context.current['id'], mock.ANY) umd.assert_called_once_with(mock.ANY, 'id') def test_update_subnet_postcommit_update_dhcp(self): self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value = { 'subnet': mock.sentinel.subnet, 'ports': []} context = fakes.FakeSubnetContext( subnet={'enable_dhcp': True, 'ip_version': 4, 'network_id': 'id', 'id': 'subnet_id'}, network={'id': 'id'}) with mock.patch.object( self.mech_driver._ovn_client, '_update_subnet_dhcp_options') as usd,\ mock.patch.object( self.mech_driver._ovn_client, 'update_metadata_port') as umd: self.mech_driver.update_subnet_postcommit(context) usd.assert_called_once_with( context.current, context.network.current, mock.ANY) umd.assert_called_once_with(mock.ANY, 'id') @mock.patch.object(provisioning_blocks, 'is_object_blocked') @mock.patch.object(provisioning_blocks, 'provisioning_complete') def test_notify_dhcp_updated(self, mock_prov_complete, mock_is_obj_block): port_id = 'fake-port-id' mock_is_obj_block.return_value = True self.mech_driver._notify_dhcp_updated(port_id) mock_prov_complete.assert_called_once_with( mock.ANY, port_id, resources.PORT, provisioning_blocks.DHCP_ENTITY) mock_is_obj_block.return_value = False mock_prov_complete.reset_mock() self.mech_driver._notify_dhcp_updated(port_id) mock_prov_complete.assert_not_called() @mock.patch.object(mech_driver.OVNMechanismDriver, '_is_port_provisioning_required', lambda *_: True) @mock.patch.object(mech_driver.OVNMechanismDriver, '_notify_dhcp_updated') @mock.patch.object(ovn_client.OVNClient, 'create_port') def test_create_port_postcommit(self, mock_create_port, mock_notify_dhcp): fake_port = fakes.FakePort.create_one_port( attrs={'status': const.PORT_STATUS_DOWN}).info() fake_ctx = mock.Mock(current=fake_port) self.mech_driver.create_port_postcommit(fake_ctx) mock_create_port.assert_called_once_with(fake_port) mock_notify_dhcp.assert_called_once_with(fake_port['id']) @mock.patch.object(mech_driver.OVNMechanismDriver, '_is_port_provisioning_required', lambda *_: True) @mock.patch.object(mech_driver.OVNMechanismDriver, '_notify_dhcp_updated') @mock.patch.object(ovn_client.OVNClient, 'update_port') def test_update_port_postcommit(self, mock_update_port, mock_notify_dhcp): fake_port = fakes.FakePort.create_one_port( attrs={'status': const.PORT_STATUS_ACTIVE}).info() fake_ctx = mock.Mock(current=fake_port) self.mech_driver.update_port_postcommit(fake_ctx) mock_update_port.assert_called_once_with( fake_port, port_object=fake_ctx.original) mock_notify_dhcp.assert_called_once_with(fake_port['id']) class OVNMechanismDriverTestCase(test_plugin.Ml2PluginV2TestCase): _mechanism_drivers = ['logger', 'ovn'] def setUp(self): cfg.CONF.set_override('tenant_network_types', ['geneve'], group='ml2') cfg.CONF.set_override('vni_ranges', ['1:65536'], group='ml2_type_geneve') super(OVNMechanismDriverTestCase, self).setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj nb_ovn = fakes.FakeOvsdbNbOvnIdl() sb_ovn = fakes.FakeOvsdbSbOvnIdl() self.mech_driver._nb_ovn = nb_ovn self.mech_driver._sb_ovn = sb_ovn self.mech_driver._insert_port_provisioning_block = mock.Mock() p = mock.patch.object(ovn_utils, 'get_revision_number', return_value=1) p.start() self.addCleanup(p.stop) class TestOVNMechansimDriverBasicGet(test_plugin.TestMl2BasicGet, OVNMechanismDriverTestCase): pass class TestOVNMechansimDriverV2HTTPResponse(test_plugin.TestMl2V2HTTPResponse, OVNMechanismDriverTestCase): pass class TestOVNMechansimDriverNetworksV2(test_plugin.TestMl2NetworksV2, OVNMechanismDriverTestCase): pass class TestOVNMechansimDriverSubnetsV2(test_plugin.TestMl2SubnetsV2, OVNMechanismDriverTestCase): def setUp(self): # Disable metadata so that we don't interfere with existing tests # in Neutron tree. Doing this because some of the tests assume that # first IP address in a subnet will be available and this is not true # with metadata since it will book an IP address on each subnet. ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', False, group='ovn') super(TestOVNMechansimDriverSubnetsV2, self).setUp() # NOTE(rtheis): Mock the OVN port update since it is getting subnet # information for ACL processing. This interferes with the update_port # mock already done by the test. def test_subnet_update_ipv4_and_ipv6_pd_v6stateless_subnets(self): with mock.patch.object(self.mech_driver._ovn_client, 'update_port'),\ mock.patch.object(self.mech_driver._ovn_client, '_get_subnet_dhcp_options_for_port', return_value={}): super(TestOVNMechansimDriverSubnetsV2, self).\ test_subnet_update_ipv4_and_ipv6_pd_v6stateless_subnets() # NOTE(rtheis): Mock the OVN port update since it is getting subnet # information for ACL processing. This interferes with the update_port # mock already done by the test. def test_subnet_update_ipv4_and_ipv6_pd_slaac_subnets(self): with mock.patch.object(self.mech_driver._ovn_client, 'update_port'),\ mock.patch.object(self.mech_driver._ovn_client, '_get_subnet_dhcp_options_for_port', return_value={}): super(TestOVNMechansimDriverSubnetsV2, self).\ test_subnet_update_ipv4_and_ipv6_pd_slaac_subnets() # NOTE(numans) Overriding the base test case here because the base test # case creates a network with vxlan type and OVN mech driver doesn't # support it. def test_create_subnet_check_mtu_in_mech_context(self): plugin = directory.get_plugin() plugin.mechanism_manager.create_subnet_precommit = mock.Mock() net_arg = {pnet.NETWORK_TYPE: 'geneve', pnet.SEGMENTATION_ID: '1'} network = self._make_network(self.fmt, 'net1', True, arg_list=(pnet.NETWORK_TYPE, pnet.SEGMENTATION_ID,), **net_arg) with self.subnet(network=network): mock_subnet_pre = plugin.mechanism_manager.create_subnet_precommit observerd_mech_context = mock_subnet_pre.call_args_list[0][0][0] self.assertEqual(network['network']['mtu'], observerd_mech_context.network.current['mtu']) class TestOVNMechansimDriverPortsV2(test_plugin.TestMl2PortsV2, OVNMechanismDriverTestCase): def setUp(self): # Disable metadata so that we don't interfere with existing tests # in Neutron tree. Doing this because some of the tests assume that # first IP address in a subnet will be available and this is not true # with metadata since it will book an IP address on each subnet. ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', False, group='ovn') super(TestOVNMechansimDriverPortsV2, self).setUp() # NOTE(rtheis): Override this test to verify that updating # a port MAC fails when the port is bound. def test_update_port_mac(self): self.check_update_port_mac( host_arg={portbindings.HOST_ID: 'fake-host'}, arg_list=(portbindings.HOST_ID,), expected_status=exc.HTTPConflict.code, expected_error='PortBound') class TestOVNMechansimDriverAllowedAddressPairs( test_plugin.TestMl2AllowedAddressPairs, OVNMechanismDriverTestCase): pass class TestOVNMechansimDriverPortSecurity( test_ext_portsecurity.PSExtDriverTestCase, OVNMechanismDriverTestCase): pass class TestOVNMechansimDriverSegment(test_segment.HostSegmentMappingTestCase): _mechanism_drivers = ['logger', 'ovn'] def setUp(self): super(TestOVNMechansimDriverSegment, self).setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj nb_ovn = fakes.FakeOvsdbNbOvnIdl() sb_ovn = fakes.FakeOvsdbSbOvnIdl() self.mech_driver._nb_ovn = nb_ovn self.mech_driver._sb_ovn = sb_ovn p = mock.patch.object(ovn_utils, 'get_revision_number', return_value=1) p.start() self.addCleanup(p.stop) def _test_segment_host_mapping(self): # Disable the callback to update SegmentHostMapping by default, so # that update_segment_host_mapping is the only path to add the mapping registry.unsubscribe( self.mech_driver._add_segment_host_mapping_for_segment, resources.SEGMENT, events.PRECOMMIT_CREATE) host = 'hostname' with self.network() as network: network = network['network'] segment1 = self._test_create_segment( network_id=network['id'], physical_network='phys_net1', segmentation_id=200, network_type='vlan')['segment'] # As geneve networks mtu shouldn't be more than 1450, update it data = {'network': {'mtu': 1450}} req = self.new_update_request('networks', data, network['id']) res = self.deserialize(self.fmt, req.get_response(self.api)) self.assertEqual(1450, res['network']['mtu']) self._test_create_segment( network_id=network['id'], segmentation_id=200, network_type='geneve')['segment'] self.mech_driver.update_segment_host_mapping(host, ['phys_net1']) segments_host_db = self._get_segments_for_host(host) self.assertEqual({segment1['id']}, set(segments_host_db)) return network['id'], host def test_update_segment_host_mapping(self): network_id, host = self._test_segment_host_mapping() # Update the mapping segment2 = self._test_create_segment( network_id=network_id, physical_network='phys_net2', segmentation_id=201, network_type='vlan')['segment'] self.mech_driver.update_segment_host_mapping(host, ['phys_net2']) segments_host_db = self._get_segments_for_host(host) self.assertEqual({segment2['id']}, set(segments_host_db)) def test_clear_segment_host_mapping(self): _, host = self._test_segment_host_mapping() # Clear the mapping self.mech_driver.update_segment_host_mapping(host, []) segments_host_db = self._get_segments_for_host(host) self.assertEqual({}, segments_host_db) def test_update_segment_host_mapping_with_new_segment(self): hostname_with_physnets = {'hostname1': ['phys_net1', 'phys_net2'], 'hostname2': ['phys_net1']} ovn_sb_api = self.mech_driver._sb_ovn ovn_sb_api.get_chassis_hostname_and_physnets.return_value = ( hostname_with_physnets) self.mech_driver.subscribe() with self.network() as network: network_id = network['network']['id'] segment = self._test_create_segment( network_id=network_id, physical_network='phys_net2', segmentation_id=201, network_type='vlan')['segment'] segments_host_db1 = self._get_segments_for_host('hostname1') # A new SegmentHostMapping should be created for hostname1 self.assertEqual({segment['id']}, set(segments_host_db1)) segments_host_db2 = self._get_segments_for_host('hostname2') self.assertFalse(set(segments_host_db2)) @mock.patch.object(n_net, 'get_random_mac', lambda *_: '01:02:03:04:05:06') class TestOVNMechansimDriverDHCPOptions(OVNMechanismDriverTestCase): def _test_get_ovn_dhcp_options_helper(self, subnet, network, expected_dhcp_options, service_mac=None): dhcp_options = self.mech_driver._ovn_client._get_ovn_dhcp_options( subnet, network, service_mac) self.assertEqual(expected_dhcp_options, dhcp_options) def test_get_ovn_dhcp_options(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': '10.0.0.0/24', 'ip_version': 4, 'enable_dhcp': True, 'gateway_ip': '10.0.0.1', 'dns_nameservers': ['7.7.7.7', '8.8.8.8'], 'host_routes': [{'destination': '20.0.0.4', 'nexthop': '10.0.0.100'}]} network = {'id': 'network-id', 'mtu': 1400} expected_dhcp_options = {'cidr': '10.0.0.0/24', 'external_ids': { 'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}} expected_dhcp_options['options'] = { 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1400', 'router': subnet['gateway_ip'], 'dns_server': '{7.7.7.7, 8.8.8.8}', 'classless_static_route': '{20.0.0.4,10.0.0.100, 0.0.0.0/0,10.0.0.1}' } self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) expected_dhcp_options['options']['server_mac'] = '11:22:33:44:55:66' self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options, service_mac='11:22:33:44:55:66') def test_get_ovn_dhcp_options_dhcp_disabled(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': '10.0.0.0/24', 'ip_version': 4, 'enable_dhcp': False, 'gateway_ip': '10.0.0.1', 'dns_nameservers': ['7.7.7.7', '8.8.8.8'], 'host_routes': [{'destination': '20.0.0.4', 'nexthop': '10.0.0.100'}]} network = {'id': 'network-id', 'mtu': 1400} expected_dhcp_options = {'cidr': '10.0.0.0/24', 'external_ids': { 'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'options': {}} self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) def test_get_ovn_dhcp_options_no_gw_ip(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': '10.0.0.0/24', 'ip_version': 4, 'enable_dhcp': True, 'gateway_ip': None, 'dns_nameservers': ['7.7.7.7', '8.8.8.8'], 'host_routes': [{'destination': '20.0.0.4', 'nexthop': '10.0.0.100'}]} network = {'id': 'network-id', 'mtu': 1400} expected_dhcp_options = {'cidr': '10.0.0.0/24', 'external_ids': { 'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'options': {}} self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) def test_get_ovn_dhcp_options_no_gw_ip_but_metadata_ip(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': '10.0.0.0/24', 'ip_version': 4, 'enable_dhcp': True, 'dns_nameservers': [], 'host_routes': [], 'gateway_ip': None} network = {'id': 'network-id', 'mtu': 1400} expected_dhcp_options = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}, 'options': {'server_id': '10.0.0.2', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1400', 'classless_static_route': '{169.254.169.254/32,10.0.0.2}'}} with mock.patch.object(self.mech_driver._ovn_client, '_find_metadata_port_ip', return_value='10.0.0.2'): self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) def test_get_ovn_dhcp_options_ipv6_subnet(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': 'ae70::/24', 'ip_version': 6, 'enable_dhcp': True, 'dns_nameservers': ['7.7.7.7', '8.8.8.8']} network = {'id': 'network-id', 'mtu': 1400} ext_ids = {'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'} expected_dhcp_options = { 'cidr': 'ae70::/24', 'external_ids': ext_ids, 'options': {'server_id': '01:02:03:04:05:06', 'dns_server': '{7.7.7.7, 8.8.8.8}'}} self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) expected_dhcp_options['options']['server_id'] = '11:22:33:44:55:66' self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options, service_mac='11:22:33:44:55:66') def test_get_ovn_dhcp_options_dhcpv6_stateless_subnet(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': 'ae70::/24', 'ip_version': 6, 'enable_dhcp': True, 'dns_nameservers': ['7.7.7.7', '8.8.8.8'], 'ipv6_address_mode': const.DHCPV6_STATELESS} network = {'id': 'network-id', 'mtu': 1400} ext_ids = {'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'} expected_dhcp_options = { 'cidr': 'ae70::/24', 'external_ids': ext_ids, 'options': {'server_id': '01:02:03:04:05:06', 'dns_server': '{7.7.7.7, 8.8.8.8}', 'dhcpv6_stateless': 'true'}} self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) expected_dhcp_options['options']['server_id'] = '11:22:33:44:55:66' self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options, service_mac='11:22:33:44:55:66') def test_get_ovn_dhcp_options_metadata_route(self): subnet = {'id': 'foo-subnet', 'network_id': 'network-id', 'cidr': '10.0.0.0/24', 'ip_version': 4, 'enable_dhcp': True, 'gateway_ip': '10.0.0.1', 'dns_nameservers': ['7.7.7.7', '8.8.8.8'], 'host_routes': []} network = {'id': 'network-id', 'mtu': 1400} expected_dhcp_options = {'cidr': '10.0.0.0/24', 'external_ids': { 'subnet_id': 'foo-subnet', ovn_const.OVN_REV_NUM_EXT_ID_KEY: '1'}} expected_dhcp_options['options'] = { 'server_id': subnet['gateway_ip'], 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': '1400', 'router': subnet['gateway_ip'], 'dns_server': '{7.7.7.7, 8.8.8.8}', 'classless_static_route': '{169.254.169.254/32,10.0.0.2, 0.0.0.0/0,10.0.0.1}' } with mock.patch.object(self.mech_driver._ovn_client, '_find_metadata_port_ip', return_value='10.0.0.2'): self._test_get_ovn_dhcp_options_helper(subnet, network, expected_dhcp_options) def _test__get_port_dhcp_options_port_dhcp_opts_set(self, ip_version=4): if ip_version == 4: ip_address = '10.0.0.11' else: ip_address = 'aef0::4' port = { 'id': 'foo-port', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'foo-subnet', 'ip_address': ip_address}]} if ip_version == 4: port['extra_dhcp_opts'] = [ {'ip_version': 4, 'opt_name': 'mtu', 'opt_value': '1200'}, {'ip_version': 4, 'opt_name': 'ntp-server', 'opt_value': '8.8.8.8'}] else: port['extra_dhcp_opts'] = [ {'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'foo-domain'}, {'ip_version': 4, 'opt_name': 'dns-server', 'opt_value': '7.7.7.7'}] self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port = ( mock.Mock( return_value=({ 'cidr': '10.0.0.0/24' if ip_version == 4 else 'aef0::/64', 'external_ids': {'subnet_id': 'foo-subnet'}, 'options': (ip_version == 4) and { 'router': '10.0.0.1', 'mtu': '1400'} or { 'server_id': '01:02:03:04:05:06'}, 'uuid': 'foo-uuid'}))) if ip_version == 4: expected_dhcp_options = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': 'foo-subnet', 'port_id': 'foo-port'}, 'options': {'router': '10.0.0.1', 'mtu': '1200', 'ntp_server': '8.8.8.8'}} else: expected_dhcp_options = { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': 'foo-subnet', 'port_id': 'foo-port'}, 'options': {'server_id': '01:02:03:04:05:06', 'domain_search': 'foo-domain'}} self.mech_driver._nb_ovn.add_dhcp_options.return_value = 'foo-val' dhcp_options = self.mech_driver._ovn_client._get_port_dhcp_options( port, ip_version) self.assertEqual({'cmd': 'foo-val'}, dhcp_options) self.mech_driver._nb_ovn.add_dhcp_options.assert_called_once_with( 'foo-subnet', port_id='foo-port', **expected_dhcp_options) def test__get_port_dhcp_options_port_dhcp_opts_set_v4(self): self._test__get_port_dhcp_options_port_dhcp_opts_set(ip_version=4) def test__get_port_dhcp_options_port_dhcp_opts_set_v6(self): self._test__get_port_dhcp_options_port_dhcp_opts_set(ip_version=6) def _test__get_port_dhcp_options_port_dhcp_opts_not_set( self, ip_version=4): if ip_version == 4: port = {'id': 'foo-port', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'foo-subnet', 'ip_address': '10.0.0.11'}]} else: port = {'id': 'foo-port', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'foo-subnet', 'ip_address': 'aef0::4'}]} if ip_version == 4: expected_dhcp_opts = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': 'foo-subnet'}, 'options': {'router': '10.0.0.1', 'mtu': '1400'}} else: expected_dhcp_opts = { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': 'foo-subnet'}, 'options': {'server_id': '01:02:03:04:05:06'}} self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port = ( mock.Mock(return_value=expected_dhcp_opts)) self.assertEqual( expected_dhcp_opts, self.mech_driver._ovn_client._get_port_dhcp_options( port, ip_version=ip_version)) # Since the port has no extra DHCPv4/v6 options defined, no new # DHCP_Options row should be created and logical switch port DHCPv4/v6 # options should point to the subnet DHCPv4/v6 options. self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() def test__get_port_dhcp_options_port_dhcp_opts_not_set_v4(self): self._test__get_port_dhcp_options_port_dhcp_opts_not_set(ip_version=4) def test__get_port_dhcp_options_port_dhcp_opts_not_set_v6(self): self._test__get_port_dhcp_options_port_dhcp_opts_not_set(ip_version=6) def _test__get_port_dhcp_options_port_dhcp_disabled(self, ip_version=4): port = { 'id': 'foo-port', 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': 'foo-subnet', 'ip_address': '10.0.0.11'}, {'subnet_id': 'foo-subnet-v6', 'ip_address': 'aef0::11'}], 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'dhcp_disabled', 'opt_value': 'False'}, {'ip_version': 6, 'opt_name': 'dhcp_disabled', 'opt_value': 'False'}] } subnet_dhcp_opts = mock.Mock() self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port = ( mock.Mock(return_value=subnet_dhcp_opts)) # No dhcp_disabled set to true, subnet dhcp options will be get for # this port. Since it doesn't have any other extra dhcp options, but # dhcp_disabled, no port dhcp options will be created. self.assertEqual( subnet_dhcp_opts, self.mech_driver._ovn_client._get_port_dhcp_options( port, ip_version)) self.assertEqual( 1, self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port. call_count) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() # Set dhcp_disabled with ip_version specified by this test case to # true, no dhcp options will be get since it's dhcp_disabled now for # ip_version be tested. opt_index = 0 if ip_version == 4 else 1 port['extra_dhcp_opts'][opt_index]['opt_value'] = 'True' self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port.\ reset_mock() self.assertIsNone( self.mech_driver._ovn_client._get_port_dhcp_options( port, ip_version)) self.assertEqual( 0, self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port. call_count) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() # Set dhcp_disabled with ip_version specified by this test case to # false, and set dhcp_disabled with ip_version not in test to true. # Subnet dhcp options will be get, since dhcp_disabled with ip_version # not in test should not affect. opt_index_1 = 1 if ip_version == 4 else 0 port['extra_dhcp_opts'][opt_index]['opt_value'] = 'False' port['extra_dhcp_opts'][opt_index_1]['opt_value'] = 'True' self.assertEqual( subnet_dhcp_opts, self.mech_driver._ovn_client._get_port_dhcp_options( port, ip_version)) self.assertEqual( 1, self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port. call_count) self.mech_driver._nb_ovn.add_dhcp_options.assert_not_called() def test__get_port_dhcp_options_port_dhcp_disabled_v4(self): self._test__get_port_dhcp_options_port_dhcp_disabled(ip_version=4) def test__get_port_dhcp_options_port_dhcp_disabled_v6(self): self._test__get_port_dhcp_options_port_dhcp_disabled(ip_version=6) def test__get_port_dhcp_options_port_with_invalid_device_owner(self): port = { 'id': 'foo-port', 'device_owner': 'neutron:router_interface', 'fixed_ips': ['fake'] } self.assertIsNone( self.mech_driver._ovn_client._get_port_dhcp_options( port, mock.ANY)) def _test__get_subnet_dhcp_options_for_port(self, ip_version=4, enable_dhcp=True): port = {'fixed_ips': [ {'ip_address': '10.0.0.4', 'subnet_id': 'v4_snet_id_1' if enable_dhcp else 'v4_snet_id_2'}, {'ip_address': '2001:dba::4', 'subnet_id': 'v6_snet_id_1' if enable_dhcp else 'v6_snet_id_2'}, {'ip_address': '2001:dbb::4', 'subnet_id': 'v6_snet_id_3'}]} def fake(subnets): fake_rows = { 'v4_snet_id_1': 'foo', 'v6_snet_id_1': {'options': {}}, 'v6_snet_id_3': {'options': { ovn_const.DHCPV6_STATELESS_OPT: 'true'}}} return [fake_rows[row] for row in fake_rows if row in subnets] self.mech_driver._nb_ovn.get_subnets_dhcp_options.side_effect = fake if ip_version == 4: expected_opts = 'foo' if enable_dhcp else None else: expected_opts = { 'options': {} if enable_dhcp else { ovn_const.DHCPV6_STATELESS_OPT: 'true'}} self.assertEqual( expected_opts, self.mech_driver._ovn_client._get_subnet_dhcp_options_for_port( port, ip_version)) def test__get_subnet_dhcp_options_for_port_v4(self): self._test__get_subnet_dhcp_options_for_port() def test__get_subnet_dhcp_options_for_port_v4_dhcp_disabled(self): self._test__get_subnet_dhcp_options_for_port(enable_dhcp=False) def test__get_subnet_dhcp_options_for_port_v6(self): self._test__get_subnet_dhcp_options_for_port(ip_version=6) def test__get_subnet_dhcp_options_for_port_v6_dhcp_disabled(self): self._test__get_subnet_dhcp_options_for_port(ip_version=6, enable_dhcp=False) class TestOVNMechanismDriverSecurityGroup( test_security_group.Ml2SecurityGroupsTestCase): # This set of test cases is supplement to test_acl.py, the purpose is to # test acl methods invoking. Content correctness of args of acl methods # is mainly guaranteed by acl_test.py. def setUp(self): cfg.CONF.set_override('mechanism_drivers', ['logger', 'ovn'], 'ml2') super(TestOVNMechanismDriverSecurityGroup, self).setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj nb_ovn = fakes.FakeOvsdbNbOvnIdl() sb_ovn = fakes.FakeOvsdbSbOvnIdl() self.mech_driver._nb_ovn = nb_ovn self.mech_driver._sb_ovn = sb_ovn self.ctx = context.get_admin_context() revision_plugin.RevisionPlugin() def _delete_default_sg_rules(self, security_group_id): res = self._list( 'security-group-rules', query_params='security_group_id=%s' % security_group_id) for r in res['security_group_rules']: self._delete('security-group-rules', r['id']) def _create_sg(self, sg_name): sg = self._make_security_group(self.fmt, sg_name, '') return sg['security_group'] def _create_empty_sg(self, sg_name): sg = self._create_sg(sg_name) self._delete_default_sg_rules(sg['id']) return sg def _create_sg_rule(self, sg_id, direction, proto, port_range_min=None, port_range_max=None, remote_ip_prefix=None, remote_group_id=None, ethertype=const.IPv4): r = self._build_security_group_rule(sg_id, direction, proto, port_range_min=port_range_min, port_range_max=port_range_max, remote_ip_prefix=remote_ip_prefix, remote_group_id=remote_group_id, ethertype=ethertype) res = self._create_security_group_rule(self.fmt, r) rule = self.deserialize(self.fmt, res) return rule['security_group_rule'] def _delete_sg_rule(self, rule_id): self._delete('security-group-rules', rule_id) def test_create_port_with_sg_default_rules(self): with self.network() as n, self.subnet(n): sg = self._create_sg('sg') self._make_port(self.fmt, n['network']['id'], security_groups=[sg['id']]) # One DHCP rule, one IPv6 rule, one IPv4 rule and # two default dropping rules. self.assertEqual( 5, self.mech_driver._nb_ovn.add_acl.call_count) def test_create_port_with_empty_sg(self): with self.network() as n, self.subnet(n): sg = self._create_empty_sg('sg') self._make_port(self.fmt, n['network']['id'], security_groups=[sg['id']]) # One DHCP rule and two default dropping rules. self.assertEqual( 3, self.mech_driver._nb_ovn.add_acl.call_count) def test_create_port_with_multi_sgs(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') sg2 = self._create_empty_sg('sg2') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_TCP, port_range_min=22, port_range_max=23) self._create_sg_rule(sg2['id'], 'egress', const.PROTO_NAME_UDP, remote_ip_prefix='0.0.0.0/0') self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id'], sg2['id']]) # One DHCP rule, one TCP rule, one UDP rule and # two default dropping rules. self.assertEqual( 5, self.mech_driver._nb_ovn.add_acl.call_count) def test_create_port_with_multi_sgs_duplicate_rules(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') sg2 = self._create_empty_sg('sg2') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_TCP, port_range_min=22, port_range_max=23, remote_ip_prefix='20.0.0.0/24') self._create_sg_rule(sg2['id'], 'ingress', const.PROTO_NAME_TCP, port_range_min=22, port_range_max=23, remote_ip_prefix='20.0.0.0/24') self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id'], sg2['id']]) # One DHCP rule, two TCP rule and two default dropping rules. self.assertEqual( 5, self.mech_driver._nb_ovn.add_acl.call_count) def test_update_port_with_sgs(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_TCP, ethertype=const.IPv6) p = self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id']])['port'] # One DHCP rule, one TCP rule and two default dropping rules. self.assertEqual( 4, self.mech_driver._nb_ovn.add_acl.call_count) sg2 = self._create_empty_sg('sg2') self._create_sg_rule(sg2['id'], 'egress', const.PROTO_NAME_UDP, remote_ip_prefix='30.0.0.0/24') data = {'port': {'security_groups': [sg1['id'], sg2['id']]}} req = self.new_update_request('ports', data, p['id']) req.get_response(self.api) self.assertEqual( 1, self.mech_driver._nb_ovn.update_acls.call_count) def test_update_sg_change_rule(self): with self.network() as n, self.subnet(n): sg = self._create_empty_sg('sg') self._make_port(self.fmt, n['network']['id'], security_groups=[sg['id']]) # One DHCP rule and two default dropping rules. self.assertEqual( 3, self.mech_driver._nb_ovn.add_acl.call_count) sg_r = self._create_sg_rule(sg['id'], 'ingress', const.PROTO_NAME_UDP, ethertype=const.IPv6) self.assertEqual( 1, self.mech_driver._nb_ovn.update_acls.call_count) self._delete_sg_rule(sg_r['id']) self.assertEqual( 2, self.mech_driver._nb_ovn.update_acls.call_count) def test_update_sg_change_rule_unrelated_port(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') sg2 = self._create_empty_sg('sg2') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_TCP, remote_group_id=sg2['id']) self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id']]) # One DHCP rule, one TCP rule and two default dropping rules. self.assertEqual( 4, self.mech_driver._nb_ovn.add_acl.call_count) sg2_r = self._create_sg_rule(sg2['id'], 'egress', const.PROTO_NAME_UDP) self.mech_driver._nb_ovn.update_acls.assert_not_called() self._delete_sg_rule(sg2_r['id']) self.mech_driver._nb_ovn.update_acls.assert_not_called() def test_update_sg_duplicate_rule(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') sg2 = self._create_empty_sg('sg2') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_UDP, port_range_min=22, port_range_max=23) self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id'], sg2['id']]) # One DHCP rule, one UDP rule and two default dropping rules. self.assertEqual( 4, self.mech_driver._nb_ovn.add_acl.call_count) # Add a new duplicate rule to sg2. It's expected to be added. sg2_r = self._create_sg_rule(sg2['id'], 'ingress', const.PROTO_NAME_UDP, port_range_min=22, port_range_max=23) self.mech_driver._nb_ovn.update_acls.assert_called_once() # Delete the duplicate rule. It's expected to be deleted. self._delete_sg_rule(sg2_r['id']) self.assertEqual( 2, self.mech_driver._nb_ovn.update_acls.call_count) def test_update_sg_duplicate_rule_multi_ports(self): with self.network() as n, self.subnet(n): sg1 = self._create_empty_sg('sg1') sg2 = self._create_empty_sg('sg2') sg3 = self._create_empty_sg('sg3') self._create_sg_rule(sg1['id'], 'ingress', const.PROTO_NAME_UDP, remote_group_id=sg3['id']) self._create_sg_rule(sg2['id'], 'egress', const.PROTO_NAME_TCP, port_range_min=60, port_range_max=70) self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id'], sg2['id']]) self._make_port(self.fmt, n['network']['id'], security_groups=[sg1['id'], sg2['id']]) self._make_port(self.fmt, n['network']['id'], security_groups=[sg2['id'], sg3['id']]) # Rules include 5 + 5 + 4 self.assertEqual( 14, self.mech_driver._nb_ovn.add_acl.call_count) # Add a rule to sg1 duplicate with sg2. It's expected to be added. sg1_r = self._create_sg_rule(sg1['id'], 'egress', const.PROTO_NAME_TCP, port_range_min=60, port_range_max=70) self.mech_driver._nb_ovn.update_acls.assert_called_once() # Add a rule to sg2 duplicate with sg1 but not duplicate with sg3. # It's expected to be added as well. sg2_r = self._create_sg_rule(sg2['id'], 'ingress', const.PROTO_NAME_UDP, remote_group_id=sg3['id']) self.assertEqual( 2, self.mech_driver._nb_ovn.update_acls.call_count) # Delete the duplicate rule in sg1. It's expected to be deleted. self._delete_sg_rule(sg1_r['id']) self.assertEqual( 3, self.mech_driver._nb_ovn.update_acls.call_count) # Delete the duplicate rule in sg2. It's expected to be deleted. self._delete_sg_rule(sg2_r['id']) self.assertEqual( 4, self.mech_driver._nb_ovn.update_acls.call_count) class TestOVNMechanismDriverMetadataPort(test_plugin.Ml2PluginV2TestCase): _mechanism_drivers = ['logger', 'ovn'] def setUp(self): super(TestOVNMechanismDriverMetadataPort, self).setUp() mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj self.mech_driver._nb_ovn = fakes.FakeOvsdbNbOvnIdl() self.mech_driver._sb_ovn = fakes.FakeOvsdbSbOvnIdl() self.nb_ovn = self.mech_driver._nb_ovn self.sb_ovn = self.mech_driver._sb_ovn ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', True, group='ovn') p = mock.patch.object(ovn_utils, 'get_revision_number', return_value=1) p.start() self.addCleanup(p.stop) def test_metadata_port_on_network_create(self): """Check metadata port create. Check that a localport is created when a neutron network is created. """ with self.network(): self.assertEqual(1, self.nb_ovn.create_lswitch_port.call_count) args, kwargs = self.nb_ovn.create_lswitch_port.call_args self.assertEqual('localport', kwargs['type']) def test_metadata_port_not_created_if_exists(self): """Check that metadata port is not created if it already exists. In the event of a sync, it might happen that a metadata port exists already. When we are creating the logical switch in OVN we don't want this port to be created again. """ with mock.patch.object( self.mech_driver._ovn_client, '_get_metadata_ports', return_value=['metadata_port1']): with self.network(): self.assertEqual(0, self.nb_ovn.create_lswitch_port.call_count) def test_metadata_ip_on_subnet_create(self): """Check metadata port update. Check that the metadata port is updated with a new IP address when a subnet is created. """ with self.network() as net1: with self.subnet(network=net1, cidr='10.0.0.0/24'): self.assertEqual(1, self.nb_ovn.set_lswitch_port.call_count) args, kwargs = self.nb_ovn.set_lswitch_port.call_args self.assertEqual('localport', kwargs['type']) self.assertEqual('10.0.0.2/24', kwargs['external_ids'].get( ovn_const.OVN_CIDRS_EXT_ID_KEY, '')) def test_metadata_port_on_network_delete(self): """Check metadata port delete. Check that the metadata port is deleted when a network is deleted. """ net = self._make_network(self.fmt, name="net1", admin_state_up=True) network_id = net['network']['id'] req = self.new_delete_request('networks', network_id) res = req.get_response(self.api) self.assertEqual(exc.HTTPNoContent.code, res.status_int) self.assertEqual(1, self.nb_ovn.delete_lswitch_port.call_count) networking-ovn-4.0.0/networking_ovn/tests/unit/ml2/test_qos_driver.py0000666000175100017510000002443213245511164026211 0ustar zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 mock from neutron_lib import constants from oslo_utils import uuidutils from neutron.objects.qos import policy as qos_policy from neutron.objects.qos import rule as qos_rule from neutron.tests import base from networking_ovn.common import utils from networking_ovn.ml2 import qos_driver context = 'context' class TestOVNQosNotificationDriver(base.BaseTestCase): def setUp(self): super(TestOVNQosNotificationDriver, self).setUp() self.mech_driver = mock.Mock() self.mech_driver._ovn_client = mock.Mock() self.mech_driver._ovn_client._qos_driver = mock.Mock() self.driver = qos_driver.OVNQosNotificationDriver.create( self.mech_driver) self.policy = "policy" def test_create_policy(self): self.driver.create_policy(context, self.policy) self.driver._driver._ovn_client._qos_driver.create_policy.\ assert_not_called() def test_update_policy(self): self.driver.update_policy(context, self.policy) self.driver._driver._ovn_client._qos_driver.update_policy.\ assert_called_once_with(context, self.policy) def test_delete_policy(self): self.driver.delete_policy(context, self.policy) self.driver._driver._ovn_client._qos_driver.delete_policy.\ assert_not_called() class TestOVNQosDriver(base.BaseTestCase): def setUp(self): super(TestOVNQosDriver, self).setUp() self.plugin = mock.Mock() self.ovn_client = mock.Mock() self.driver = qos_driver.OVNQosDriver(self.ovn_client) self.driver._plugin_property = self.plugin self.port_id = uuidutils.generate_uuid() self.policy_id = uuidutils.generate_uuid() self.network_id = uuidutils.generate_uuid() self.network_policy_id = uuidutils.generate_uuid() self.policy = self._create_fake_policy() self.port = self._create_fake_port() self.rule = self._create_bw_limit_rule() self.expected = {'qos_max_rate': '1000', 'qos_burst': '100000'} def _create_bw_limit_rule(self): rule_obj = qos_rule.QosBandwidthLimitRule() rule_obj.id = uuidutils.generate_uuid() rule_obj.max_kbps = 1 rule_obj.max_burst_kbps = 100 rule_obj.obj_reset_changes() return rule_obj def _create_fake_policy(self): policy_dict = {'id': self.network_policy_id} policy_obj = qos_policy.QosPolicy(context, **policy_dict) policy_obj.obj_reset_changes() return policy_obj def _create_fake_port(self): return {'id': self.port_id, 'qos_policy_id': self.policy_id, 'network_id': self.network_id, 'device_owner': 'compute:fake'} def _create_fake_network(self): return {'id': self.network_id, 'qos_policy_id': self.network_policy_id} def test__is_network_device_port(self): self.assertFalse(utils.is_network_device_port(self.port)) port = self._create_fake_port() port['device_owner'] = constants.DEVICE_OWNER_DHCP self.assertTrue(utils.is_network_device_port(port)) port['device_owner'] = 'neutron:LOADBALANCERV2' self.assertTrue(utils.is_network_device_port(port)) def _generate_port_options(self, policy_id, return_val, expected_result): with mock.patch.object(qos_rule, 'get_rules', return_value=return_val) as get_rules: options = self.driver._generate_port_options(context, policy_id) if policy_id: get_rules.assert_called_once_with(context, policy_id) else: get_rules.assert_not_called() self.assertEqual(expected_result, options) def test__generate_port_options_no_policy_id(self): self._generate_port_options(None, [], {}) def test__generate_port_options_no_rules(self): self._generate_port_options(self.policy_id, [], {}) def test__generate_port_options_with_rule(self): self._generate_port_options(self.policy_id, [self.rule], self.expected) def _get_qos_options(self, port, port_policy, network_policy): with mock.patch.object(qos_policy.QosPolicy, 'get_network_policy', return_value=self.policy) as get_network_policy: with mock.patch.object(self.driver, '_generate_port_options', return_value={}) as generate_port_options: options = self.driver.get_qos_options(port) if network_policy: get_network_policy.\ assert_called_once_with(context, self.network_id) generate_port_options. \ assert_called_once_with(context, self.network_policy_id) elif port_policy: get_network_policy.assert_not_called() generate_port_options.\ assert_called_once_with(context, self.policy_id) else: get_network_policy.assert_not_called() generate_port_options.assert_not_called() self.assertEqual({}, options) def test_get_qos_options_no_qos(self): port = self._create_fake_port() port.pop('qos_policy_id') self._get_qos_options(port, False, False) def test_get_qos_options_network_port(self): port = self._create_fake_port() port['device_owner'] = constants.DEVICE_OWNER_DHCP self._get_qos_options(port, False, False) @mock.patch('neutron_lib.context.get_admin_context', return_value=context) def test_get_qos_options_port_policy(self, *mocks): self._get_qos_options(self.port, True, False) @mock.patch('neutron_lib.context.get_admin_context', return_value=context) def test_get_qos_options_network_policy(self, *mocks): port = self._create_fake_port() port['qos_policy_id'] = None self._get_qos_options(port, False, True) def _update_network_ports(self, port, called): with mock.patch.object(self.plugin, 'get_ports', return_value=[port]) as get_ports: with mock.patch.object(self.ovn_client, 'update_port') as update_port: self.driver._update_network_ports( context, self.network_id, {}) get_ports.assert_called_once_with( context, filters={'network_id': [self.network_id]}) if called: update_port.assert_called() else: update_port.assert_not_called() def test__update_network_ports_port_policy(self): self._update_network_ports(self.port, False) def test__update_network_ports_network_device(self): port = self._create_fake_port() port['device_owner'] = constants.DEVICE_OWNER_DHCP self._update_network_ports(port, False) def test__update_network_ports(self): port = self._create_fake_port() port['qos_policy_id'] = None self._update_network_ports(port, True) def _update_network(self, network, called): with mock.patch.object(self.driver, '_generate_port_options', return_value={}) as generate_port_options: with mock.patch.object(self.driver, '_update_network_ports' ) as update_network_ports: self.driver.update_network(network) if called: generate_port_options.assert_called_once_with( context, self.network_policy_id) update_network_ports.assert_called_once_with( context, self.network_id, {}) else: generate_port_options.assert_not_called() update_network_ports.assert_not_called() @mock.patch('neutron_lib.context.get_admin_context', return_value=context) def test_update_network_no_qos(self, *mocks): network = self._create_fake_network() network.pop('qos_policy_id') self._update_network(network, False) @mock.patch('neutron_lib.context.get_admin_context', return_value=context) def test_update_network_policy_change(self, *mocks): network = self._create_fake_network() self._update_network(network, True) def test_update_policy(self): with mock.patch.object(self.driver, '_generate_port_options', return_value={}) as generate_port_options, \ mock.patch.object(self.policy, 'get_bound_networks', return_value=[self.network_id] ) as get_bound_networks, \ mock.patch.object(self.driver, '_update_network_ports' ) as update_network_ports, \ mock.patch.object(self.policy, 'get_bound_ports', return_value=[self.port_id] ) as get_bound_ports, \ mock.patch.object(self.plugin, 'get_port', return_value=self.port) as get_port, \ mock.patch.object(self.ovn_client, 'update_port', ) as update_port: self.driver.update_policy(context, self.policy) generate_port_options.assert_called_once_with( context, self.network_policy_id) get_bound_networks.assert_called_once() update_network_ports.assert_called_once_with( context, self.network_id, {}) get_bound_ports.assert_called_once() get_port.assert_called_once_with(context, self.port_id) update_port.assert_called_once_with(self.port, qos_options={}) networking-ovn-4.0.0/networking_ovn/tests/unit/ml2/__init__.py0000666000175100017510000000000013245511145024514 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/unit/ml2/test_trunk_driver.py0000666000175100017510000002625013245511145026551 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 mock from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from networking_ovn.common.constants import OVN_ML2_MECH_DRIVER_NAME from networking_ovn.ml2 import trunk_driver from networking_ovn.tests.unit import fakes from neutron.services.trunk import constants as trunk_consts from neutron.tests import base from oslo_config import cfg class TestTrunkHandler(base.BaseTestCase): def setUp(self): super(TestTrunkHandler, self).setUp() self.context = mock.Mock() self.plugin_driver = mock.Mock() self.plugin_driver._plugin = mock.Mock() self.plugin_driver._plugin.update_port = mock.Mock() self.plugin_driver._nb_ovn = fakes.FakeOvsdbNbOvnIdl() self.handler = trunk_driver.OVNTrunkHandler(self.plugin_driver) self.trunk_1 = mock.Mock() self.trunk_1.port_id = "parent_port_1" self.trunk_2 = mock.Mock() self.trunk_2.port_id = "parent_port_2" self.sub_port_1 = mock.Mock() self.sub_port_1.segmentation_id = 40 self.sub_port_1.trunk_id = "trunk-1" self.sub_port_1.port_id = "sub_port_1" self.sub_port_2 = mock.Mock() self.sub_port_2.segmentation_id = 41 self.sub_port_2.trunk_id = "trunk-1" self.sub_port_2.port_id = "sub_port_2" self.sub_port_3 = mock.Mock() self.sub_port_3.segmentation_id = 42 self.sub_port_3.trunk_id = "trunk-2" self.sub_port_3.port_id = "sub_port_3" self.sub_port_4 = mock.Mock() self.sub_port_4.segmentation_id = 43 self.sub_port_4.trunk_id = "trunk-2" self.sub_port_4.port_id = "sub_port_4" self.get_trunk_object = mock.patch( "neutron.objects.trunk.Trunk.get_object").start() self.get_trunk_object.side_effect = lambda ctxt, id: \ self.trunk_1 if id == 'trunk-1' else self.trunk_2 def _get_binding_profile_info(self, parent_name=None, tag=None): binding_profile = {} if parent_name and tag: binding_profile = {'parent_name': parent_name, 'tag': tag} return {'port': {'binding:profile': binding_profile}} def _assert_update_port_calls(self, calls): self.assertEqual(len(calls), self.plugin_driver._plugin.update_port.call_count) self.plugin_driver._plugin.update_port.assert_has_calls( calls, any_order=True) def test_create_trunk(self): self.trunk_1.sub_ports = [] self.handler.trunk_created(self.trunk_1) self.plugin_driver._plugin.update_port.assert_not_called() self.trunk_1.sub_ports = [self.sub_port_1, self.sub_port_2] self.handler.trunk_created(self.trunk_1) calls = [mock.call(mock.ANY, s_port.port_id, self._get_binding_profile_info( trunk.port_id, s_port.segmentation_id)) for trunk, s_port in [(self.trunk_1, self.sub_port_1), (self.trunk_1, self.sub_port_2)]] self._assert_update_port_calls(calls) def test_delete_trunk(self): self.trunk_1.sub_ports = [] self.handler.trunk_deleted(self.trunk_1) self.plugin_driver._plugin.update_port.assert_not_called() self.trunk_1.sub_ports = [self.sub_port_1, self.sub_port_2] self.handler.trunk_deleted(self.trunk_1) calls = [mock.call(mock.ANY, s_port.port_id, self._get_binding_profile_info( trunk.port_id, None)) for trunk, s_port in [(self.trunk_1, self.sub_port_1), (self.trunk_1, self.sub_port_2)]] self._assert_update_port_calls(calls) def test_subports_added(self): self.handler.subports_added(self.trunk_1, [self.sub_port_1, self.sub_port_2]) self.handler.subports_added(self.trunk_2, [self.sub_port_3, self.sub_port_4]) calls = [mock.call(mock.ANY, s_port.port_id, self._get_binding_profile_info( trunk.port_id, s_port.segmentation_id)) for trunk, s_port in [(self.trunk_1, self.sub_port_1), (self.trunk_1, self.sub_port_2), (self.trunk_2, self.sub_port_3), (self.trunk_2, self.sub_port_4)]] self._assert_update_port_calls(calls) def test_subports_deleted(self): self.handler.subports_deleted(self.trunk_1, [self.sub_port_1, self.sub_port_2]) self.handler.subports_deleted(self.trunk_2, [self.sub_port_3, self.sub_port_4]) calls = [mock.call(mock.ANY, s_port.port_id, self._get_binding_profile_info( trunk.port_id, None)) for trunk, s_port in [(self.trunk_1, self.sub_port_1), (self.trunk_1, self.sub_port_2), (self.trunk_2, self.sub_port_3), (self.trunk_2, self.sub_port_4)]] self._assert_update_port_calls(calls) def _fake_trunk_event_payload(self): payload = mock.Mock() payload.current_trunk = mock.Mock() payload.current_trunk.port_id = 'current_trunk_port_id' payload.original_trunk = mock.Mock() payload.original_trunk.port_id = 'original_trunk_port_id' current_subport = mock.Mock() current_subport.segmentation_id = 40 current_subport.trunk_id = 'current_trunk_port_id' current_subport.port_id = 'current_subport_port_id' original_subport = mock.Mock() original_subport.segmentation_id = 41 original_subport.trunk_id = 'original_trunk_port_id' original_subport.port_id = 'original_subport_port_id' payload.current_trunk.sub_ports = [current_subport] payload.original_trunk.sub_ports = [original_subport] return payload def test_trunk_event_create(self): fake_payload = self._fake_trunk_event_payload() self.handler.trunk_event( mock.ANY, events.AFTER_CREATE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_called_once_with( mock.ANY, fake_payload.current_trunk.sub_ports[0].port_id, self._get_binding_profile_info( fake_payload.current_trunk.port_id, fake_payload.current_trunk.sub_ports[0].segmentation_id)) def test_trunk_event_delete(self): fake_payload = self._fake_trunk_event_payload() self.handler.trunk_event( mock.ANY, events.AFTER_DELETE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_called_once_with( mock.ANY, fake_payload.original_trunk.sub_ports[0].port_id, self._get_binding_profile_info( fake_payload.original_trunk.port_id, None)) def test_trunk_event_invalid(self): fake_payload = self._fake_trunk_event_payload() self.handler.trunk_event( mock.ANY, events.BEFORE_DELETE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_not_called() def _fake_subport_event_payload(self): payload = mock.Mock() payload.original_trunk = mock.Mock() payload.original_trunk.port_id = 'original_trunk_port_id' original_subport = mock.Mock() original_subport.segmentation_id = 41 original_subport.trunk_id = 'original_trunk_port_id' original_subport.port_id = 'original_subport_port_id' payload.subports = [original_subport] return payload def test_subport_event_create(self): fake_payload = self._fake_subport_event_payload() self.handler.subport_event( mock.ANY, events.AFTER_CREATE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_called_once_with( mock.ANY, fake_payload.subports[0].port_id, self._get_binding_profile_info( fake_payload.original_trunk.port_id, fake_payload.subports[0].segmentation_id)) def test_subport_event_delete(self): fake_payload = self._fake_subport_event_payload() self.handler.subport_event( mock.ANY, events.AFTER_DELETE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_called_once_with( mock.ANY, fake_payload.subports[0].port_id, self._get_binding_profile_info( fake_payload.original_trunk.port_id, None)) def test_subport_event_invalid(self): fake_payload = self._fake_trunk_event_payload() self.handler.subport_event( mock.ANY, events.BEFORE_DELETE, mock.ANY, fake_payload) self.plugin_driver._plugin.update_port.assert_not_called() class TestTrunkDriver(base.BaseTestCase): def setUp(self): super(TestTrunkDriver, self).setUp() def test_is_loaded(self): driver = trunk_driver.OVNTrunkDriver.create(mock.Mock()) cfg.CONF.set_override('mechanism_drivers', ["logger", OVN_ML2_MECH_DRIVER_NAME], group='ml2') self.assertTrue(driver.is_loaded) cfg.CONF.set_override('mechanism_drivers', ['ovs', 'logger'], group='ml2') self.assertFalse(driver.is_loaded) cfg.CONF.set_override('core_plugin', 'some_plugin') self.assertFalse(driver.is_loaded) def test_register(self): driver = trunk_driver.OVNTrunkDriver.create(mock.Mock()) with mock.patch.object(registry, 'subscribe') as mock_subscribe: driver.register(mock.ANY, mock.ANY, mock.Mock()) calls = [mock.call.mock_subscribe(mock.ANY, trunk_consts.TRUNK, events.AFTER_CREATE), mock.call.mock_subscribe(mock.ANY, trunk_consts.SUBPORTS, events.AFTER_CREATE), mock.call.mock_subscribe(mock.ANY, trunk_consts.TRUNK, events.AFTER_DELETE), mock.call.mock_subscribe(mock.ANY, trunk_consts.SUBPORTS, events.AFTER_DELETE)] mock_subscribe.assert_has_calls(calls, any_order=True) networking-ovn-4.0.0/networking_ovn/tests/unit/fakes.py0000666000175100017510000005566713245511145023411 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import mock from neutron_lib.api.definitions import l3 from oslo_utils import uuidutils from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils class FakeOvsdbNbOvnIdl(object): def __init__(self, **kwargs): self.lswitch_table = FakeOvsdbTable.create_one_ovsdb_table() self.lsp_table = FakeOvsdbTable.create_one_ovsdb_table() self.lrouter_table = FakeOvsdbTable.create_one_ovsdb_table() self.lrouter_static_route_table = \ FakeOvsdbTable.create_one_ovsdb_table() self.lrp_table = FakeOvsdbTable.create_one_ovsdb_table() self.addrset_table = FakeOvsdbTable.create_one_ovsdb_table() self.acl_table = FakeOvsdbTable.create_one_ovsdb_table() self.dhcp_options_table = FakeOvsdbTable.create_one_ovsdb_table() self.nat_table = FakeOvsdbTable.create_one_ovsdb_table() self._tables = {} self._tables['Logical_Switch'] = self.lswitch_table self._tables['Logical_Switch_Port'] = self.lsp_table self._tables['Logical_Router'] = self.lrouter_table self._tables['Logical_Router_Port'] = self.lrp_table self._tables['Logical_Router_Static_Route'] = \ self.lrouter_static_route_table self._tables['ACL'] = self.acl_table self._tables['Address_Set'] = self.addrset_table self._tables['DHCP_Options'] = self.dhcp_options_table self._tables['NAT'] = self.nat_table self.transaction = mock.MagicMock() self.ls_add = mock.Mock() self.set_lswitch_ext_ids = mock.Mock() self.ls_del = mock.Mock() self.create_lswitch_port = mock.Mock() self.set_lswitch_port = mock.Mock() self.delete_lswitch_port = mock.Mock() self.get_acls_for_lswitches = mock.Mock() self.create_lrouter = mock.Mock() self.lrp_del = mock.Mock() self.update_lrouter = mock.Mock() self.delete_lrouter = mock.Mock() self.add_lrouter_port = mock.Mock() self.update_lrouter_port = mock.Mock() self.delete_lrouter_port = mock.Mock() self.set_lrouter_port_in_lswitch_port = mock.Mock() self.add_acl = mock.Mock() self.delete_acl = mock.Mock() self.update_acls = mock.Mock() self.idl = mock.Mock() self.add_static_route = mock.Mock() self.delete_static_route = mock.Mock() self.create_address_set = mock.Mock() self.update_address_set_ext_ids = mock.Mock() self.delete_address_set = mock.Mock() self.update_address_set = mock.Mock() self.get_all_chassis_gateway_bindings = mock.Mock() self.get_gateway_chassis_binding = mock.Mock() self.get_unhosted_gateways = mock.Mock() self.add_dhcp_options = mock.Mock() self.delete_dhcp_options = mock.Mock() self.get_subnet_dhcp_options = mock.Mock() self.get_subnet_dhcp_options.return_value = { 'subnet': None, 'ports': []} self.get_subnets_dhcp_options = mock.Mock() self.get_subnets_dhcp_options.return_value = [] self.get_all_dhcp_options = mock.Mock() self.get_router_port_options = mock.MagicMock() self.get_router_port_options.return_value = {} self.add_nat_rule_in_lrouter = mock.Mock() self.delete_nat_rule_in_lrouter = mock.Mock() self.add_nat_ip_to_lrport_peer_options = mock.Mock() self.delete_nat_ip_from_lrport_peer_options = mock.Mock() self.get_lrouter_nat_rules = mock.Mock() self.get_lrouter_nat_rules.return_value = [] self.set_nat_rule_in_lrouter = mock.Mock() self.check_for_row_by_value_and_retry = mock.Mock() self.get_parent_port = mock.Mock() self.get_parent_port.return_value = [] self.dns_add = mock.Mock() self.get_lswitch = mock.Mock() fake_ovs_row = FakeOvsdbRow.create_one_ovsdb_row() self.get_lswitch.return_value = fake_ovs_row self.get_ls_and_dns_record = mock.Mock() self.get_ls_and_dns_record.return_value = (fake_ovs_row, None) self.ls_set_dns_records = mock.Mock() self.get_floatingip = mock.Mock() self.get_floatingip.return_value = None self.check_revision_number = mock.Mock() self.lookup = mock.MagicMock() # TODO(lucasagomes): The get_floatingip_by_ips() method is part # of a backwards compatibility layer for the Pike -> Queens release, # remove it in the Rocky release. self.get_floatingip_by_ips = mock.Mock() self.get_floatingip_by_ips.return_value = None self.is_col_present = mock.Mock() self.is_col_present.return_value = False self.get_lrouter = mock.Mock() self.get_lrouter.return_value = None self.delete_lrouter_ext_gw = mock.Mock() self.delete_lrouter_ext_gw.return_value = None class FakeOvsdbSbOvnIdl(object): def __init__(self, **kwargs): self.chassis_exists = mock.Mock() self.chassis_exists.return_value = True self.get_chassis_hostname_and_physnets = mock.Mock() self.get_chassis_hostname_and_physnets.return_value = {} self.get_all_chassis = mock.Mock() self.get_chassis_data_for_ml2_bind_port = mock.Mock() self.get_chassis_data_for_ml2_bind_port.return_value = \ ('fake', '', ['fake-physnet']) class FakeOvsdbTransaction(object): def __init__(self, **kwargs): self.insert = mock.Mock() class FakePlugin(object): def __init__(self, **kwargs): self.get_ports = mock.Mock() self._get_port_security_group_bindings = mock.Mock() class FakeResource(dict): def __init__(self, manager=None, info=None, loaded=False, methods=None): """Set attributes and methods for a resource. :param manager: The resource manager :param Dictionary info: A dictionary with all attributes :param bool loaded: True if the resource is loaded in memory :param Dictionary methods: A dictionary with all methods """ info = info or {} super(FakeResource, self).__init__(info) methods = methods or {} self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) self._add_methods(methods) self._loaded = loaded # Add a revision number by default setattr(self, 'revision_number', 1) @property def db_obj(self): return self def _add_details(self, info): for (k, v) in info.items(): setattr(self, k, v) def _add_methods(self, methods): """Fake methods with MagicMock objects. For each <@key, @value> pairs in methods, add an callable MagicMock object named @key as an attribute, and set the mock's return_value to @value. When users access the attribute with (), @value will be returned, which looks like a function call. """ for (name, ret) in methods.items(): method = mock.MagicMock(return_value=ret) setattr(self, name, method) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) def keys(self): return self._info.keys() def info(self): return self._info def update(self, info): super(FakeResource, self).update(info) self._add_details(info) class FakeNetwork(object): """Fake one or more networks.""" @staticmethod def create_one_network(attrs=None): """Create a fake network. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the network """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() network_attrs = { 'id': 'network-id-' + fake_uuid, 'name': 'network-name-' + fake_uuid, 'status': 'ACTIVE', 'tenant_id': 'project-id-' + fake_uuid, 'admin_state_up': True, 'shared': False, 'subnets': [], 'provider:network_type': 'geneve', 'provider:physical_network': None, 'provider:segmentation_id': 10, 'router:external': False, 'availability_zones': [], 'availability_zone_hints': [], 'is_default': False, } # Overwrite default attributes. network_attrs.update(attrs) return FakeResource(info=copy.deepcopy(network_attrs), loaded=True) class FakeNetworkContext(object): def __init__(self, network, segments): self.fake_network = network self.fake_segments = segments self._plugin_context = mock.MagicMock() @property def current(self): return self.fake_network @property def original(self): return None @property def network_segments(self): return self.fake_segments class FakeSubnetContext(object): def __init__(self, subnet, original_subnet=None, network=None): self.fake_subnet = subnet self.fake_original_subnet = original_subnet self.fake_network = FakeNetworkContext(network, None) @property def current(self): return self.fake_subnet @property def original(self): return self.fake_original_subnet @property def network(self): return self.fake_network class FakeOvsdbRow(FakeResource): """Fake one or more OVSDB rows.""" @staticmethod def create_one_ovsdb_row(attrs=None, methods=None): """Create a fake OVSDB row. :param Dictionary attrs: A dictionary with all attributes :param Dictionary methods: A dictionary with all methods :return: A FakeResource object faking the OVSDB row """ attrs = attrs or {} methods = methods or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() ovsdb_row_attrs = { 'uuid': fake_uuid, 'name': 'name-' + fake_uuid, 'external_ids': {}, } # Set default methods. ovsdb_row_methods = { 'addvalue': None, 'delete': None, 'delvalue': None, 'verify': None, } # Overwrite default attributes and methods. ovsdb_row_attrs.update(attrs) ovsdb_row_methods.update(methods) return FakeResource(info=copy.deepcopy(ovsdb_row_attrs), loaded=True, methods=copy.deepcopy(ovsdb_row_methods)) class FakeOvsdbTable(FakeResource): """Fake one or more OVSDB tables.""" @staticmethod def create_one_ovsdb_table(attrs=None): """Create a fake OVSDB table. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the OVSDB table """ attrs = attrs or {} # Set default attributes. ovsdb_table_attrs = { 'rows': {}, 'columns': {}, } # Overwrite default attributes. ovsdb_table_attrs.update(attrs) return FakeResource(info=copy.deepcopy(ovsdb_table_attrs), loaded=True) class FakePort(object): """Fake one or more ports.""" @staticmethod def create_one_port(attrs=None): """Create a fake port. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the port """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() port_attrs = { 'admin_state_up': True, 'allowed_address_pairs': [{}], 'binding:host_id': 'binding-host-id-' + fake_uuid, 'binding:profile': {}, 'binding:vif_details': {}, 'binding:vif_type': 'ovs', 'binding:vnic_type': 'normal', 'device_id': 'device-id-' + fake_uuid, 'device_owner': 'compute:nova', 'dns_assignment': [{}], 'dns_name': 'dns-name-' + fake_uuid, 'extra_dhcp_opts': [{}], 'fixed_ips': [{'subnet_id': 'subnet-id-' + fake_uuid, 'ip_address': '10.10.10.20'}], 'id': 'port-id-' + fake_uuid, 'mac_address': 'fa:16:3e:a9:4e:72', 'name': 'port-name-' + fake_uuid, 'network_id': 'network-id-' + fake_uuid, 'port_security_enabled': True, 'security_groups': [], 'status': 'ACTIVE', 'tenant_id': 'project-id-' + fake_uuid, } # Overwrite default attributes. port_attrs.update(attrs) return FakeResource(info=copy.deepcopy(port_attrs), loaded=True) class FakePortContext(object): def __init__(self, port, host, segments_to_bind): self.fake_port = port self.fake_host = host self.fake_segments_to_bind = segments_to_bind self.set_binding = mock.Mock() @property def current(self): return self.fake_port @property def host(self): return self.fake_host @property def segments_to_bind(self): return self.fake_segments_to_bind class FakeSecurityGroup(object): """Fake one or more security groups.""" @staticmethod def create_one_security_group(attrs=None): """Create a fake security group. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the security group """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() security_group_attrs = { 'id': 'security-group-id-' + fake_uuid, 'name': 'security-group-name-' + fake_uuid, 'description': 'security-group-description-' + fake_uuid, 'tenant_id': 'project-id-' + fake_uuid, 'security_group_rules': [], } # Overwrite default attributes. security_group_attrs.update(attrs) return FakeResource(info=copy.deepcopy(security_group_attrs), loaded=True) class FakeSecurityGroupRule(object): """Fake one or more security group rules.""" @staticmethod def create_one_security_group_rule(attrs=None): """Create a fake security group rule. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the security group rule """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() security_group_rule_attrs = { 'direction': 'ingress', 'ethertype': 'IPv4', 'id': 'security-group-rule-id-' + fake_uuid, 'port_range_max': 22, 'port_range_min': 22, 'protocol': 'tcp', 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'security_group_id': 'security-group-id-' + fake_uuid, 'tenant_id': 'project-id-' + fake_uuid, } # Overwrite default attributes. security_group_rule_attrs.update(attrs) return FakeResource(info=copy.deepcopy(security_group_rule_attrs), loaded=True) class FakeSegment(object): """Fake one or more segments.""" @staticmethod def create_one_segment(attrs=None): """Create a fake segment. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the segment """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() segment_attrs = { 'id': 'segment-id-' + fake_uuid, 'network_type': 'geneve', 'physical_network': None, 'segmentation_id': 10, } # Overwrite default attributes. segment_attrs.update(attrs) return FakeResource(info=copy.deepcopy(segment_attrs), loaded=True) class FakeSubnet(object): """Fake one or more subnets.""" @staticmethod def create_one_subnet(attrs=None): """Create a fake subnet. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the subnet """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() subnet_attrs = { 'id': 'subnet-id-' + fake_uuid, 'name': 'subnet-name-' + fake_uuid, 'network_id': 'network-id-' + fake_uuid, 'cidr': '10.10.10.0/24', 'tenant_id': 'project-id-' + fake_uuid, 'enable_dhcp': True, 'dns_nameservers': [], 'allocation_pools': [], 'host_routes': [], 'ip_version': 4, 'gateway_ip': '10.10.10.1', 'ipv6_address_mode': 'None', 'ipv6_ra_mode': 'None', 'subnetpool_id': None, } # Overwrite default attributes. subnet_attrs.update(attrs) return FakeResource(info=copy.deepcopy(subnet_attrs), loaded=True) class FakeFloatingIp(object): """Fake one or more floating ips.""" @staticmethod def create_one_fip(attrs=None): """Create a fake floating ip. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the floating ip """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() fip_attrs = { 'id': 'fip-id-' + fake_uuid, 'tenant_id': '', 'fixed_ip_address': '10.0.0.10', 'floating_ip_address': '172.21.0.100', 'router_id': 'router-id', 'port_id': 'port_id', 'fixed_port_id': 'port_id', 'floating_port_id': 'fip-port-id', 'status': 'Active', 'floating_network_id': 'fip-net-id', 'dns': '', 'dns_domain': '', 'dns_name': '', 'project_id': '', } # Overwrite default attributes. fip_attrs.update(attrs) return FakeResource(info=copy.deepcopy(fip_attrs), loaded=True) class FakeOVNPort(object): """Fake one or more ports.""" @staticmethod def create_one_port(attrs=None): """Create a fake ovn port. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object faking the port """ attrs = attrs or {} # Set default attributes. fake_uuid = uuidutils.generate_uuid() port_attrs = { 'addresses': [], 'dhcpv4_options': '', 'dhcpv6_options': [], 'enabled': True, 'external_ids': {}, 'name': fake_uuid, 'options': {}, 'parent_name': [], 'port_security': [], 'tag': [], 'tag_request': [], 'type': '', 'up': False, } # Overwrite default attributes. port_attrs.update(attrs) return type('Logical_Switch_Port', (object, ), port_attrs) @staticmethod def from_neutron_port(port): """Create a fake ovn port based on a neutron port.""" external_ids = { ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: utils.ovn_name(port['network_id']), ovn_const.OVN_SG_IDS_EXT_ID_KEY: ' '.join(port['security_groups']), ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: port.get('device_owner', '')} addresses = [port['mac_address'], ] addresses += [x['ip_address'] for x in port.get('fixed_ips', [])] port_security = ( addresses + [x['ip_address'] for x in port.get('allowed_address_pairs', [])]) return FakeOVNPort.create_one_port( {'external_ids': external_ids, 'addresses': addresses, 'port_security': port_security}) FakeStaticRoute = collections.namedtuple( 'Static_Routes', ['ip_prefix', 'nexthop', 'external_ids']) class FakeOVNRouter(object): @staticmethod def create_one_router(attrs=None): router_attrs = { 'enabled': False, 'external_ids': {}, 'load_balancer': [], 'name': '', 'nat': [], 'options': {}, 'ports': [], 'static_routes': [], } # Overwrite default attributes. router_attrs.update(attrs) return type('Logical_Router', (object, ), router_attrs) @staticmethod def from_neutron_router(router): def _get_subnet_id(gw_info): subnet_id = '' ext_ips = gw_info.get('external_fixed_ips', []) if ext_ips: subnet_id = ext_ips[0]['subnet_id'] return subnet_id external_ids = { ovn_const.OVN_GW_PORT_EXT_ID_KEY: router.get('gw_port_id') or '', ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: router.get('name', 'no_router_name')} # Get the routes routes = [] for r in router.get('routes', []): routes.append(FakeStaticRoute(ip_prefix=r['destination'], nexthop=r['nexthop'], external_ids={})) gw_info = router.get(l3.EXTERNAL_GW_INFO) if gw_info: external_ids = { ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', ovn_const.OVN_SUBNET_EXT_ID_KEY: _get_subnet_id(gw_info)} routes.append(FakeStaticRoute( ip_prefix='0.0.0.0/0', nexthop='', external_ids=external_ids)) return FakeOVNRouter.create_one_router( {'external_ids': external_ids, 'enabled': router.get('admin_state_up') or False, 'name': utils.ovn_name(router['id']), 'static_routes': routes}) networking-ovn-4.0.0/networking_ovn/tests/unit/test_ovn_parent_tag.py0000666000175100017510000000512413245511145026344 0ustar zuulzuul00000000000000# Copyright (c) 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 mock from networking_ovn.common import constants as ovn_const from networking_ovn.tests.unit.ml2 import test_mech_driver OVN_PROFILE = ovn_const.OVN_PORT_BINDING_PROFILE class TestOVNParentTagPortBinding(test_mech_driver.OVNMechanismDriverTestCase): def test_create_port_with_invalid_parent(self): binding = {OVN_PROFILE: {"parent_name": 'invalid', 'tag': 1}} with self.network() as n: with self.subnet(n): self._create_port( self.fmt, n['network']['id'], expected_res_status=404, arg_list=(OVN_PROFILE,), **binding) @mock.patch('neutron.db.db_base_plugin_v2.NeutronDbPluginV2.get_port') def test_create_port_with_parent_and_tag(self, mock_get_port): binding = {OVN_PROFILE: {"parent_name": '', 'tag': 1}} with self.network() as n: with self.subnet(n) as s: with self.port(s) as p: binding[OVN_PROFILE]['parent_name'] = p['port']['id'] res = self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), **binding) port = self.deserialize(self.fmt, res) self.assertEqual(port['port'][OVN_PROFILE], binding[OVN_PROFILE]) mock_get_port.assert_called_with(mock.ANY, p['port']['id']) def test_create_port_with_invalid_tag(self): binding = {OVN_PROFILE: {"parent_name": '', 'tag': 'a'}} with self.network() as n: with self.subnet(n) as s: with self.port(s) as p: binding[OVN_PROFILE]['parent_name'] = p['port']['id'] self._create_port(self.fmt, n['network']['id'], arg_list=(OVN_PROFILE,), expected_res_status=400, **binding) networking-ovn-4.0.0/networking_ovn/tests/functional/0000775000175100017510000000000013245511554023110 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/db/0000775000175100017510000000000013245511554023475 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/db/test_migrations.py0000666000175100017510000000445513245511145027270 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 oslo_config import cfg from neutron.db.migration.alembic_migrations import external from neutron.db.migration import cli as migration from neutron.tests.functional.db import test_migrations from neutron.tests.unit import testlib_api from networking_ovn.db import head # EXTERNAL_TABLES should contain all names of tables that are not related to # current repo. EXTERNAL_TABLES = external.TABLES VERSION_TABLE = 'ovn_alembic_version' class _TestModelsMigrationsOVN(test_migrations._TestModelsMigrations): def db_sync(self, engine): cfg.CONF.set_override('connection', engine.url, group='database') for conf in migration.get_alembic_configs(): self.alembic_config = conf self.alembic_config.neutron_config = cfg.CONF migration.do_alembic_command(conf, 'upgrade', 'heads') def get_metadata(self): return head.get_metadata() def include_object(self, object_, name, type_, reflected, compare_to): if type_ == 'table' and (name.startswith('alembic') or name == VERSION_TABLE or name in EXTERNAL_TABLES): return False if type_ == 'index' and reflected and name.startswith("idx_autoinc_"): return False return True class TestModelsMigrationsMysql(testlib_api.MySQLTestCaseMixin, _TestModelsMigrationsOVN, testlib_api.SqlTestCaseLight): pass class TestModelsMigrationsPostgresql(testlib_api.PostgreSQLTestCaseMixin, _TestModelsMigrationsOVN, testlib_api.SqlTestCaseLight): pass networking-ovn-4.0.0/networking_ovn/tests/functional/db/__init__.py0000666000175100017510000000000013245511145025572 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/test_ovsdb_monitor.py0000666000175100017510000001374713245511164027420 0ustar zuulzuul00000000000000# 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 mock from networking_ovn.ovsdb import ovsdb_monitor from networking_ovn.tests.functional import base from neutron.common import utils as n_utils class TestNBDbMonitor(base.TestOVNFunctionalBase): def setUp(self): super(TestNBDbMonitor, self).setUp(ovn_worker=True) def _test_port_up_down_helper(self, port, ovn_mech_driver): # Set the Logical_Switch_Port.up to True. This is to mock # the vif plug. When the Logical_Switch_Port.up changes from # False to True, ovsdb_monitor should call # mech_driver.set_port_status_up. with self.nb_api.transaction(check_error=True) as txn: txn.add(self.nb_api.set_lswitch_port(port['id'], True, up=True)) ovn_mech_driver.set_port_status_up.assert_called_once_with(port['id']) ovn_mech_driver.set_port_status_down.assert_not_called() # Set the Logical_Switch_Port.up to False. ovsdb_monitor should # call mech_driver.set_port_status_down with self.nb_api.transaction(check_error=True) as txn: txn.add(self.nb_api.set_lswitch_port(port['id'], True, up=False)) ovn_mech_driver.set_port_status_down.assert_called_once_with( port['id']) def test_port_up_down_events(self): """Test the port up down events. This test case creates a port, sets the LogicalSwitchPort.up to True and False to test if the ovsdb monitor handles these events from the ovsdb server and calls the mech_driver functions 'set_port_status_up()' or 'set_port_status_down()' are not. For now mocking the 'set_port_status_up()' and 'set_port_status_down()' OVN mech driver functions to check if these functions are called or not by the ovsdb monitor. Ideally it would have been good to check that the port status is set to ACTIVE when mech_driver.set_port_status_up calls "provisioning_blocks.provisioning_complete". But it is not happening because port.binding.vif_type is unbound. TODO(numans) - Remove the mocking of these functions and instead create the port properly so that vif_type is set to "ovs". """ self.mech_driver.set_port_status_up = mock.Mock() self.mech_driver.set_port_status_down = mock.Mock() with self.port(name='port') as p: p = p['port'] # using the monitor IDL connection to the NB DB, set the # Logical_Switch_Port.up to False first. This is to mock the # ovn-controller setting it to False when the logical switch # port is created. with self.nb_api.transaction(check_error=True) as txn: txn.add(self.nb_api.set_lswitch_port(p['id'], True, up=False)) self._test_port_up_down_helper(p, self.mech_driver) def test_ovsdb_monitor_lock(self): """Test case to test the ovsdb monitor lock used by OvnConnection. This test case created another IDL connection to the NB DB using the ovsdb_monitor.OvnConnection. With this we will have 2 'ovsdb_monitor.OvnConnection's. At the start the lock should be with the IDL connection created by the 'TestOVNFunctionalBase' setup() function. The port up/down events should be handled by the first IDL connection. Then we will restart the first IDL connection so that the 2nd IDL connection created in this test case gets the lock and it should handle the port up/down events. Please note that the "self.monitor_nb_idl_con" created by the base class is created using 'connection.Connection' and hence it will not contend for any lock. """ fake_driver = mock.MagicMock() _idl = ovsdb_monitor.OvnNbIdl.from_server( self.ovsdb_server_mgr.get_ovsdb_connection_path(), 'OVN_Northbound', fake_driver) tst_ovn_conn = self.useFixture( base.ConnectionFixture(idl=_idl, timeout=10)).connection tst_ovn_conn.start() self.mech_driver.set_port_status_up = mock.Mock() self.mech_driver.set_port_status_down = mock.Mock() with self.port(name='port') as p: p = p['port'] with self.nb_api.transaction(check_error=True) as txn: txn.add(self.nb_api.set_lswitch_port(p['id'], True, up=False)) self._test_port_up_down_helper(p, self.mech_driver) fake_driver.set_port_status_up.assert_not_called() fake_driver.set_port_status_down.assert_not_called() # Now restart the mech_driver's IDL connection. self.mech_driver._nb_ovn.idl.force_reconnect() # Wait till the test_ovn_idl_conn has acquired the lock. n_utils.wait_until_true(lambda: tst_ovn_conn.idl.has_lock) self.mech_driver.set_port_status_up.reset_mock() self.mech_driver.set_port_status_down.reset_mock() fake_driver.set_port_status_up.reset_mock() fake_driver.set_port_status_down.reset_mock() self._test_port_up_down_helper(p, fake_driver) self.assertFalse(self.mech_driver.set_port_status_up.called) self.assertFalse(self.mech_driver.set_port_status_down.called) class TestNBDbMonitorOverTcp(TestNBDbMonitor): def get_ovsdb_server_protocol(self): return 'tcp' class TestNBDbMonitorOverSsl(TestNBDbMonitor): def get_ovsdb_server_protocol(self): return 'ssl' networking-ovn-4.0.0/networking_ovn/tests/functional/test_mech_driver.py0000666000175100017510000001323213245511145027007 0ustar zuulzuul00000000000000# 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. from networking_ovn.common import utils from networking_ovn.tests.functional import base from oslo_config import cfg from oslo_utils import uuidutils class TestPortBinding(base.TestOVNFunctionalBase): def setUp(self): super(TestPortBinding, self).setUp() self.ovs_host = 'ovs-host' self.dpdk_host = 'dpdk-host' self.invalid_dpdk_host = 'invalid-host' self.add_fake_chassis(self.ovs_host) self.add_fake_chassis( self.dpdk_host, external_ids={'datapath-type': 'netdev', 'iface-types': 'dummy,dummy-internal,dpdkvhostuser'}) self.add_fake_chassis( self.invalid_dpdk_host, external_ids={'datapath-type': 'netdev', 'iface-types': 'dummy,dummy-internal,geneve,vxlan'}) self.n1 = self._make_network(self.fmt, 'n1', True) res = self._create_subnet(self.fmt, self.n1['network']['id'], '10.0.0.0/24') self.deserialize(self.fmt, res) def _create_or_update_port(self, port_id=None, hostname=None): if port_id is None: port_data = { 'port': {'network_id': self.n1['network']['id'], 'tenant_id': self._tenant_id}} if hostname: port_data['port']['device_id'] = uuidutils.generate_uuid() port_data['port']['device_owner'] = 'compute:None' port_data['port']['binding:host_id'] = hostname port_req = self.new_create_request('ports', port_data, self.fmt) port_res = port_req.get_response(self.api) p = self.deserialize(self.fmt, port_res) port_id = p['port']['id'] else: port_data = { 'port': {'device_id': uuidutils.generate_uuid(), 'device_owner': 'compute:None', 'binding:host_id': hostname}} port_req = self.new_update_request('ports', port_data, port_id, self.fmt) port_res = port_req.get_response(self.api) self.deserialize(self.fmt, port_res) return port_id def _verify_vif_details(self, port_id, expected_host_name, expected_vif_type, expected_vif_details): port_req = self.new_show_request('ports', port_id) port_res = port_req.get_response(self.api) p = self.deserialize(self.fmt, port_res) self.assertEqual(expected_host_name, p['port']['binding:host_id']) self.assertEqual(expected_vif_type, p['port']['binding:vif_type']) self.assertEqual(expected_vif_details, p['port']['binding:vif_details']) def test_port_binding_create_port(self): port_id = self._create_or_update_port(hostname=self.ovs_host) self._verify_vif_details(port_id, self.ovs_host, 'ovs', {'port_filter': True}) port_id = self._create_or_update_port(hostname=self.dpdk_host) expected_vif_details = {'port_filter': False, 'vhostuser_mode': 'client', 'vhostuser_ovs_plug': True} expected_vif_details['vhostuser_socket'] = ( utils.ovn_vhu_sockpath(cfg.CONF.ovn.vhost_sock_dir, port_id)) self._verify_vif_details(port_id, self.dpdk_host, 'vhostuser', expected_vif_details) port_id = self._create_or_update_port(hostname=self.invalid_dpdk_host) self._verify_vif_details(port_id, self.invalid_dpdk_host, 'ovs', {'port_filter': True}) def test_port_binding_update_port(self): port_id = self._create_or_update_port() self._verify_vif_details(port_id, '', 'unbound', {}) port_id = self._create_or_update_port(port_id=port_id, hostname=self.ovs_host) self._verify_vif_details(port_id, self.ovs_host, 'ovs', {'port_filter': True}) port_id = self._create_or_update_port(port_id=port_id, hostname=self.dpdk_host) expected_vif_details = {'port_filter': False, 'vhostuser_mode': 'client', 'vhostuser_ovs_plug': True} expected_vif_details['vhostuser_socket'] = ( utils.ovn_vhu_sockpath(cfg.CONF.ovn.vhost_sock_dir, port_id)) self._verify_vif_details(port_id, self.dpdk_host, 'vhostuser', expected_vif_details) port_id = self._create_or_update_port(port_id=port_id, hostname=self.invalid_dpdk_host) self._verify_vif_details(port_id, self.invalid_dpdk_host, 'ovs', {'port_filter': True}) class TestPortBindingOverTcp(TestPortBinding): def get_ovsdb_server_protocol(self): return 'tcp' class TestPortBindingOverSsl(TestPortBinding): def get_ovsdb_server_protocol(self): return 'ssl' networking-ovn-4.0.0/networking_ovn/tests/functional/test_router.py0000666000175100017510000002073013245511145026041 0ustar zuulzuul00000000000000# 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 mock from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils as ovn_utils from networking_ovn.tests.functional import base from neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import provider_net as pnet from neutron_lib import constants as n_consts from ovsdbapp.backend.ovs_idl import idlutils class TestRouter(base.TestOVNFunctionalBase): def setUp(self): super(TestRouter, self).setUp() self.chassis1 = self.add_fake_chassis( 'ovs-host1', physical_nets=['physnet1', 'physnet3']) self.chassis2 = self.add_fake_chassis( 'ovs-host2', physical_nets=['physnet2', 'physnet3']) def _create_router(self, name, gw_info=None): router = {'router': {'name': name, 'admin_state_up': True, 'tenant_id': self._tenant_id}} if gw_info: router['router']['external_gateway_info'] = gw_info return self.l3_plugin.create_router(self.context, router) def _create_ext_network(self, name, net_type, physnet, seg, gateway, cidr): arg_list = (pnet.NETWORK_TYPE, external_net.EXTERNAL,) net_arg = {pnet.NETWORK_TYPE: net_type, external_net.EXTERNAL: True} if seg: arg_list = arg_list + (pnet.SEGMENTATION_ID,) net_arg[pnet.SEGMENTATION_ID] = seg if physnet: arg_list = arg_list + (pnet.PHYSICAL_NETWORK,) net_arg[pnet.PHYSICAL_NETWORK] = physnet network = self._make_network(self.fmt, name, True, arg_list=arg_list, **net_arg) self._make_subnet(self.fmt, network, gateway, cidr, ip_version=4) return network def _set_redirect_chassis_to_invalid_chassis(self, ovn_client): with ovn_client._nb_idl.transaction(check_error=True) as txn: for lrp in self.nb_api.tables[ 'Logical_Router_Port'].rows.values(): txn.add(ovn_client._nb_idl.update_lrouter_port( lrp.name, gateway_chassis=[ovn_const.OVN_GATEWAY_INVALID_CHASSIS])) def test_gateway_chassis_on_router_gateway_port(self): ext2 = self._create_ext_network( 'ext2', 'flat', 'physnet3', None, "20.0.0.1", "20.0.0.0/24") gw_info = {'network_id': ext2['network']['id']} self._create_router('router1', gw_info=gw_info) expected = [row.name for row in self.sb_api.tables['Chassis'].rows.values()] for row in self.nb_api.tables[ 'Logical_Router_Port'].rows.values(): if self.nb_api.tables.get('Gateway_Chassis'): chassis = [gwc.chassis_name for gwc in row.gateway_chassis] self.assertItemsEqual(expected, chassis) else: rc = row.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY) self.assertIn(rc, expected) def test_gateway_chassis_with_bridge_mappings(self): ovn_client = self.l3_plugin._ovn_client # Create external networks with vlan, flat and geneve network types ext1 = self._create_ext_network( 'ext1', 'vlan', 'physnet1', 1, "10.0.0.1", "10.0.0.0/24") ext2 = self._create_ext_network( 'ext2', 'flat', 'physnet3', None, "20.0.0.1", "20.0.0.0/24") ext3 = self._create_ext_network( 'ext3', 'geneve', None, 10, "30.0.0.1", "30.0.0.0/24") # mock select function and check if it is called with expected # candidates. self.candidates = [] def fake_select(*args, **kwargs): self.assertItemsEqual(self.candidates, kwargs['candidates']) # We are not interested in further processing, let us return # INVALID_CHASSIS to avoid erros return [ovn_const.OVN_GATEWAY_INVALID_CHASSIS] with mock.patch.object(ovn_client._ovn_scheduler, 'select', side_effect=fake_select) as client_select,\ mock.patch.object(self.l3_plugin.scheduler, 'select', side_effect=fake_select) as plugin_select: self.candidates = [self.chassis1] gw_info = {'network_id': ext1['network']['id']} router1 = self._create_router('router1', gw_info=gw_info) # set redirect-chassis to neutron-ovn-invalid-chassis, so # that schedule_unhosted_gateways will try to schedule it self._set_redirect_chassis_to_invalid_chassis(ovn_client) self.l3_plugin.schedule_unhosted_gateways() self.candidates = [self.chassis1, self.chassis2] gw_info = {'network_id': ext2['network']['id']} self.l3_plugin.update_router( self.context, router1['id'], {'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}}) self._set_redirect_chassis_to_invalid_chassis(ovn_client) self.l3_plugin.schedule_unhosted_gateways() self.candidates = [] gw_info = {'network_id': ext3['network']['id']} self.l3_plugin.update_router( self.context, router1['id'], {'router': {l3_apidef.EXTERNAL_GW_INFO: gw_info}}) self._set_redirect_chassis_to_invalid_chassis(ovn_client) self.l3_plugin.schedule_unhosted_gateways() # Check ovn_client._ovn_scheduler.select called for router # create and updates self.assertEqual(3, client_select.call_count) # Check self.l3_plugin.scheduler.select called for # schedule_unhosted_gateways self.assertEqual(3, plugin_select.call_count) def _validate_router_ipv6_ra_configs(self, lrp_name, expected_ra_confs): lrp = idlutils.row_by_value(self.nb_api.idl, 'Logical_Router_Port', 'name', lrp_name) self.assertEqual(expected_ra_confs, lrp.ipv6_ra_configs) def _test_router_port_ipv6_ra_configs_helper( self, cidr='aef0::/64', ip_version=6, address_mode=n_consts.IPV6_SLAAC,): router1 = self._create_router('router1') n1 = self._make_network(self.fmt, 'n1', True) if ip_version == 6: kwargs = {'ip_version': 6, 'cidr': 'aef0::/64', 'ipv6_address_mode': address_mode, 'ipv6_ra_mode': address_mode} else: kwargs = {'ip_version': 4, 'cidr': '10.0.0.0/24'} res = self._create_subnet(self.fmt, n1['network']['id'], **kwargs) n1_s1 = self.deserialize(self.fmt, res) n1_s1_id = n1_s1['subnet']['id'] router_iface_info = self.l3_plugin.add_router_interface( self.context, router1['id'], {'subnet_id': n1_s1_id}) lrp_name = ovn_utils.ovn_lrouter_port_name( router_iface_info['port_id']) if ip_version == 6: expected_ra_configs = { 'address_mode': ovn_utils.get_ovn_ipv6_address_mode( address_mode), 'send_periodic': 'true', 'mtu': '1450'} else: expected_ra_configs = {} self._validate_router_ipv6_ra_configs(lrp_name, expected_ra_configs) def test_router_port_ipv6_ra_configs_addr_mode_slaac(self): self._test_router_port_ipv6_ra_configs_helper() def test_router_port_ipv6_ra_configs_addr_mode_dhcpv6_stateful(self): self._test_router_port_ipv6_ra_configs_helper( address_mode=n_consts.DHCPV6_STATEFUL) def test_router_port_ipv6_ra_configs_addr_mode_dhcpv6_stateless(self): self._test_router_port_ipv6_ra_configs_helper( address_mode=n_consts.DHCPV6_STATELESS) def test_router_port_ipv6_ra_configs_ipv4(self): self._test_router_port_ipv6_ra_configs_helper( ip_version=4) networking-ovn-4.0.0/networking_ovn/tests/functional/test_impl_idl.py0000666000175100017510000001460313245511145026314 0ustar zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 uuid from ovsdbapp import event as ovsdb_event from ovsdbapp.tests.functional import base from ovsdbapp.tests.functional.schema.ovn_southbound import test_impl_idl as \ test_sb from ovsdbapp.tests import utils from networking_ovn.ovsdb import impl_idl_ovn as impl class WaitForPortBindingEvent(test_sb.WaitForPortBindingEvent): def run(self, event, row, old): self.row = row super(WaitForPortBindingEvent, self).run(event, row, old) class TestSbApi(base.FunctionalTestCase): schemas = ['OVN_Southbound', 'OVN_Northbound'] def setUp(self): super(TestSbApi, self).setUp() self.data = { 'chassis': [ {'external_ids': {'ovn-bridge-mappings': 'public:br-ex,private:br-0'}}, {'external_ids': {'ovn-bridge-mappings': 'public:br-ex,public2:br-ex'}}, {'external_ids': {'ovn-bridge-mappings': 'public:br-ex'}}, ] } self.api = impl.OvsdbSbOvnIdl(self.connection['OVN_Southbound']) self.nbapi = impl.OvsdbNbOvnIdl(self.connection['OVN_Northbound']) self.load_test_data() self.handler = ovsdb_event.RowEventHandler() self.api.idl.notify = self.handler.notify def load_test_data(self): with self.api.transaction(check_error=True) as txn: for i, chassis in enumerate(self.data['chassis']): chassis['name'] = utils.get_rand_device_name('chassis') chassis['hostname'] = '%s.localdomain.com' % chassis['name'] txn.add(self.api.chassis_add( chassis['name'], ['geneve'], '192.0.2.%d' % (i + 1,), hostname=chassis['hostname'], external_ids=chassis['external_ids'])) def test_get_chassis_hostname_and_physnets(self): mapping = self.api.get_chassis_hostname_and_physnets() self.assertTrue(len(self.data['chassis']) <= len(mapping)) self.assertTrue(set(mapping.keys()) >= {c['hostname'] for c in self.data['chassis']}) def test_get_all_chassis(self): chassis_list = set(self.api.get_all_chassis()) our_chassis = {c['name'] for c in self.data['chassis']} self.assertTrue(our_chassis <= chassis_list) def test_get_chassis_data_for_ml2_bind_port(self): host = self.data['chassis'][0]['hostname'] dp, iface, phys = self.api.get_chassis_data_for_ml2_bind_port(host) self.assertEqual(dp, '') self.assertEqual(iface, '') self.assertItemsEqual(phys, ['private', 'public']) def test_chassis_exists(self): self.assertTrue(self.api.chassis_exists( self.data['chassis'][0]['hostname'])) self.assertFalse(self.api.chassis_exists("nochassishere")) def test_get_chassis_and_physnets(self): mapping = self.api.get_chassis_and_physnets() self.assertTrue(len(self.data['chassis']) <= len(mapping)) self.assertTrue(set(mapping.keys()) >= {c['name'] for c in self.data['chassis']}) def _add_switch_port(self, chassis_name, type='localport'): sname, pname = (utils.get_rand_device_name(prefix=p) for p in ('switch', 'port')) chassis = self.api.lookup('Chassis', chassis_name) row_event = WaitForPortBindingEvent(pname) self.handler.watch_event(row_event) with self.nbapi.transaction(check_error=True) as txn: switch = txn.add(self.nbapi.ls_add(sname)) port = txn.add(self.nbapi.lsp_add(sname, pname, type=type)) row_event.wait() return chassis, switch.result, port.result, row_event.row def test_get_metadata_port_network(self): chassis, switch, port, binding = self._add_switch_port( self.data['chassis'][0]['name']) result = self.api.get_metadata_port_network(str(binding.datapath.uuid)) self.assertEqual(binding, result) self.assertEqual(binding.datapath.external_ids['logical-switch'], str(switch.uuid)) def test_get_metadata_port_network_missing(self): val = str(uuid.uuid4()) self.assertIsNone(self.api.get_metadata_port_network(val)) def test_set_get_chassis_metadata_networks(self): name = self.data['chassis'][0]['name'] nets = [str(uuid.uuid4()) for _ in range(3)] self.api.set_chassis_metadata_networks(name, nets).execute( check_error=True) self.assertEqual(nets, self.api.get_chassis_metadata_networks(name)) def test_get_network_port_bindings_by_ip(self): chassis, switch, port, binding = self._add_switch_port( self.data['chassis'][0]['name']) mac = 'de:ad:be:ef:4d:ad' ipaddr = '192.0.2.1' self.nbapi.lsp_set_addresses( port.name, ['%s %s' % (mac, ipaddr)]).execute(check_error=True) self.api.lsp_bind(port.name, chassis.name).execute(check_error=True) result = self.api.get_network_port_bindings_by_ip( str(binding.datapath.uuid), ipaddr) self.assertIn(binding, result) def test_get_ports_on_chassis(self): chassis, switch, port, binding = self._add_switch_port( self.data['chassis'][0]['name']) self.api.lsp_bind(port.name, chassis.name).execute(check_error=True) self.assertEqual([binding], self.api.get_ports_on_chassis(chassis.name)) def test_get_logical_port_chassis_and_datapath(self): chassis, switch, port, binding = self._add_switch_port( self.data['chassis'][0]['name']) self.api.lsp_bind(port.name, chassis.name).execute(check_error=True) self.assertEqual( (chassis.name, str(binding.datapath.uuid)), self.api.get_logical_port_chassis_and_datapath(port.name)) networking-ovn-4.0.0/networking_ovn/tests/functional/base.py0000666000175100017510000002263713245511164024405 0ustar zuulzuul00000000000000# 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 os import time import fixtures import mock from neutron.conf.plugins.ml2 import config from neutron.plugins.ml2.drivers import type_geneve # noqa from neutron.tests.unit.plugins.ml2 import test_plugin from neutron_lib.plugins import constants from neutron_lib.plugins import directory from oslo_config import cfg from oslo_log import log from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import command from ovsdbapp.backend.ovs_idl import connection # Load all the models to register them into SQLAlchemy metadata before using # the SqlFixture from networking_ovn.db import models # noqa from networking_ovn.ovsdb import impl_idl_ovn from networking_ovn.ovsdb import ovsdb_monitor from networking_ovn.tests import base from networking_ovn.tests.functional.resources import process LOG = log.getLogger(__name__) # This is the directory from which infra fetches log files for functional tests DEFAULT_LOG_DIR = os.path.join(os.environ.get('OS_LOG_PATH', '/tmp'), 'dsvm-functional-logs') class AddFakeChassisCommand(command.BaseCommand): """Add a fake chassis in OVN SB DB for functional test.""" def __init__(self, api, name, ip, **columns): super(AddFakeChassisCommand, self).__init__(api) self.name = name self.ip = ip self.columns = columns def run_idl(self, txn): encap_row = txn.insert(self.api._tables['Encap']) encap_row.type = 'geneve' encap_row.ip = self.ip self.columns.update({'encaps': [encap_row.uuid]}) row = txn.insert(self.api._tables['Chassis']) row.name = self.name for col, val in self.columns.items(): setattr(row, col, val) class ConnectionFixture(fixtures.Fixture): def __init__(self, idl=None, constr=None, schema=None, timeout=60): self.idl = idl or ovsdb_monitor.BaseOvnIdl.from_server( constr, schema) self.connection = connection.Connection( idl=self.idl, timeout=timeout) def _setUp(self): self.addCleanup(self.stop) self.connection.start() def stop(self): self.connection.stop() class TestOVNFunctionalBase(test_plugin.Ml2PluginV2TestCase): # Please see networking_ovn/tests/contrib/gate_hook.sh. # It installs openvswitch in the '/usr/local' path and the ovn-nb schema # file will be present in this path. OVS_INSTALL_SHARE_PATH = '/usr/local/share/openvswitch' _mechanism_drivers = ['logger', 'ovn'] _extension_drivers = ['port_security'] l3_plugin = 'networking_ovn.l3.l3_ovn.OVNL3RouterPlugin' def setUp(self, ovn_worker=False): config.cfg.CONF.set_override('extension_drivers', self._extension_drivers, group='ml2') config.cfg.CONF.set_override('tenant_network_types', ['geneve'], group='ml2') config.cfg.CONF.set_override('vni_ranges', ['1:65536'], group='ml2_type_geneve') super(TestOVNFunctionalBase, self).setUp() base.setup_test_logging( cfg.CONF, DEFAULT_LOG_DIR, "%s.txt" % self.id()) mm = directory.get_plugin().mechanism_manager self.mech_driver = mm.mech_drivers['ovn'].obj self.l3_plugin = directory.get_plugin(constants.L3) self.ovsdb_server_mgr = None self.ovn_worker = ovn_worker self._start_ovsdb_server_and_idls() def get_additional_service_plugins(self): p = super(TestOVNFunctionalBase, self).get_additional_service_plugins() p.update({'revision_plugin_name': 'revisions'}) return p @property def _ovsdb_protocol(self): return self.get_ovsdb_server_protocol() def get_ovsdb_server_protocol(self): return 'unix' def _start_ovsdb_server_and_idls(self): self.temp_dir = self.useFixture(fixtures.TempDir()).path # Start 2 ovsdb-servers one each for OVN NB DB and OVN SB DB # ovsdb-server with OVN SB DB can be used to test the chassis up/down # events. mgr = self.ovsdb_server_mgr = self.useFixture( process.OvsdbServer(self.temp_dir, self.OVS_INSTALL_SHARE_PATH, ovn_nb_db=True, ovn_sb_db=True, protocol=self._ovsdb_protocol)) set_cfg = cfg.CONF.set_override set_cfg('ovn_nb_connection', self.ovsdb_server_mgr.get_ovsdb_connection_path(), 'ovn') set_cfg('ovn_sb_connection', self.ovsdb_server_mgr.get_ovsdb_connection_path( db_type='sb'), 'ovn') set_cfg('ovn_nb_private_key', self.ovsdb_server_mgr.private_key, 'ovn') set_cfg('ovn_nb_certificate', self.ovsdb_server_mgr.certificate, 'ovn') set_cfg('ovn_nb_ca_cert', self.ovsdb_server_mgr.ca_cert, 'ovn') set_cfg('ovn_sb_private_key', self.ovsdb_server_mgr.private_key, 'ovn') set_cfg('ovn_sb_certificate', self.ovsdb_server_mgr.certificate, 'ovn') set_cfg('ovn_sb_ca_cert', self.ovsdb_server_mgr.ca_cert, 'ovn') num_attempts = 0 # 5 seconds should be more than enough for the transaction to complete # for the test cases. # This also fixes the bug #1607639. cfg.CONF.set_override( 'ovsdb_connection_timeout', 5, 'ovn') # Created monitor IDL connection to the OVN NB DB. # This monitor IDL connection can be used to # - Verify that the ML2 OVN driver has written to the OVN NB DB # as expected. # - Create and delete resources in OVN NB DB outside of the # ML2 OVN driver scope to test scenarios like ovn_nb_sync. while num_attempts < 3: try: con = self.useFixture(ConnectionFixture( constr=mgr.get_ovsdb_connection_path(), schema='OVN_Northbound')).connection self.nb_api = impl_idl_ovn.OvsdbNbOvnIdl(con) break except Exception: LOG.exception("Error connecting to the OVN_Northbound DB") num_attempts += 1 time.sleep(1) num_attempts = 0 # Create monitor IDL connection to the OVN SB DB. # This monitor IDL connection can be used to # - Create chassis rows # - Update chassis columns etc. while num_attempts < 3: try: con = self.useFixture(ConnectionFixture( constr=mgr.get_ovsdb_connection_path('sb'), schema='OVN_Southbound')).connection self.sb_api = impl_idl_ovn.OvsdbSbOvnIdl(con) break except Exception: LOG.exception("Error connecting to the OVN_Southbound DB") num_attempts += 1 time.sleep(1) trigger = mock.MagicMock() if self.ovn_worker: trigger.im_class = ovsdb_monitor.OvnWorker cfg.CONF.set_override('neutron_sync_mode', 'off', 'ovn') trigger.im_class.__name__ = 'trigger' self.addCleanup(self.stop) # mech_driver.post_fork_initialize creates the IDL connections self.mech_driver.post_fork_initialize(mock.ANY, mock.ANY, trigger) def stop(self): if self.ovn_worker: self.mech_driver.nb_synchronizer.stop() self.mech_driver.sb_synchronizer.stop() self.mech_driver._nb_ovn.ovsdb_connection.stop() self.mech_driver._sb_ovn.ovsdb_connection.stop() def restart(self): self.stop() # The OVN sync test starts its own synchronizers... self.l3_plugin._nb_ovn_idl.ovsdb_connection.stop() self.l3_plugin._sb_ovn_idl.ovsdb_connection.stop() # Stop our monitor connections self.nb_api.ovsdb_connection.stop() self.sb_api.ovsdb_connection.stop() if self.ovsdb_server_mgr: self.ovsdb_server_mgr.stop() self.mech_driver._nb_ovn = None self.mech_driver._sb_ovn = None self.l3_plugin._nb_ovn_idl = None self.l3_plugin._sb_ovn_idl = None self.nb_api.ovsdb_connection = None self.sb_api.ovsdb_connection = None self._start_ovsdb_server_and_idls() def add_fake_chassis(self, host, physical_nets=None, external_ids=None): physical_nets = physical_nets or [] external_ids = external_ids or {} bridge_mapping = ",".join(["%s:br-provider%s" % (phys_net, i) for i, phys_net in enumerate(physical_nets)]) name = uuidutils.generate_uuid() external_ids['ovn-bridge-mappings'] = bridge_mapping self.sb_api.chassis_add( name, ['geneve'], '172.24.4.10', external_ids=external_ids, hostname=host).execute(check_error=True) return name networking-ovn-4.0.0/networking_ovn/tests/functional/test_qos_driver.py0000666000175100017510000001541613245511145026703 0ustar zuulzuul00000000000000# Copyright 2017 DtDream Technology 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. from networking_ovn.tests.functional import base from neutron.extensions import qos as qos_ext from neutron.tests.unit.api import test_extensions from ovsdbapp.backend.ovs_idl import idlutils class QoSTestExtensionManager(object): def get_resources(self): return qos_ext.Qos.get_resources() def get_actions(self): return [] def get_request_extensions(self): return [] class TestOVNQosDriver(base.TestOVNFunctionalBase): _extension_drivers = ['qos'] def setUp(self): super(TestOVNQosDriver, self).setUp() qos_mgr = QoSTestExtensionManager() self.resource_prefix_map = {'policies': '/qos'} self.qos_api = test_extensions.setup_extensions_middleware(qos_mgr) def get_additional_service_plugins(self): p = super(TestOVNQosDriver, self).get_additional_service_plugins() p.update({'qos_plugin_name': 'qos'}) return p def _test_qos_policy_create(self): data = {'policy': {'name': 'test-policy', 'tenant_id': self._tenant_id}} policy_req = self.new_create_request('policies', data, self.fmt) policy_res = policy_req.get_response(self.qos_api) policy = self.deserialize(self.fmt, policy_res)['policy'] return policy['id'] def _test_qos_policy_rule_create(self, policy_id, max_burst, max_bw): data = {'bandwidth_limit_rule': {'max_burst_kbps': max_burst, 'max_kbps': max_bw, 'tenant_id': self._tenant_id}} policy_rule_req = self.new_create_request( 'policies', data, self.fmt, policy_id, 'bandwidth_limit_rules') policy_rule_res = policy_rule_req.get_response(self.qos_api) policy_rule = self.deserialize(self.fmt, policy_rule_res)['bandwidth_limit_rule'] return policy_rule['id'] def _test_qos_policy_rule_update( self, policy_id, rule_id, max_burst, max_bw): data = {'bandwidth_limit_rule': {'max_burst_kbps': max_burst, 'max_kbps': max_bw}} policy_rule_req = self.new_update_request( 'policies', data, policy_id, self.fmt, subresource='bandwidth_limit_rules' + '/' + rule_id) policy_rule_req.get_response(self.qos_api) def _test_qos_policy_rule_delete( self, policy_id, rule_id): policy_rule_req = self.new_delete_request( 'policies', policy_id, self.fmt, subresource='bandwidth_limit_rules', sub_id=rule_id) policy_rule_req.get_response(self.qos_api) def _test_port_create(self, network_id, policy_id=None): data = {'port': {'network_id': network_id, 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'qos_policy_id': policy_id}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p1 = self.deserialize(self.fmt, port_res)['port'] return p1['id'] def _test_port_update(self, port_id, policy_id): data = {'port': {'qos_policy_id': policy_id}} port_req = self.new_update_request('ports', data, port_id, self.fmt) port_req.get_response(self.api) def _verify_qos_option_row_for_port(self, port_id, expected_lsp_qos_options): lsp = idlutils.row_by_value(self.nb_api.idl, 'Logical_Switch_Port', 'name', port_id, None) observed_lsp_qos_options = {} if lsp.options: if 'qos_burst' in lsp.options: observed_lsp_qos_options['qos_burst'] = lsp.options.get( 'qos_burst') if 'qos_max_rate' in lsp.options: observed_lsp_qos_options['qos_max_rate'] = lsp.options.get( 'qos_max_rate') self.assertEqual(expected_lsp_qos_options, observed_lsp_qos_options) def test_port_qos_options_add_and_remove(self): expected_burst = 100 expected_max_rate = 1 network_id = self._make_network(self.fmt, 'n1', True)['network']['id'] self._create_subnet(self.fmt, network_id, '10.0.0.0/24') port_id = self._test_port_create(network_id) policy_id = self._test_qos_policy_create() self._test_qos_policy_rule_create( policy_id, expected_burst, expected_max_rate) # port add QoS policy self._test_port_update(port_id, policy_id) expected_options = { 'qos_burst': str(expected_burst * 1000), 'qos_max_rate': str(expected_max_rate * 1000), } self._verify_qos_option_row_for_port(port_id, expected_options) # port remove QoS policy self._test_port_update(port_id, None) self._verify_qos_option_row_for_port(port_id, {}) def test_port_qos_options_with_rule(self): expected_burst = 100 expected_max_rate = 1 network_id = self._make_network(self.fmt, 'n1', True)['network']['id'] self._create_subnet(self.fmt, network_id, '10.0.0.0/24') policy_id = self._test_qos_policy_create() policy_rule_id = self._test_qos_policy_rule_create( policy_id, expected_burst, expected_max_rate) port_id = self._test_port_create(network_id, policy_id) # check qos options expected_options = { 'qos_burst': str(expected_burst * 1000), 'qos_max_rate': str(expected_max_rate * 1000), } self._verify_qos_option_row_for_port(port_id, expected_options) # update qos rule self._test_qos_policy_rule_update( policy_id, policy_rule_id, expected_burst * 2, expected_max_rate * 2) expected_options = { 'qos_burst': str(expected_burst * 2 * 1000), 'qos_max_rate': str(expected_max_rate * 2 * 1000), } self._verify_qos_option_row_for_port(port_id, expected_options) # delete qos rule self._test_qos_policy_rule_delete(policy_id, policy_rule_id) self._verify_qos_option_row_for_port(port_id, {}) networking-ovn-4.0.0/networking_ovn/tests/functional/test_revision_numbers.py0000666000175100017510000002076413245511164030122 0ustar zuulzuul00000000000000# Copyright 2017 Red Hat, 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 networking_ovn.common import constants as ovn_const from networking_ovn.tests.functional import base class TestRevisionNumbers(base.TestOVNFunctionalBase): def _create_network(self, name): data = {'network': {'name': name, 'tenant_id': self._tenant_id}} req = self.new_create_request('networks', data, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['network'] def _update_network_name(self, net_id, new_name): data = {'network': {'name': new_name}} req = self.new_update_request('networks', data, net_id, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['network'] def _find_network_row_by_name(self, name): for row in self.nb_api._tables['Logical_Switch'].rows.values(): if (row.external_ids.get( ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY) == name): return row def _create_port(self, name, net_id): data = {'port': {'name': name, 'tenant_id': self._tenant_id, 'network_id': net_id}} req = self.new_create_request('ports', data, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['port'] def _update_port_name(self, port_id, new_name): data = {'port': {'name': new_name}} req = self.new_update_request('ports', data, port_id, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['port'] def _find_port_row_by_name(self, name): for row in self.nb_api._tables['Logical_Switch_Port'].rows.values(): if (row.external_ids.get( ovn_const.OVN_PORT_NAME_EXT_ID_KEY) == name): return row def _create_router(self, name): data = {'router': {'name': name, 'tenant_id': self._tenant_id}} req = self.new_create_request('routers', data, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['router'] def _update_router_name(self, net_id, new_name): data = {'router': {'name': new_name}} req = self.new_update_request('routers', data, net_id, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['router'] def _find_router_row_by_name(self, name): for row in self.nb_api._tables['Logical_Router'].rows.values(): if (row.external_ids.get( ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY) == name): return row def _create_subnet(self, net_id, cidr, name='subnet1'): data = {'subnet': {'name': name, 'tenant_id': self._tenant_id, 'network_id': net_id, 'cidr': cidr, 'ip_version': 4, 'enable_dhcp': True}} req = self.new_create_request('subnets', data, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['subnet'] def _update_subnet_name(self, subnet_id, new_name): data = {'subnet': {'name': new_name}} req = self.new_update_request('subnets', data, subnet_id, self.fmt) res = req.get_response(self.api) return self.deserialize(self.fmt, res)['subnet'] def _find_subnet_row_by_id(self, subnet_id): for row in self.nb_api._tables['DHCP_Options'].rows.values(): if (row.external_ids.get('subnet_id') == subnet_id and not row.external_ids.get('port_id')): return row def test_create_network(self): name = 'net1' neutron_net = self._create_network(name) ovn_net = self._find_network_row_by_name(name) ovn_revision = ovn_net.external_ids[ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(2), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(neutron_net['revision_number']), ovn_revision) def test_update_network(self): new_name = 'netnew1' neutron_net = self._create_network('net1') updated_net = self._update_network_name(neutron_net['id'], new_name) ovn_net = self._find_network_row_by_name(new_name) ovn_revision = ovn_net.external_ids[ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(3), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(updated_net['revision_number']), ovn_revision) def test_create_port(self): name = 'port1' neutron_net = self._create_network('net1') neutron_port = self._create_port(name, neutron_net['id']) ovn_port = self._find_port_row_by_name(name) ovn_revision = ovn_port.external_ids[ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(2), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(neutron_port['revision_number']), ovn_revision) def test_update_port(self): new_name = 'portnew1' neutron_net = self._create_network('net1') neutron_port = self._create_port('port1', neutron_net['id']) updated_port = self._update_port_name(neutron_port['id'], new_name) ovn_port = self._find_port_row_by_name(new_name) ovn_revision = ovn_port.external_ids[ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(3), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(updated_port['revision_number']), ovn_revision) def test_create_router(self): name = 'router1' neutron_router = self._create_router(name) ovn_router = self._find_router_row_by_name(name) ovn_revision = ovn_router.external_ids[ ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(0), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(neutron_router['revision_number']), ovn_revision) def test_update_router(self): new_name = 'newrouter' neutron_router = self._create_router('router1') updated_router = self._update_router_name(neutron_router['id'], new_name) ovn_router = self._find_router_row_by_name(new_name) ovn_revision = ovn_router.external_ids[ ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(1), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(updated_router['revision_number']), ovn_revision) def test_create_subnet(self): neutron_net = self._create_network('net1') neutron_subnet = self._create_subnet(neutron_net['id'], '10.0.0.0/24') ovn_subnet = self._find_subnet_row_by_id(neutron_subnet['id']) ovn_revision = ovn_subnet.external_ids[ ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(0), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(neutron_subnet['revision_number']), ovn_revision) def test_update_subnet(self): neutron_net = self._create_network('net1') neutron_subnet = self._create_subnet(neutron_net['id'], '10.0.0.0/24') updated_subnet = self._update_subnet_name( neutron_subnet['id'], 'newsubnet') ovn_subnet = self._find_subnet_row_by_id(neutron_subnet['id']) ovn_revision = ovn_subnet.external_ids[ ovn_const.OVN_REV_NUM_EXT_ID_KEY] self.assertEqual(str(1), ovn_revision) # Assert it also matches with the newest returned by neutron API self.assertEqual(str(updated_subnet['revision_number']), ovn_revision) # TODO(lucasagomes): Add a test for floating IPs here when we get # the router stuff done. networking-ovn-4.0.0/networking_ovn/tests/functional/__init__.py0000666000175100017510000000000013245511145025205 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/test_ovn_db_sync.py0000666000175100017510000021665613245511145027042 0ustar zuulzuul00000000000000# 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. from neutron.services.segments import db as segments_db from neutron.tests.unit.api import test_extensions from neutron.tests.unit.extensions import test_extraroute from neutron.tests.unit.extensions import test_securitygroup from neutron_lib.api.definitions import dns as dns_apidef from neutron_lib.api.definitions import l3 from neutron_lib import constants from neutron_lib import context from neutron_lib.plugins import directory from oslo_utils import uuidutils from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn.common import acl as acl_utils from networking_ovn.common import config as ovn_config from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils from networking_ovn import ovn_db_sync from networking_ovn.tests.functional import base class TestOvnNbSync(base.TestOVNFunctionalBase): _extension_drivers = ['port_security', 'dns'] def setUp(self): ovn_config.cfg.CONF.set_override('dns_domain', 'ovn.test') super(TestOvnNbSync, self).setUp() ext_mgr = test_extraroute.ExtraRouteTestExtensionManager() self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) sg_mgr = test_securitygroup.SecurityGroupTestExtensionManager() self._sg_api = test_extensions.setup_extensions_middleware(sg_mgr) self.create_lswitches = [] self.create_lswitch_ports = [] self.create_lrouters = [] self.create_lrouter_ports = [] self.create_lrouter_routes = [] self.create_lrouter_nats = [] self.update_lrouter_ports = [] self.create_acls = [] self.delete_lswitches = [] self.delete_lswitch_ports = [] self.delete_lrouters = [] self.delete_lrouter_ports = [] self.delete_lrouter_routes = [] self.delete_lrouter_nats = [] self.delete_acls = [] self.create_address_sets = [] self.delete_address_sets = [] self.update_address_sets = [] self.expected_dhcp_options_rows = [] self.reset_lport_dhcpv4_options = [] self.reset_lport_dhcpv6_options = [] self.stale_lport_dhcpv4_options = [] self.stale_lport_dhcpv6_options = [] self.orphaned_lport_dhcp_options = [] self.lport_dhcpv4_disabled = {} self.lport_dhcpv6_disabled = {} self.missed_dhcp_options = [] self.dirty_dhcp_options = [] self.lport_dhcp_ignored = [] self.match_old_mac_dhcp_subnets = [] self.expected_dns_records = [] ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', True, group='ovn') def _api_for_resource(self, resource): if resource in ['security-groups']: return self._sg_api else: return super(TestOvnNbSync, self)._api_for_resource(resource) def _create_resources(self, restart_ovsdb_processes=False): net_kwargs = {dns_apidef.DNSDOMAIN: 'ovn.test.'} net_kwargs['arg_list'] = (dns_apidef.DNSDOMAIN,) res = self._create_network(self.fmt, 'n1', True, **net_kwargs) n1 = self.deserialize(self.fmt, res) self.expected_dns_records = [ {'external_ids': {'ls_name': utils.ovn_name(n1['network']['id'])}, 'records': {}} ] res = self._create_subnet(self.fmt, n1['network']['id'], '10.0.0.0/24') n1_s1 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n1['network']['id'], '2001:dba::/64', ip_version=6, enable_dhcp=True) n1_s2 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n1['network']['id'], '2001:dbb::/64', ip_version=6, ipv6_address_mode='slaac', ipv6_ra_mode='slaac') n1_s3 = self.deserialize(self.fmt, res) self.expected_dhcp_options_rows.append({ 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': n1_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'classless_static_route': '{169.254.169.254/32,10.0.0.2, 0.0.0.0/0,10.0.0.1}', 'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': n1_s1['subnet']['gateway_ip']}}) self.expected_dhcp_options_rows.append({ 'cidr': '2001:dba::/64', 'external_ids': {'subnet_id': n1_s2['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'server_id': '01:02:03:04:05:06'}}) n1_s1_dhcp_options_uuid = ( self.mech_driver._nb_ovn.get_subnet_dhcp_options( n1_s1['subnet']['id'])['subnet']['uuid']) n1_s2_dhcpv6_options_uuid = ( self.mech_driver._nb_ovn.get_subnet_dhcp_options( n1_s2['subnet']['id'])['subnet']['uuid']) update_port_ids_v4 = [] update_port_ids_v6 = [] n1_port_dict = {} for p in ['p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7']: if p in ['p1', 'p5']: port_kwargs = {'arg_list': (dns_apidef.DNSNAME,), dns_apidef.DNSNAME: 'n1-' + p, 'device_id': 'n1-' + p} else: port_kwargs = {} res = self._create_port(self.fmt, n1['network']['id'], name='n1-' + p, device_owner='compute:None', **port_kwargs) port = self.deserialize(self.fmt, res) n1_port_dict[p] = port['port']['id'] lport_name = port['port']['id'] lswitch_name = 'neutron-' + n1['network']['id'] if p in ['p1', 'p5']: port_ips = " ".join([f['ip_address'] for f in port['port']['fixed_ips']]) hname = 'n1-' + p self.expected_dns_records[0]['records'][hname] = port_ips hname = 'n1-' + p + '.ovn.test.' self.expected_dns_records[0]['records'][hname] = port_ips if p == 'p1': fake_subnet = {'cidr': '11.11.11.11/24'} dhcp_acls = acl_utils.add_acl_dhcp(port['port'], fake_subnet) for dhcp_acl in dhcp_acls: self.create_acls.append(dhcp_acl) elif p == 'p2': self.delete_lswitch_ports.append((lport_name, lswitch_name)) update_port_ids_v4.append(port['port']['id']) update_port_ids_v6.append(port['port']['id']) self.expected_dhcp_options_rows.append({ 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': n1_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0', 'port_id': port['port']['id']}, 'options': { 'classless_static_route': '{169.254.169.254/32,10.0.0.2, 0.0.0.0/0,10.0.0.1}', 'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': n1_s1['subnet']['gateway_ip'], 'tftp_server': '20.0.0.20', 'dns_server': '8.8.8.8'}}) self.expected_dhcp_options_rows.append({ 'cidr': '2001:dba::/64', 'external_ids': {'subnet_id': n1_s2['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0', 'port_id': port['port']['id']}, 'options': {'server_id': '01:02:03:04:05:06', 'domain_search': 'foo-domain'}}) self.dirty_dhcp_options.append({ 'subnet_id': n1_s1['subnet']['id'], 'port_id': lport_name}) self.dirty_dhcp_options.append({ 'subnet_id': n1_s2['subnet']['id'], 'port_id': lport_name}) elif p == 'p3': self.delete_acls.append((lport_name, lswitch_name)) self.reset_lport_dhcpv4_options.append(lport_name) self.lport_dhcpv6_disabled.update({ lport_name: n1_s2_dhcpv6_options_uuid}) data = {'port': { 'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'dhcp_disabled', 'opt_value': 'True'}]}} port_req = self.new_update_request('ports', data, lport_name) port_req.get_response(self.api) elif p == 'p4': self.lport_dhcpv4_disabled.update({ lport_name: n1_s1_dhcp_options_uuid}) data = {'port': { 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'dhcp_disabled', 'opt_value': 'True'}]}} port_req = self.new_update_request('ports', data, lport_name) port_req.get_response(self.api) self.reset_lport_dhcpv6_options.append(lport_name) elif p == 'p5': self.stale_lport_dhcpv4_options.append({ 'subnet_id': n1_s1['subnet']['id'], 'port_id': port['port']['id'], 'cidr': '10.0.0.0/24', 'options': {'server_id': '10.0.0.254', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(3 * 60 * 60), 'mtu': str(n1['network']['mtu'] / 2), 'router': '10.0.0.254', 'tftp_server': '20.0.0.234', 'dns_server': '8.8.8.8'}, 'external_ids': {'subnet_id': n1_s1['subnet']['id'], 'port_id': port['port']['id']}, }) elif p == 'p6': self.delete_lswitch_ports.append((lport_name, lswitch_name)) elif p == 'p7': update_port_ids_v4.append(port['port']['id']) update_port_ids_v6.append(port['port']['id']) self.expected_dhcp_options_rows.append({ 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': n1_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0', 'port_id': port['port']['id']}, 'options': { 'classless_static_route': '{169.254.169.254/32,10.0.0.2, 0.0.0.0/0,10.0.0.1}', 'server_id': '10.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': n1_s1['subnet']['gateway_ip'], 'tftp_server': '20.0.0.20', 'dns_server': '8.8.8.8'}}) self.expected_dhcp_options_rows.append({ 'cidr': '2001:dba::/64', 'external_ids': {'subnet_id': n1_s2['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0', 'port_id': port['port']['id']}, 'options': {'server_id': '01:02:03:04:05:06', 'domain_search': 'foo-domain'}}) self.reset_lport_dhcpv4_options.append(lport_name) self.reset_lport_dhcpv6_options.append(lport_name) self.dirty_dhcp_options.append({'subnet_id': n1_s1['subnet']['id']}) self.dirty_dhcp_options.append({'subnet_id': n1_s2['subnet']['id']}) res = self._create_network(self.fmt, 'n2', True, **net_kwargs) n2 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n2['network']['id'], '20.0.0.0/24') n2_s1 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n2['network']['id'], '2001:dbd::/64', ip_version=6) n2_s2 = self.deserialize(self.fmt, res) self.expected_dhcp_options_rows.append({ 'cidr': '20.0.0.0/24', 'external_ids': {'subnet_id': n2_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'classless_static_route': '{169.254.169.254/32,20.0.0.2, 0.0.0.0/0,20.0.0.1}', 'server_id': '20.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(n2['network']['mtu']), 'router': n2_s1['subnet']['gateway_ip']}}) self.expected_dhcp_options_rows.append({ 'cidr': '2001:dbd::/64', 'external_ids': {'subnet_id': n2_s2['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'server_id': '01:02:03:04:05:06'}}) for p in ['p1', 'p2']: port = self._make_port(self.fmt, n2['network']['id'], name='n2-' + p, device_owner='compute:None') if p == 'p1': update_port_ids_v4.append(port['port']['id']) self.expected_dhcp_options_rows.append({ 'cidr': '20.0.0.0/24', 'external_ids': {'subnet_id': n2_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0', 'port_id': port['port']['id']}, 'options': { 'classless_static_route': '{169.254.169.254/32,20.0.0.2, 0.0.0.0/0,20.0.0.1}', 'server_id': '20.0.0.1', 'server_mac': '01:02:03:04:05:06', 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': n2_s1['subnet']['gateway_ip'], 'tftp_server': '20.0.0.20', 'dns_server': '8.8.8.8'}}) self.missed_dhcp_options.extend([ opts['uuid'] for opts in self.mech_driver._nb_ovn.get_subnets_dhcp_options( [n2_s1['subnet']['id'], n2_s2['subnet']['id']])]) for port_id in update_port_ids_v4: data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': '20.0.0.20'}, {'ip_version': 4, 'opt_name': 'dns-server', 'opt_value': '8.8.8.8'}]}} port_req = self.new_update_request('ports', data, port_id) port_req.get_response(self.api) for port_id in update_port_ids_v6: data = {'port': {'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'foo-domain'}]}} port_req = self.new_update_request('ports', data, port_id) port_req.get_response(self.api) # External network and subnet e1 = self._make_network(self.fmt, 'e1', True, arg_list=('router:external', 'provider:network_type', 'provider:physical_network'), **{'router:external': True, 'provider:network_type': 'flat', 'provider:physical_network': 'public'}) self.assertEqual(True, e1['network']['router:external']) self.assertEqual('flat', e1['network']['provider:network_type']) self.assertEqual('public', e1['network']['provider:physical_network']) res = self._create_subnet(self.fmt, e1['network']['id'], '100.0.0.0/24', gateway_ip='100.0.0.254', allocation_pools=[{'start': '100.0.0.2', 'end': '100.0.0.253'}], enable_dhcp=False) e1_s1 = self.deserialize(self.fmt, res) self.create_lswitches.append('neutron-' + uuidutils.generate_uuid()) self.create_lswitch_ports.append(('neutron-' + uuidutils.generate_uuid(), 'neutron-' + n1['network']['id'])) self.create_lswitch_ports.append(('neutron-' + uuidutils.generate_uuid(), 'neutron-' + n1['network']['id'])) self.delete_lswitches.append('neutron-' + n2['network']['id']) self.delete_lswitch_ports.append( (utils.ovn_provnet_port_name(e1['network']['id']), utils.ovn_name(e1['network']['id']))) r1 = self.l3_plugin.create_router( self.context, {'router': { 'name': 'r1', 'admin_state_up': True, 'tenant_id': self._tenant_id, 'external_gateway_info': { 'enable_snat': True, 'network_id': e1['network']['id'], 'external_fixed_ips': [ {'ip_address': '100.0.0.2', 'subnet_id': e1_s1['subnet']['id']}]}}}) self.l3_plugin.add_router_interface( self.context, r1['id'], {'subnet_id': n1_s1['subnet']['id']}) r1_p2 = self.l3_plugin.add_router_interface( self.context, r1['id'], {'subnet_id': n1_s2['subnet']['id']}) self.l3_plugin.add_router_interface( self.context, r1['id'], {'subnet_id': n1_s3['subnet']['id']}) r1_p3 = self.l3_plugin.add_router_interface( self.context, r1['id'], {'subnet_id': n2_s1['subnet']['id']}) self.update_lrouter_ports.append(('lrp-' + r1_p2['port_id'], 'neutron-' + r1['id'], n1_s2['subnet']['gateway_ip'])) self.delete_lrouter_ports.append(('lrp-' + r1_p3['port_id'], 'neutron-' + r1['id'])) self.delete_lrouter_ports.append(('lrp-' + r1['gw_port_id'], 'neutron-' + r1['id'])) self.l3_plugin.update_router( self.context, r1['id'], {'router': {'routes': [{'destination': '10.10.0.0/24', 'nexthop': '20.0.0.10'}, {'destination': '10.11.0.0/24', 'nexthop': '20.0.0.11'}]}}) r1_f1 = self.l3_plugin.create_floatingip( self.context, {'floatingip': { 'tenant_id': self._tenant_id, 'floating_network_id': e1['network']['id'], 'floating_ip_address': '100.0.0.20', 'subnet_id': None, 'port_id': n1_port_dict['p1']}}) r1_f2 = self.l3_plugin.create_floatingip( self.context, {'floatingip': { 'tenant_id': self._tenant_id, 'floating_network_id': e1['network']['id'], 'subnet_id': None, 'floating_ip_address': '100.0.0.21'}}) self.l3_plugin.update_floatingip( self.context, r1_f2['id'], {'floatingip': { 'port_id': n1_port_dict['p2']}}) # update External subnet gateway ip to test function _subnet_update # of L3 OVN plugin. data = {'subnet': {'gateway_ip': '100.0.0.1'}} subnet_req = self.new_update_request( 'subnets', data, e1_s1['subnet']['id']) subnet_req.get_response(self.api) # Static routes self.create_lrouter_routes.append(('neutron-' + r1['id'], '10.12.0.0/24', '20.0.0.12')) self.create_lrouter_routes.append(('neutron-' + r1['id'], '10.13.0.0/24', '20.0.0.13')) self.delete_lrouter_routes.append(('neutron-' + r1['id'], '10.10.0.0/24', '20.0.0.10')) # Gateway default route self.delete_lrouter_routes.append(('neutron-' + r1['id'], '0.0.0.0/0', '100.0.0.1')) # Gateway sNATs self.create_lrouter_nats.append(('neutron-' + r1['id'], {'external_ip': '100.0.0.100', 'logical_ip': '200.0.0.0/24', 'type': 'snat'})) self.delete_lrouter_nats.append(('neutron-' + r1['id'], {'external_ip': '100.0.0.2', 'logical_ip': '10.0.0.0/24', 'type': 'snat'})) # Floating IPs self.create_lrouter_nats.append(('neutron-' + r1['id'], {'external_ip': '100.0.0.200', 'logical_ip': '200.0.0.200', 'type': 'dnat_and_snat'})) self.create_lrouter_nats.append(('neutron-' + r1['id'], {'external_ip': '100.0.0.201', 'logical_ip': '200.0.0.201', 'type': 'dnat_and_snat', 'external_mac': '01:02:03:04:05:06', 'logical_port': 'vm1' })) self.delete_lrouter_nats.append(('neutron-' + r1['id'], {'external_ip': r1_f1['floating_ip_address'], 'logical_ip': r1_f1['fixed_ip_address'], 'type': 'dnat_and_snat'})) res = self._create_network(self.fmt, 'n4', True, **net_kwargs) n4 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n4['network']['id'], '40.0.0.0/24', enable_dhcp=False) self.expected_dns_records.append( {'external_ids': {'ls_name': utils.ovn_name(n4['network']['id'])}, 'records': {}} ) n4_s1 = self.deserialize(self.fmt, res) n4_port_dict = {} for p in ['p1', 'p2', 'p3']: if p in ['p1', 'p2']: port_kwargs = {'arg_list': (dns_apidef.DNSNAME,), dns_apidef.DNSNAME: 'n4-' + p, 'device_id': 'n4-' + p} else: port_kwargs = {} res = self._create_port(self.fmt, n4['network']['id'], name='n4-' + p, device_owner='compute:None', **port_kwargs) port = self.deserialize(self.fmt, res) if p in ['p1', 'p2']: port_ips = " ".join([f['ip_address'] for f in port['port']['fixed_ips']]) hname = 'n4-' + p self.expected_dns_records[1]['records'][hname] = port_ips hname = 'n4-' + p + '.ovn.test.' self.expected_dns_records[1]['records'][hname] = port_ips n4_port_dict[p] = port['port']['id'] self.lport_dhcp_ignored.append(port['port']['id']) r2 = self.l3_plugin.create_router( self.context, {'router': {'name': 'r2', 'admin_state_up': True, 'tenant_id': self._tenant_id}}) n1_prtr = self._make_port(self.fmt, n1['network']['id'], name='n1-p-rtr') self.l3_plugin.add_router_interface( self.context, r2['id'], {'port_id': n1_prtr['port']['id']}) self.l3_plugin.add_router_interface( self.context, r2['id'], {'subnet_id': n4_s1['subnet']['id']}) self.l3_plugin.update_router( self.context, r2['id'], {'router': {'routes': [{'destination': '10.20.0.0/24', 'nexthop': '10.0.0.20'}], 'external_gateway_info': { 'enable_snat': False, 'network_id': e1['network']['id'], 'external_fixed_ips': [ {'ip_address': '100.0.0.3', 'subnet_id': e1_s1['subnet']['id']}]}}}) self.l3_plugin.create_floatingip( self.context, {'floatingip': { 'tenant_id': self._tenant_id, 'floating_network_id': e1['network']['id'], 'floating_ip_address': '100.0.0.30', 'subnet_id': None, 'port_id': n4_port_dict['p1']}}) self.l3_plugin.create_floatingip( self.context, {'floatingip': { 'tenant_id': self._tenant_id, 'floating_network_id': e1['network']['id'], 'floating_ip_address': '100.0.0.31', 'subnet_id': None, 'port_id': n4_port_dict['p2']}}) # To test l3_plugin.disassociate_floatingips, associating floating IP # to port p3 and then deleting p3. self.l3_plugin.create_floatingip( self.context, {'floatingip': { 'tenant_id': self._tenant_id, 'floating_network_id': e1['network']['id'], 'floating_ip_address': '100.0.0.32', 'subnet_id': None, 'port_id': n4_port_dict['p3']}}) self._delete('ports', n4_port_dict['p3']) self.create_lrouters.append('neutron-' + uuidutils.generate_uuid()) self.create_lrouter_ports.append(('lrp-' + uuidutils.generate_uuid(), 'neutron-' + r1['id'])) self.create_lrouter_ports.append(('lrp-' + uuidutils.generate_uuid(), 'neutron-' + r1['id'])) self.delete_lrouters.append('neutron-' + r2['id']) address_set_name = n1_prtr['port']['security_groups'][0] self.create_address_sets.extend([('fake_sg', 'ip4'), ('fake_sg', 'ip6')]) self.delete_address_sets.append((address_set_name, 'ip6')) address_adds = ['10.0.0.101', '10.0.0.102'] address_dels = [] for address in n1_prtr['port']['fixed_ips']: address_dels.append(address['ip_address']) self.update_address_sets.append((address_set_name, 'ip4', address_adds, address_dels)) # Create a network and subnet with orphaned OVN resources. n3 = self._make_network(self.fmt, 'n3', True) res = self._create_subnet(self.fmt, n3['network']['id'], '30.0.0.0/24') n3_s1 = self.deserialize(self.fmt, res) res = self._create_subnet(self.fmt, n3['network']['id'], '2001:dbc::/64', ip_version=6) n3_s2 = self.deserialize(self.fmt, res) if not restart_ovsdb_processes: # Test using original mac when syncing. dhcp_mac_v4 = (self.mech_driver._nb_ovn.get_subnet_dhcp_options( n3_s1['subnet']['id'])['subnet'].get('options', {}) .get('server_mac')) dhcp_mac_v6 = (self.mech_driver._nb_ovn.get_subnet_dhcp_options( n3_s2['subnet']['id'])['subnet'].get('options', {}) .get('server_id')) self.assertTrue(dhcp_mac_v4 is not None) self.assertTrue(dhcp_mac_v6 is not None) self.match_old_mac_dhcp_subnets.append(n3_s1['subnet']['id']) self.match_old_mac_dhcp_subnets.append(n3_s2['subnet']['id']) else: dhcp_mac_v4 = '01:02:03:04:05:06' dhcp_mac_v6 = '01:02:03:04:05:06' self.expected_dhcp_options_rows.append({ 'cidr': '30.0.0.0/24', 'external_ids': {'subnet_id': n3_s1['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'classless_static_route': '{169.254.169.254/32,30.0.0.2, 0.0.0.0/0,30.0.0.1}', 'server_id': '30.0.0.1', 'server_mac': dhcp_mac_v4, 'lease_time': str(12 * 60 * 60), 'mtu': str(n3['network']['mtu']), 'router': n3_s1['subnet']['gateway_ip']}}) self.expected_dhcp_options_rows.append({ 'cidr': '2001:dbc::/64', 'external_ids': {'subnet_id': n3_s2['subnet']['id'], ovn_const.OVN_REV_NUM_EXT_ID_KEY: '0'}, 'options': {'server_id': dhcp_mac_v6}}) fake_port_id1 = uuidutils.generate_uuid() fake_port_id2 = uuidutils.generate_uuid() self.create_lswitch_ports.append(('neutron-' + fake_port_id1, 'neutron-' + n3['network']['id'])) self.create_lswitch_ports.append(('neutron-' + fake_port_id2, 'neutron-' + n3['network']['id'])) stale_dhcpv4_options1 = { 'subnet_id': n3_s1['subnet']['id'], 'port_id': fake_port_id1, 'cidr': '30.0.0.0/24', 'options': {'server_id': '30.0.0.254', 'server_mac': dhcp_mac_v4, 'lease_time': str(3 * 60 * 60), 'mtu': str(n3['network']['mtu'] / 2), 'router': '30.0.0.254', 'tftp_server': '30.0.0.234', 'dns_server': '8.8.8.8'}, 'external_ids': {'subnet_id': n3_s1['subnet']['id'], 'port_id': fake_port_id1}, } self.stale_lport_dhcpv4_options.append(stale_dhcpv4_options1) stale_dhcpv4_options2 = stale_dhcpv4_options1.copy() stale_dhcpv4_options2.update({ 'port_id': fake_port_id2, 'external_ids': {'subnet_id': n3_s1['subnet']['id'], 'port_id': fake_port_id2}}) self.stale_lport_dhcpv4_options.append(stale_dhcpv4_options2) self.orphaned_lport_dhcp_options.append(fake_port_id2) stale_dhcpv6_options1 = { 'subnet_id': n3_s2['subnet']['id'], 'port_id': fake_port_id1, 'cidr': '2001:dbc::/64', 'options': {'server_id': dhcp_mac_v6, 'domain-search': 'foo-domain'}, 'external_ids': {'subnet_id': n3_s2['subnet']['id'], 'port_id': fake_port_id1}, } self.stale_lport_dhcpv6_options.append(stale_dhcpv6_options1) stale_dhcpv6_options2 = stale_dhcpv6_options1.copy() stale_dhcpv6_options2.update({ 'port_id': fake_port_id2, 'external_ids': {'subnet_id': n3_s2['subnet']['id'], 'port_id': fake_port_id2}}) self.stale_lport_dhcpv6_options.append(stale_dhcpv6_options2) fake_port = {'id': fake_port_id1, 'network_id': n3['network']['id']} dhcp_acls = acl_utils.add_acl_dhcp(fake_port, n3_s1['subnet']) for dhcp_acl in dhcp_acls: self.create_acls.append(dhcp_acl) columns = list(self.nb_api.tables['ACL'].columns) if not (('name' in columns) and ('severity' in columns)): for acl in self.create_acls: acl.pop('name') acl.pop('severity') def _modify_resources_in_nb_db(self): self._delete_metadata_ports() with self.nb_api.transaction(check_error=True) as txn: for lswitch_name in self.create_lswitches: external_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: lswitch_name} txn.add(self.nb_api.ls_add(lswitch_name, True, external_ids=external_ids)) for lswitch_name in self.delete_lswitches: txn.add(self.nb_api.ls_del(lswitch_name, True)) for lport_name, lswitch_name in self.create_lswitch_ports: external_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: lport_name} txn.add(self.nb_api.create_lswitch_port( lport_name, lswitch_name, True, external_ids=external_ids)) for lport_name, lswitch_name in self.delete_lswitch_ports: txn.add(self.nb_api.delete_lswitch_port(lport_name, lswitch_name, True)) for lrouter_name in self.create_lrouters: external_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: lrouter_name} txn.add(self.nb_api.create_lrouter(lrouter_name, True, external_ids=external_ids)) for lrouter_name in self.delete_lrouters: txn.add(self.nb_api.delete_lrouter(lrouter_name, True)) for lrport, lrouter_name in self.create_lrouter_ports: txn.add(self.nb_api.add_lrouter_port(lrport, lrouter_name)) for lrport, lrouter_name, networks in self.update_lrouter_ports: txn.add(self.nb_api.update_lrouter_port( lrport, True, **{'networks': [networks], 'ipv6_ra_configs': {'foo': 'bar'}})) for lrport, lrouter_name in self.delete_lrouter_ports: txn.add(self.nb_api.delete_lrouter_port(lrport, lrouter_name, True)) for lrouter_name, ip_prefix, nexthop in self.create_lrouter_routes: txn.add(self.nb_api.add_static_route(lrouter_name, ip_prefix=ip_prefix, nexthop=nexthop)) for lrouter_name, ip_prefix, nexthop in self.delete_lrouter_routes: txn.add(self.nb_api.delete_static_route(lrouter_name, ip_prefix, nexthop, True)) for lrouter_name, nat_dict in( self.create_lrouter_nats): txn.add(self.nb_api.add_nat_rule_in_lrouter( lrouter_name, **nat_dict)) for lrouter_name, nat_dict in( self.delete_lrouter_nats): txn.add(self.nb_api.delete_nat_rule_in_lrouter( lrouter_name, if_exists=True, **nat_dict)) for acl in self.create_acls: txn.add(self.nb_api.add_acl(**acl)) for lport_name, lswitch_name in self.delete_acls: txn.add(self.nb_api.delete_acl(lswitch_name, lport_name, True)) for name, ip_version in self.create_address_sets: ovn_name = utils.ovn_addrset_name(name, ip_version) external_ids = {ovn_const.OVN_SG_EXT_ID_KEY: name} txn.add(self.nb_api.create_address_set( ovn_name, True, external_ids=external_ids)) for name, ip_version in self.delete_address_sets: ovn_name = utils.ovn_addrset_name(name, ip_version) txn.add(self.nb_api.delete_address_set(ovn_name, True)) for name, ip_version, ip_adds, ip_dels in self.update_address_sets: ovn_name = utils.ovn_addrset_name(name, ip_version) txn.add(self.nb_api.update_address_set(ovn_name, ip_adds, ip_dels, True)) for lport_name in self.reset_lport_dhcpv4_options: txn.add(self.nb_api.set_lswitch_port(lport_name, True, dhcpv4_options=[])) for lport_name in self.reset_lport_dhcpv6_options: txn.add(self.nb_api.set_lswitch_port(lport_name, True, dhcpv6_options=[])) for dhcp_opts in self.stale_lport_dhcpv4_options: dhcpv4_opts = txn.add(self.nb_api.add_dhcp_options( dhcp_opts['subnet_id'], port_id=dhcp_opts['port_id'], cidr=dhcp_opts['cidr'], options=dhcp_opts['options'], external_ids=dhcp_opts['external_ids'], may_exist=False)) if dhcp_opts['port_id'] in self.orphaned_lport_dhcp_options: continue txn.add(self.nb_api.set_lswitch_port( lport_name, True, dhcpv4_options=dhcpv4_opts)) for dhcp_opts in self.stale_lport_dhcpv6_options: dhcpv6_opts = txn.add(self.nb_api.add_dhcp_options( dhcp_opts['subnet_id'], port_id=dhcp_opts['port_id'], cidr=dhcp_opts['cidr'], options=dhcp_opts['options'], external_ids=dhcp_opts['external_ids'], may_exist=False)) if dhcp_opts['port_id'] in self.orphaned_lport_dhcp_options: continue txn.add(self.nb_api.set_lswitch_port( lport_name, True, dhcpv6_options=dhcpv6_opts)) for row_uuid in self.missed_dhcp_options: txn.add(self.nb_api.delete_dhcp_options(row_uuid)) for dhcp_opts in self.dirty_dhcp_options: external_ids = {'subnet_id': dhcp_opts['subnet_id']} if dhcp_opts.get('port_id'): external_ids['port_id'] = dhcp_opts['port_id'] txn.add(self.nb_api.add_dhcp_options( dhcp_opts['subnet_id'], port_id=dhcp_opts.get('port_id'), external_ids=external_ids, options={'foo': 'bar'})) for port_id in self.lport_dhcpv4_disabled: txn.add(self.nb_api.set_lswitch_port( port_id, True, dhcpv4_options=[self.lport_dhcpv4_disabled[port_id]])) for port_id in self.lport_dhcpv6_disabled: txn.add(self.nb_api.set_lswitch_port( port_id, True, dhcpv6_options=[self.lport_dhcpv6_disabled[port_id]])) # Delete the first DNS record and clear the second row records i = 0 for dns_row in self.nb_api.tables['DNS'].rows.values(): if i == 0: txn.add(self.nb_api.dns_del(dns_row.uuid)) else: txn.add(self.nb_api.dns_set_records(dns_row.uuid, **{})) i += 1 def _validate_networks(self, should_match=True): db_networks = self._list('networks') db_net_ids = [net['id'] for net in db_networks['networks']] db_provnet_ports = [utils.ovn_provnet_port_name(net['id']) for net in db_networks['networks'] if net.get('provider:physical_network')] # Get the list of lswitch ids stored in the OVN plugin IDL _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_lswitch_ids = [ row.name.replace('neutron-', '') for row in ( _plugin_nb_ovn._tables['Logical_Switch'].rows.values())] # Get the list of lswitch ids stored in the monitor IDL connection monitor_lswitch_ids = [ row.name.replace('neutron-', '') for row in ( self.nb_api.tables['Logical_Switch'].rows.values())] # Get the list of provnet ports stored in the OVN plugin IDL plugin_provnet_ports = [row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.name.startswith(ovn_const.OVN_PROVNET_PORT_NAME_PREFIX)] # Get the list of provnet ports stored in the monitor IDL connection monitor_provnet_ports = [row.name for row in ( self.nb_api.tables['Logical_Switch_Port'].rows.values()) if row.name.startswith(ovn_const.OVN_PROVNET_PORT_NAME_PREFIX)] if should_match: self.assertItemsEqual(db_net_ids, plugin_lswitch_ids) self.assertItemsEqual(db_net_ids, monitor_lswitch_ids) self.assertItemsEqual(db_provnet_ports, plugin_provnet_ports) self.assertItemsEqual(db_provnet_ports, monitor_provnet_ports) else: self.assertRaises( AssertionError, self.assertItemsEqual, db_net_ids, plugin_lswitch_ids) self.assertRaises( AssertionError, self.assertItemsEqual, db_net_ids, monitor_lswitch_ids) self.assertRaises( AssertionError, self.assertItemsEqual, db_provnet_ports, plugin_provnet_ports) self.assertRaises( AssertionError, self.assertItemsEqual, db_provnet_ports, monitor_provnet_ports) def _validate_metadata_ports(self, should_match=True): """Validate metadata ports. This method will check that all networks have one and only one metadata port and that every metadata port in Neutron also exists in OVN. """ db_ports = self._list('ports') db_metadata_ports_ids = [] db_metadata_ports_nets = [] for port in db_ports['ports']: if port['device_owner'] == constants.DEVICE_OWNER_DHCP: db_metadata_ports_ids.append(port['id']) db_metadata_ports_nets.append(port['network_id']) db_networks = self._list('networks') db_net_ids = [net['id'] for net in db_networks['networks']] # Retrieve all localports in OVN _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_metadata_ports = [row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.type == ovn_const.OVN_NEUTRON_OWNER_TO_PORT_TYPE.get( constants.DEVICE_OWNER_DHCP)] if should_match: # Check that metadata ports exist in both Neutron and OVN dbs. self.assertItemsEqual(db_metadata_ports_ids, plugin_metadata_ports) # Check that all networks have one and only one metadata port. self.assertItemsEqual(db_metadata_ports_nets, db_net_ids) else: metadata_sync = (sorted(db_metadata_ports_ids) == sorted(plugin_metadata_ports)) metadata_unique = (sorted(db_net_ids) == sorted(db_metadata_ports_nets)) self.assertFalse(metadata_sync and metadata_unique) def _validate_ports(self, should_match=True): db_ports = self._list('ports') db_port_ids = [port['id'] for port in db_ports['ports'] if not utils.is_lsp_ignored(port)] db_port_ids_dhcp_valid = set( port['id'] for port in db_ports['ports'] if not utils.is_network_device_port(port) and port['id'] not in self.lport_dhcp_ignored) _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_lport_ids = [ row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if ovn_const.OVN_PORT_NAME_EXT_ID_KEY in row.external_ids] plugin_lport_ids_dhcpv4_enabled = [ row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.dhcpv4_options] plugin_lport_ids_dhcpv6_enabled = [ row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.dhcpv6_options] monitor_lport_ids = [ row.name for row in ( self.nb_api.tables['Logical_Switch_Port']. rows.values()) if ovn_const.OVN_PORT_NAME_EXT_ID_KEY in row.external_ids] monitor_lport_ids_dhcpv4_enabled = [ row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.dhcpv4_options] monitor_lport_ids_dhcpv6_enabled = [ row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.dhcpv6_options] if should_match: self.assertItemsEqual(db_port_ids, plugin_lport_ids) self.assertItemsEqual(db_port_ids, monitor_lport_ids) expected_dhcpv4_options_ports_ids = ( db_port_ids_dhcp_valid.difference( set(self.lport_dhcpv4_disabled.keys()))) self.assertItemsEqual(expected_dhcpv4_options_ports_ids, plugin_lport_ids_dhcpv4_enabled) self.assertItemsEqual(expected_dhcpv4_options_ports_ids, monitor_lport_ids_dhcpv4_enabled) expected_dhcpv6_options_ports_ids = ( db_port_ids_dhcp_valid.difference( set(self.lport_dhcpv6_disabled.keys()))) self.assertItemsEqual(expected_dhcpv6_options_ports_ids, plugin_lport_ids_dhcpv6_enabled) self.assertItemsEqual(expected_dhcpv6_options_ports_ids, monitor_lport_ids_dhcpv6_enabled) else: self.assertRaises( AssertionError, self.assertItemsEqual, db_port_ids, plugin_lport_ids) self.assertRaises( AssertionError, self.assertItemsEqual, db_port_ids, monitor_lport_ids) self.assertRaises( AssertionError, self.assertItemsEqual, db_port_ids, plugin_lport_ids_dhcpv4_enabled) self.assertRaises( AssertionError, self.assertItemsEqual, db_port_ids, monitor_lport_ids_dhcpv4_enabled) def _validate_dhcp_opts(self, should_match=True): observed_plugin_dhcp_options_rows = [] _plugin_nb_ovn = self.mech_driver._nb_ovn for row in _plugin_nb_ovn._tables['DHCP_Options'].rows.values(): opts = dict(row.options) ids = dict(row.external_ids) if ids.get('subnet_id') not in self.match_old_mac_dhcp_subnets: if 'server_mac' in opts: opts['server_mac'] = '01:02:03:04:05:06' else: opts['server_id'] = '01:02:03:04:05:06' observed_plugin_dhcp_options_rows.append({ 'cidr': row.cidr, 'external_ids': row.external_ids, 'options': opts}) observed_monitor_dhcp_options_rows = [] for row in self.nb_api.tables['DHCP_Options'].rows.values(): opts = dict(row.options) ids = dict(row.external_ids) if ids.get('subnet_id') not in self.match_old_mac_dhcp_subnets: if 'server_mac' in opts: opts['server_mac'] = '01:02:03:04:05:06' else: opts['server_id'] = '01:02:03:04:05:06' observed_monitor_dhcp_options_rows.append({ 'cidr': row.cidr, 'external_ids': row.external_ids, 'options': opts}) if should_match: self.assertItemsEqual(self.expected_dhcp_options_rows, observed_plugin_dhcp_options_rows) self.assertItemsEqual(self.expected_dhcp_options_rows, observed_monitor_dhcp_options_rows) else: self.assertRaises( AssertionError, self.assertItemsEqual, self.expected_dhcp_options_rows, observed_plugin_dhcp_options_rows) self.assertRaises( AssertionError, self.assertItemsEqual, self.expected_dhcp_options_rows, observed_monitor_dhcp_options_rows) def _build_acl_to_compare(self, acl): acl_to_compare = {} for acl_key in getattr(acl, "_data", {}): try: acl_to_compare[acl_key] = getattr(acl, acl_key) except AttributeError: pass return acl_to_compare def _validate_acls(self, should_match=True): # Get the neutron DB ACLs. db_acls = [] sg_cache = {} subnet_cache = {} for db_port in self._list('ports')['ports']: acls = acl_utils.add_acls(self.plugin, context.get_admin_context(), db_port, sg_cache, subnet_cache, self.mech_driver._nb_ovn) for acl in acls: acl.pop('lport') acl.pop('lswitch') db_acls.append(acl) # Get the list of ACLs stored in the OVN plugin IDL. _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_acls = [] for row in _plugin_nb_ovn._tables['Logical_Switch'].rows.values(): for acl in getattr(row, 'acls', []): plugin_acls.append(self._build_acl_to_compare(acl)) # Get the list of ACLs stored in the OVN monitor IDL. monitor_acls = [] for row in self.nb_api.tables['Logical_Switch'].rows.values(): for acl in getattr(row, 'acls', []): monitor_acls.append(self._build_acl_to_compare(acl)) if should_match: self.assertItemsEqual(db_acls, plugin_acls) self.assertItemsEqual(db_acls, monitor_acls) else: self.assertRaises( AssertionError, self.assertItemsEqual, db_acls, plugin_acls) self.assertRaises( AssertionError, self.assertItemsEqual, db_acls, monitor_acls) def _validate_routers_and_router_ports(self, should_match=True): db_routers = self._list('routers') db_router_ids = [] db_routes = {} db_nats = {} for db_router in db_routers['routers']: db_router_ids.append(db_router['id']) db_routes[db_router['id']] = [db_route['destination'] + db_route['nexthop'] for db_route in db_router['routes']] db_nats[db_router['id']] = [] if db_router.get(l3.EXTERNAL_GW_INFO): gw_info = self.l3_plugin._ovn_client._get_gw_info( self.context, db_router) # Add gateway default route and snats if gw_info.gateway_ip: db_routes[db_router['id']].append('0.0.0.0/0' + gw_info.gateway_ip) if gw_info.router_ip and utils.is_snat_enabled(db_router): networks = self.l3_plugin._ovn_client.\ _get_v4_network_of_all_router_ports(self.context, db_router['id']) db_nats[db_router['id']].extend( [gw_info.router_ip + network + 'snat' for network in networks]) fips = self._list('floatingips') fip_macs = {} if ovn_config.is_ovn_distributed_floating_ip(): params = 'device_owner=%s' % constants.DEVICE_OWNER_FLOATINGIP fports = self._list('ports', query_params=params)['ports'] fip_macs = {p['device_id']: p['mac_address'] for p in fports if p['device_id']} for fip in fips['floatingips']: if fip['router_id']: mac_address = '' fip_port = '' if fip['id'] in fip_macs: mac_address = fip_macs[fip['id']] fip_port = fip['port_id'] db_nats[fip['router_id']].append( fip['floating_ip_address'] + fip['fixed_ip_address'] + 'dnat_and_snat' + mac_address + fip_port) _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_lrouter_ids = [ row.name.replace('neutron-', '') for row in ( _plugin_nb_ovn._tables['Logical_Router'].rows.values())] monitor_lrouter_ids = [ row.name.replace('neutron-', '') for row in ( self.nb_api.tables['Logical_Router'].rows.values())] if should_match: self.assertItemsEqual(db_router_ids, plugin_lrouter_ids) self.assertItemsEqual(db_router_ids, monitor_lrouter_ids) else: self.assertRaises( AssertionError, self.assertItemsEqual, db_router_ids, plugin_lrouter_ids) self.assertRaises( AssertionError, self.assertItemsEqual, db_router_ids, monitor_lrouter_ids) def _get_networks_for_router_port(port_fixed_ips): _ovn_client = self.l3_plugin._ovn_client networks, _ = ( _ovn_client._get_nets_and_ipv6_ra_confs_for_router_port( port_fixed_ips)) return networks def _get_ipv6_ra_configs_for_router_port(port_fixed_ips): _ovn_client = self.l3_plugin._ovn_client networks, ipv6_ra_configs = ( _ovn_client._get_nets_and_ipv6_ra_confs_for_router_port( port_fixed_ips)) return ipv6_ra_configs for router_id in db_router_ids: r_ports = self._list('ports', query_params='device_id=%s' % (router_id)) r_port_ids = [p['id'] for p in r_ports['ports']] r_port_networks = { p['id']: _get_networks_for_router_port(p['fixed_ips']) for p in r_ports['ports']} r_port_ipv6_ra_configs = { p['id']: _get_ipv6_ra_configs_for_router_port(p['fixed_ips']) for p in r_ports['ports']} r_routes = db_routes[router_id] r_nats = db_nats[router_id] try: lrouter = idlutils.row_by_value( self.mech_driver._nb_ovn.idl, 'Logical_Router', 'name', 'neutron-' + str(router_id), None) lports = getattr(lrouter, 'ports', []) plugin_lrouter_port_ids = [lport.name.replace('lrp-', '') for lport in lports] plugin_lport_networks = { lport.name.replace('lrp-', ''): lport.networks for lport in lports} plugin_lport_ra_configs = { lport.name.replace('lrp-', ''): lport.ipv6_ra_configs for lport in lports} sroutes = getattr(lrouter, 'static_routes', []) plugin_routes = [sroute.ip_prefix + sroute.nexthop for sroute in sroutes] nats = getattr(lrouter, 'nat', []) plugin_nats = [ nat.external_ip + nat.logical_ip + nat.type + (nat.external_mac[0] if nat.external_mac else '') + (nat.logical_port[0] if nat.logical_port else '') for nat in nats] except idlutils.RowNotFound: plugin_lrouter_port_ids = [] plugin_routes = [] plugin_nats = [] try: lrouter = idlutils.row_by_value( self.nb_api.idl, 'Logical_Router', 'name', 'neutron-' + router_id, None) lports = getattr(lrouter, 'ports', []) monitor_lrouter_port_ids = [lport.name.replace('lrp-', '') for lport in lports] monitor_lport_networks = { lport.name.replace('lrp-', ''): lport.networks for lport in lports} monitor_lport_ra_configs = { lport.name.replace('lrp-', ''): lport.ipv6_ra_configs for lport in lports} sroutes = getattr(lrouter, 'static_routes', []) monitor_routes = [sroute.ip_prefix + sroute.nexthop for sroute in sroutes] nats = getattr(lrouter, 'nat', []) monitor_nats = [ nat.external_ip + nat.logical_ip + nat.type + (nat.external_mac[0] if nat.external_mac else '') + (nat.logical_port[0] if nat.logical_port else '') for nat in nats] except idlutils.RowNotFound: monitor_lrouter_port_ids = [] monitor_routes = [] monitor_nats = [] if should_match: self.assertItemsEqual(r_port_ids, plugin_lrouter_port_ids) self.assertItemsEqual(r_port_ids, monitor_lrouter_port_ids) for p in plugin_lport_networks: self.assertItemsEqual(r_port_networks[p], plugin_lport_networks[p]) self.assertItemsEqual(r_port_ipv6_ra_configs[p], plugin_lport_ra_configs[p]) for p in monitor_lport_networks: self.assertItemsEqual(r_port_networks[p], monitor_lport_networks[p]) self.assertItemsEqual(r_port_ipv6_ra_configs[p], monitor_lport_ra_configs[p]) self.assertItemsEqual(r_routes, plugin_routes) self.assertItemsEqual(r_routes, monitor_routes) self.assertItemsEqual(r_nats, plugin_nats) self.assertItemsEqual(r_nats, monitor_nats) else: self.assertRaises( AssertionError, self.assertItemsEqual, r_port_ids, plugin_lrouter_port_ids) self.assertRaises( AssertionError, self.assertItemsEqual, r_port_ids, monitor_lrouter_port_ids) for _p in self.update_lrouter_ports: p = _p[0].replace('lrp-', '') if p in plugin_lport_networks: self.assertRaises( AssertionError, self.assertItemsEqual, r_port_networks[p], plugin_lport_networks[p]) self.assertRaises( AssertionError, self.assertItemsEqual, r_port_ipv6_ra_configs[p], plugin_lport_ra_configs[p]) if p in monitor_lport_networks: self.assertRaises( AssertionError, self.assertItemsEqual, r_port_networks[p], monitor_lport_networks[p]) self.assertRaises( AssertionError, self.assertItemsEqual, r_port_ipv6_ra_configs[p], monitor_lport_ra_configs[p]) self.assertRaises( AssertionError, self.assertItemsEqual, r_routes, plugin_routes) self.assertRaises( AssertionError, self.assertItemsEqual, r_routes, monitor_routes) self.assertRaises( AssertionError, self.assertItemsEqual, r_nats, plugin_nats) self.assertRaises( AssertionError, self.assertItemsEqual, r_nats, monitor_nats) def _validate_address_sets(self, should_match=True): db_ports = self._list('ports')['ports'] sgs = self._list('security-groups')['security_groups'] db_sgs = {} for sg in sgs: for ip_version in ['ip4', 'ip6']: name = utils.ovn_addrset_name(sg['id'], ip_version) db_sgs[name] = [] for port in db_ports: sg_ids = utils.get_lsp_security_groups(port) addresses = acl_utils.acl_port_ips(port) for sg_id in sg_ids: for ip_version in addresses: name = utils.ovn_addrset_name(sg_id, ip_version) db_sgs[name].extend(addresses[ip_version]) _plugin_nb_ovn = self.mech_driver._nb_ovn nb_address_sets = _plugin_nb_ovn.get_address_sets() nb_sgs = {} for nb_sgid, nb_values in nb_address_sets.items(): nb_sgs[nb_sgid] = nb_values['addresses'] mn_sgs = {} for row in self.nb_api.tables['Address_Set'].rows.values(): mn_sgs[getattr(row, 'name')] = getattr(row, 'addresses') if should_match: self.assertItemsEqual(nb_sgs, db_sgs) self.assertItemsEqual(mn_sgs, db_sgs) else: self.assertRaises(AssertionError, self.assertItemsEqual, nb_sgs, db_sgs) self.assertRaises(AssertionError, self.assertItemsEqual, mn_sgs, db_sgs) def _delete_metadata_ports(self): """Delete some metadata ports. This method will delete one half of the metadata ports from Neutron and the remaining ones only from OVN. This way we can exercise the metadata sync completely: ie., that metadata ports are recreated in Neutron when missing and that the corresponding OVN localports are also created. """ db_ports = self._list('ports') db_metadata_ports = [port for port in db_ports['ports'] if port['device_owner'] == constants.DEVICE_OWNER_DHCP] lswitches = {} ports_to_delete = len(db_metadata_ports) / 2 for port in db_metadata_ports: lswitches[port['id']] = 'neutron-' + port['network_id'] if ports_to_delete: self._delete('ports', port['id']) ports_to_delete -= 1 _plugin_nb_ovn = self.mech_driver._nb_ovn plugin_metadata_ports = [row.name for row in ( _plugin_nb_ovn._tables['Logical_Switch_Port'].rows.values()) if row.type == ovn_const.OVN_NEUTRON_OWNER_TO_PORT_TYPE.get( constants.DEVICE_OWNER_DHCP)] with self.nb_api.transaction(check_error=True) as txn: for port in plugin_metadata_ports: txn.add(self.nb_api.delete_lswitch_port(port, lswitches[port], True)) def _validate_dns_records(self, should_match=True): observed_dns_records = [] for dns_row in self.nb_api.tables['DNS'].rows.values(): observed_dns_records.append( {'external_ids': dns_row.external_ids, 'records': dns_row.records}) if should_match: self.assertItemsEqual(self.expected_dns_records, observed_dns_records) else: self.assertRaises(AssertionError, self.assertItemsEqual, self.expected_dns_records, observed_dns_records) def _validate_resources(self, should_match=True): self._validate_networks(should_match=should_match) self._validate_metadata_ports(should_match=should_match) self._validate_ports(should_match=should_match) self._validate_dhcp_opts(should_match=should_match) self._validate_acls(should_match=should_match) self._validate_routers_and_router_ports(should_match=should_match) self._validate_address_sets(should_match=should_match) self._validate_dns_records(should_match=should_match) def _sync_resources(self, mode): nb_synchronizer = ovn_db_sync.OvnNbSynchronizer( self.plugin, self.mech_driver._nb_ovn, self.mech_driver._sb_ovn, mode, self.mech_driver) self.addCleanup(nb_synchronizer.stop) nb_synchronizer.do_sync() def _test_ovn_nb_sync_helper(self, mode, modify_resources=True, restart_ovsdb_processes=False, should_match_after_sync=True): self._create_resources(restart_ovsdb_processes) self._validate_resources(should_match=True) if modify_resources: self._modify_resources_in_nb_db() if restart_ovsdb_processes: # Restart the ovsdb-server and plugin idl. # This causes a new ovsdb-server to be started with empty # OVN NB DB self.restart() if modify_resources or restart_ovsdb_processes: self._validate_resources(should_match=False) self._sync_resources(mode) self._validate_resources(should_match=should_match_after_sync) def test_ovn_nb_sync_repair(self): self._test_ovn_nb_sync_helper('repair') def test_ovn_nb_sync_repair_delete_ovn_nb_db(self): # In this test case, the ovsdb-server for OVN NB DB is restarted # with empty OVN NB DB. self._test_ovn_nb_sync_helper('repair', modify_resources=False, restart_ovsdb_processes=True) def test_ovn_nb_sync_log(self): self._test_ovn_nb_sync_helper('log', should_match_after_sync=False) def test_ovn_nb_sync_off(self): self._test_ovn_nb_sync_helper('off', should_match_after_sync=False) class TestOvnSbSync(base.TestOVNFunctionalBase): def setUp(self): super(TestOvnSbSync, self).setUp(ovn_worker=False) self.segments_plugin = directory.get_plugin('segments') self.sb_synchronizer = ovn_db_sync.OvnSbSynchronizer( self.plugin, self.mech_driver._sb_ovn, self.mech_driver) self.addCleanup(self.sb_synchronizer.stop) self.ctx = context.get_admin_context() def get_additional_service_plugins(self): p = super(TestOvnSbSync, self).get_additional_service_plugins() p.update({'segments': 'neutron.services.segments.plugin.Plugin'}) return p def _sync_resources(self): self.sb_synchronizer.sync_hostname_and_physical_networks(self.ctx) def create_segment(self, network_id, physical_network, segmentation_id): segment_data = {'network_id': network_id, 'physical_network': physical_network, 'segmentation_id': segmentation_id, 'network_type': 'vlan', 'name': constants.ATTR_NOT_SPECIFIED, 'description': constants.ATTR_NOT_SPECIFIED} return self.segments_plugin.create_segment( self.ctx, segment={'segment': segment_data}) def test_ovn_sb_sync_add_new_host(self): with self.network() as network: network_id = network['network']['id'] self.create_segment(network_id, 'physnet1', 50) self.add_fake_chassis('host1', ['physnet1']) segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertFalse(segment_hosts) self._sync_resources() segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertEqual({'host1'}, segment_hosts) def test_ovn_sb_sync_update_existing_host(self): with self.network() as network: network_id = network['network']['id'] segment = self.create_segment(network_id, 'physnet1', 50) segments_db.update_segment_host_mapping( self.ctx, 'host1', {segment['id']}) segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertEqual({'host1'}, segment_hosts) self.add_fake_chassis('host1', ['physnet2']) self._sync_resources() segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertFalse(segment_hosts) def test_ovn_sb_sync_delete_stale_host(self): with self.network() as network: network_id = network['network']['id'] segment = self.create_segment(network_id, 'physnet1', 50) segments_db.update_segment_host_mapping( self.ctx, 'host1', {segment['id']}) segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertEqual({'host1'}, segment_hosts) # Since there is no chassis in the sb DB, host1 is the stale host # recorded in neutron DB. It should be deleted after sync. self._sync_resources() segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertFalse(segment_hosts) def test_ovn_sb_sync(self): with self.network() as network: network_id = network['network']['id'] seg1 = self.create_segment(network_id, 'physnet1', 50) self.create_segment(network_id, 'physnet2', 51) segments_db.update_segment_host_mapping( self.ctx, 'host1', {seg1['id']}) segments_db.update_segment_host_mapping( self.ctx, 'host2', {seg1['id']}) segments_db.update_segment_host_mapping( self.ctx, 'host3', {seg1['id']}) segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) self.assertEqual({'host1', 'host2', 'host3'}, segment_hosts) self.add_fake_chassis('host2', ['physnet2']) self.add_fake_chassis('host3', ['physnet3']) self.add_fake_chassis('host4', ['physnet1']) self._sync_resources() segment_hosts = segments_db.get_hosts_mapped_with_segments(self.ctx) # host1 should be cleared since it is not in the chassis DB. host3 # should be cleared since there is no segment for mapping. self.assertEqual({'host2', 'host4'}, segment_hosts) class TestOvnNbSyncOverTcp(TestOvnNbSync): def setUp(self): super(TestOvnNbSyncOverTcp, self).setUp() ovn_config.cfg.CONF.set_override( 'enable_distributed_floating_ip', True, group='ovn') def get_ovsdb_server_protocol(self): return 'tcp' class TestOvnSbSyncOverTcp(TestOvnSbSync): def get_ovsdb_server_protocol(self): return 'tcp' class TestOvnNbSyncOverSsl(TestOvnNbSync): def get_ovsdb_server_protocol(self): return 'ssl' class TestOvnSbSyncOverSsl(TestOvnSbSync): def get_ovsdb_server_protocol(self): return 'ssl' networking-ovn-4.0.0/networking_ovn/tests/functional/test_ovn_db_resources.py0000666000175100017510000012235713245511145030072 0ustar zuulzuul00000000000000# 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 mock import netaddr from neutron_lib.api.definitions import dns as dns_apidef from neutron_lib.utils import net as n_net from oslo_config import cfg from ovsdbapp.backend.ovs_idl import idlutils from networking_ovn.common import config as ovn_config from networking_ovn.common import constants as ovn_const from networking_ovn.common import utils from networking_ovn.tests.functional import base class TestNBDbResources(base.TestOVNFunctionalBase): def setUp(self): super(TestNBDbResources, self).setUp() self.orig_get_random_mac = n_net.get_random_mac cfg.CONF.set_override('quota_subnet', -1, group='QUOTAS') ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', False, group='ovn') def tearDown(self): super(TestNBDbResources, self).tearDown() # FIXME(lucasagomes): Map the revision numbers properly instead # of stripping them out. Currently, tests like test_dhcp_options() # are quite complex making it difficult to map the exact the revision # number that the DHCP Option will be at assertion time, we need to # refactor it a little to make it easier for mapping these updates. def _strip_revision_number(self, ext_ids): ext_ids.pop(ovn_const.OVN_REV_NUM_EXT_ID_KEY, None) return ext_ids def _verify_dhcp_option_rows(self, expected_dhcp_options_rows): expected_dhcp_options_rows = list(expected_dhcp_options_rows.values()) observed_dhcp_options_rows = [] for row in self.nb_api.tables['DHCP_Options'].rows.values(): ext_ids = self._strip_revision_number(row.external_ids) observed_dhcp_options_rows.append({ 'cidr': row.cidr, 'external_ids': ext_ids, 'options': row.options}) self.assertItemsEqual(expected_dhcp_options_rows, observed_dhcp_options_rows) def _verify_dhcp_option_row_for_port(self, port_id, expected_lsp_dhcpv4_options, expected_lsp_dhcpv6_options=None): lsp = idlutils.row_by_value(self.nb_api.idl, 'Logical_Switch_Port', 'name', port_id, None) if lsp.dhcpv4_options: ext_ids = self._strip_revision_number( lsp.dhcpv4_options[0].external_ids) observed_lsp_dhcpv4_options = { 'cidr': lsp.dhcpv4_options[0].cidr, 'external_ids': ext_ids, 'options': lsp.dhcpv4_options[0].options} else: observed_lsp_dhcpv4_options = {} if lsp.dhcpv6_options: ext_ids = self._strip_revision_number( lsp.dhcpv6_options[0].external_ids) observed_lsp_dhcpv6_options = { 'cidr': lsp.dhcpv6_options[0].cidr, 'external_ids': ext_ids, 'options': lsp.dhcpv6_options[0].options} else: observed_lsp_dhcpv6_options = {} if expected_lsp_dhcpv6_options is None: expected_lsp_dhcpv6_options = {} self.assertEqual(expected_lsp_dhcpv4_options, observed_lsp_dhcpv4_options) self.assertEqual(expected_lsp_dhcpv6_options, observed_lsp_dhcpv6_options) def _get_subnet_dhcp_mac(self, subnet): mac_key = 'server_id' if subnet['ip_version'] == 6 else 'server_mac' dhcp_options = self.mech_driver._nb_ovn.get_subnet_dhcp_options( subnet['id'])['subnet'] return dhcp_options.get('options', {}).get( mac_key) if dhcp_options else None def test_dhcp_options(self): """Test for DHCP_Options table rows When a new subnet is created, a new row has to be created in the DHCP_Options table for this subnet with the dhcp options stored in the DHCP_Options.options column. When ports are created for this subnet (with IPv4 address set and DHCP enabled in the subnet), the Logical_Switch_Port.dhcpv4_options column should refer to the appropriate row of DHCP_Options. In cases where a port has extra DHCPv4 options defined, a new row in the DHCP_Options table should be created for this port and Logical_Switch_Port.dhcpv4_options colimn should refer to this row. In order to map the DHCP_Options row to the subnet (and to a port), subnet_id is stored in DHCP_Options.external_ids column. For DHCP_Options row which belongs to a port, port_id is also stored in the DHCP_Options.external_ids along with the subnet_id. """ n1 = self._make_network(self.fmt, 'n1', True) created_subnets = {} expected_dhcp_options_rows = {} dhcp_mac = {} for cidr in ['10.0.0.0/24', '20.0.0.0/24', '30.0.0.0/24', '40.0.0.0/24', 'aef0::/64', 'bef0::/64']: ip_version = netaddr.IPNetwork(cidr).ip.version res = self._create_subnet(self.fmt, n1['network']['id'], cidr, ip_version=ip_version) subnet = self.deserialize(self.fmt, res)['subnet'] created_subnets[cidr] = subnet dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) if ip_version == 4: options = {'server_id': cidr.replace('0/24', '1'), 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': subnet['gateway_ip']} else: options = {'server_id': dhcp_mac[subnet['id']]} expected_dhcp_options_rows[subnet['id']] = { 'cidr': cidr, 'external_ids': {'subnet_id': subnet['id']}, 'options': options} for (cidr, enable_dhcp, gateway_ip) in [ ('50.0.0.0/24', False, '50.0.0.1'), ('60.0.0.0/24', True, None), ('cef0::/64', False, 'cef0::1'), ('def0::/64', True, None)]: ip_version = netaddr.IPNetwork(cidr).ip.version res = self._create_subnet(self.fmt, n1['network']['id'], cidr, ip_version=ip_version, enable_dhcp=enable_dhcp, gateway_ip=gateway_ip) subnet = self.deserialize(self.fmt, res)['subnet'] created_subnets[cidr] = subnet dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) if enable_dhcp: if ip_version == 4: options = {} else: options = {'server_id': dhcp_mac[subnet['id']]} expected_dhcp_options_rows[subnet['id']] = { 'cidr': cidr, 'external_ids': {'subnet_id': subnet['id']}, 'options': options} # create a subnet with dns nameservers and host routes n2 = self._make_network(self.fmt, 'n2', True) res = self._create_subnet( self.fmt, n2['network']['id'], '10.0.0.0/24', dns_nameservers=['7.7.7.7', '8.8.8.8'], host_routes=[{'destination': '30.0.0.0/24', 'nexthop': '10.0.0.4'}, {'destination': '40.0.0.0/24', 'nexthop': '10.0.0.8'}]) subnet = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) static_routes = ('{30.0.0.0/24,10.0.0.4, 40.0.0.0/24,' '10.0.0.8, 0.0.0.0/0,10.0.0.1}') expected_dhcp_options_rows[subnet['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n2['network']['mtu']), 'router': subnet['gateway_ip'], 'dns_server': '{7.7.7.7, 8.8.8.8}', 'classless_static_route': static_routes}} # create an IPv6 subnet with dns nameservers res = self._create_subnet( self.fmt, n2['network']['id'], 'ae10::/64', ip_version=6, dns_nameservers=['be10::7', 'be10::8']) subnet = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) expected_dhcp_options_rows[subnet['id']] = { 'cidr': 'ae10::/64', 'external_ids': {'subnet_id': subnet['id']}, 'options': {'server_id': dhcp_mac[subnet['id']], 'dns_server': '{be10::7, be10::8}'}} # Verify that DHCP_Options rows are created for these subnets or not self._verify_dhcp_option_rows(expected_dhcp_options_rows) for cidr in ['20.0.0.0/24', 'aef0::/64']: subnet = created_subnets[cidr] # Disable dhcp in subnet and verify DHCP_Options data = {'subnet': {'enable_dhcp': False}} req = self.new_update_request('subnets', data, subnet['id']) req.get_response(self.api) options = expected_dhcp_options_rows.pop(subnet['id']) self._verify_dhcp_option_rows(expected_dhcp_options_rows) # Re-enable dhcp in subnet and verify DHCP_Options n_net.get_random_mac = mock.Mock() n_net.get_random_mac.return_value = dhcp_mac[subnet['id']] data = {'subnet': {'enable_dhcp': True}} req = self.new_update_request('subnets', data, subnet['id']) req.get_response(self.api) expected_dhcp_options_rows[subnet['id']] = options self._verify_dhcp_option_rows(expected_dhcp_options_rows) n_net.get_random_mac = self.orig_get_random_mac # Create a port and verify if Logical_Switch_Port.dhcpv4_options # is properly set or not subnet = created_subnets['40.0.0.0/24'] subnet_v6 = created_subnets['aef0::/64'] p = self._make_port( self.fmt, n1['network']['id'], fixed_ips=[ {'subnet_id': subnet['id']}, {'subnet_id': subnet_v6['id']}]) self._verify_dhcp_option_row_for_port( p['port']['id'], expected_dhcp_options_rows[subnet['id']], expected_dhcp_options_rows[subnet_v6['id']]) self._verify_dhcp_option_rows(expected_dhcp_options_rows) # create a port with dhcp disabled subnet subnet = created_subnets['50.0.0.0/24'] p = self._make_port(self.fmt, n1['network']['id'], fixed_ips=[{'subnet_id': subnet['id']}]) self._verify_dhcp_option_row_for_port(p['port']['id'], {}) self._verify_dhcp_option_rows(expected_dhcp_options_rows) # Delete the first subnet created subnet = created_subnets['10.0.0.0/24'] req = self.new_delete_request('subnets', subnet['id']) req.get_response(self.api) # Verify that DHCP_Options rows are deleted or not del expected_dhcp_options_rows[subnet['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) def test_port_dhcp_options(self): dhcp_mac = {} n1 = self._make_network(self.fmt, 'n1', True) res = self._create_subnet(self.fmt, n1['network']['id'], '10.0.0.0/24') subnet = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) res = self._create_subnet(self.fmt, n1['network']['id'], 'aef0::/64', ip_version=6) subnet_v6 = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet_v6['id']] = self._get_subnet_dhcp_mac(subnet_v6) expected_dhcp_options_rows = { subnet['id']: { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': subnet['gateway_ip']}}, subnet_v6['id']: { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': subnet_v6['id']}, 'options': {'server_id': dhcp_mac[subnet_v6['id']]}}} expected_dhcp_v4_options_rows = { subnet['id']: expected_dhcp_options_rows[subnet['id']]} expected_dhcp_v6_options_rows = { subnet_v6['id']: expected_dhcp_options_rows[subnet_v6['id']]} data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': subnet['id']}], 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'mtu', 'opt_value': '1100'}, {'ip_version': 4, 'opt_name': 'ntp-server', 'opt_value': '8.8.8.8'}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p1 = self.deserialize(self.fmt, port_res) expected_dhcp_options_rows['v4-' + p1['port']['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id'], 'port_id': p1['port']['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': '1100', 'router': subnet['gateway_ip'], 'ntp_server': '8.8.8.8'}} expected_dhcp_v4_options_rows['v4-' + p1['port']['id']] = \ expected_dhcp_options_rows['v4-' + p1['port']['id']] data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': subnet['id']}], 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'ip-forward-enable', 'opt_value': '1'}, {'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': '10.0.0.100'}, {'ip_version': 4, 'opt_name': 'dns-server', 'opt_value': '20.20.20.20'}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p2 = self.deserialize(self.fmt, port_res) expected_dhcp_options_rows['v4-' + p2['port']['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id'], 'port_id': p2['port']['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': subnet['gateway_ip'], 'ip_forward_enable': '1', 'tftp_server': '10.0.0.100', 'dns_server': '20.20.20.20'}} expected_dhcp_v4_options_rows['v4-' + p2['port']['id']] = \ expected_dhcp_options_rows['v4-' + p2['port']['id']] data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': subnet_v6['id']}], 'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'dns-server', 'opt_value': 'aef0::1'}, {'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'foo-domain'}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p3 = self.deserialize(self.fmt, port_res) expected_dhcp_options_rows['v6-' + p3['port']['id']] = { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': subnet_v6['id'], 'port_id': p3['port']['id']}, 'options': {'server_id': dhcp_mac[subnet_v6['id']], 'dns_server': 'aef0::1', 'domain_search': 'foo-domain'}} expected_dhcp_v6_options_rows['v6-' + p3['port']['id']] = \ expected_dhcp_options_rows['v6-' + p3['port']['id']] data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': subnet['id']}, {'subnet_id': subnet_v6['id']}], 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': '100.0.0.100'}, {'ip_version': 6, 'opt_name': 'dns-server', 'opt_value': 'aef0::100'}, {'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': 'bar-domain'}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p4 = self.deserialize(self.fmt, port_res) expected_dhcp_options_rows['v6-' + p4['port']['id']] = { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': subnet_v6['id'], 'port_id': p4['port']['id']}, 'options': {'server_id': dhcp_mac[subnet_v6['id']], 'dns_server': 'aef0::100', 'domain_search': 'bar-domain'}} expected_dhcp_options_rows['v4-' + p4['port']['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id'], 'port_id': p4['port']['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': subnet['gateway_ip'], 'tftp_server': '100.0.0.100'}} expected_dhcp_v4_options_rows['v4-' + p4['port']['id']] = \ expected_dhcp_options_rows['v4-' + p4['port']['id']] expected_dhcp_v6_options_rows['v6-' + p4['port']['id']] = \ expected_dhcp_options_rows['v6-' + p4['port']['id']] # test port without extra_dhcp_opts but using subnet DHCP options data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'fixed_ips': [{'subnet_id': subnet['id']}, {'subnet_id': subnet_v6['id']}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p5 = self.deserialize(self.fmt, port_res) self._verify_dhcp_option_rows(expected_dhcp_options_rows) self._verify_dhcp_option_row_for_port( p1['port']['id'], expected_dhcp_options_rows['v4-' + p1['port']['id']]) self._verify_dhcp_option_row_for_port( p2['port']['id'], expected_dhcp_options_rows['v4-' + p2['port']['id']]) self._verify_dhcp_option_row_for_port( p3['port']['id'], {}, expected_lsp_dhcpv6_options=expected_dhcp_options_rows[ 'v6-' + p3['port']['id']]) self._verify_dhcp_option_row_for_port( p4['port']['id'], expected_dhcp_options_rows['v4-' + p4['port']['id']], expected_lsp_dhcpv6_options=expected_dhcp_options_rows[ 'v6-' + p4['port']['id']]) self._verify_dhcp_option_row_for_port( p5['port']['id'], expected_dhcp_options_rows[subnet['id']], expected_lsp_dhcpv6_options=expected_dhcp_options_rows[ subnet_v6['id']]) # Update the subnet with dns_server. It should get propagated # to the DHCP options of the p1. Note that it should not get # propagate to DHCP options of port p2 because, it has overridden # dns-server in the Extra DHCP options. data = {'subnet': {'dns_nameservers': ['7.7.7.7', '8.8.8.8']}} req = self.new_update_request('subnets', data, subnet['id']) req.get_response(self.api) for i in [subnet['id'], 'v4-' + p1['port']['id'], 'v4-' + p4['port']['id']]: expected_dhcp_options_rows[i]['options']['dns_server'] = ( '{7.7.7.7, 8.8.8.8}') self._verify_dhcp_option_rows(expected_dhcp_options_rows) # Update the port p2 by removing dns-server and tfp-server in the # extra DHCP options. dns-server option from the subnet DHCP options # should be updated in the p2 DHCP options data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'ip-forward-enable', 'opt_value': '0'}, {'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': None}, {'ip_version': 4, 'opt_name': 'dns-server', 'opt_value': None}]}} port_req = self.new_update_request('ports', data, p2['port']['id']) port_req.get_response(self.api) p2_expected = expected_dhcp_options_rows['v4-' + p2['port']['id']] p2_expected['options']['dns_server'] = '{7.7.7.7, 8.8.8.8}' p2_expected['options']['ip_forward_enable'] = '0' del p2_expected['options']['tftp_server'] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # Test subnet DHCP disabling and enabling for (subnet_id, expect_subnet_rows_disabled, expect_port_row_disabled ) in [ (subnet['id'], expected_dhcp_v6_options_rows, [(p4, {}, expected_dhcp_options_rows['v6-' + p4['port']['id']]), (p5, {}, expected_dhcp_options_rows[subnet_v6['id']])]), (subnet_v6['id'], expected_dhcp_v4_options_rows, [(p4, expected_dhcp_options_rows['v4-' + p4['port']['id']], {}), (p5, expected_dhcp_options_rows[subnet['id']], {})])]: # Disable subnet's DHCP and verify DHCP_Options, data = {'subnet': {'enable_dhcp': False}} req = self.new_update_request('subnets', data, subnet_id) req.get_response(self.api) # DHCP_Options belonging to the subnet or it's ports should be all # removed, current DHCP_Options should be equal to # expect_subnet_rows_disabled self._verify_dhcp_option_rows(expect_subnet_rows_disabled) # Verify that the corresponding port DHCP options were cleared # and the others were not affected. for p in expect_port_row_disabled: self._verify_dhcp_option_row_for_port( p[0]['port']['id'], p[1], p[2]) # Re-enable dhcpv4 in subnet and verify DHCP_Options n_net.get_random_mac = mock.Mock() n_net.get_random_mac.return_value = dhcp_mac[subnet_id] data = {'subnet': {'enable_dhcp': True}} req = self.new_update_request('subnets', data, subnet_id) req.get_response(self.api) self._verify_dhcp_option_rows(expected_dhcp_options_rows) self._verify_dhcp_option_row_for_port( p4['port']['id'], expected_dhcp_options_rows['v4-' + p4['port']['id']], expected_dhcp_options_rows['v6-' + p4['port']['id']]) self._verify_dhcp_option_row_for_port( p5['port']['id'], expected_dhcp_options_rows[subnet['id']], expected_lsp_dhcpv6_options=expected_dhcp_options_rows[ subnet_v6['id']]) n_net.get_random_mac = self.orig_get_random_mac # Disable dhcp in p2 data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'dhcp_disabled', 'opt_value': 'true'}]}} port_req = self.new_update_request('ports', data, p2['port']['id']) port_req.get_response(self.api) del expected_dhcp_options_rows['v4-' + p2['port']['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # delete port p1. port_req = self.new_delete_request('ports', p1['port']['id']) port_req.get_response(self.api) del expected_dhcp_options_rows['v4-' + p1['port']['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # delete the IPv6 extra DHCP options for p4 data = {'port': {'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'dns-server', 'opt_value': None}, {'ip_version': 6, 'opt_name': 'domain-search', 'opt_value': None}]}} port_req = self.new_update_request('ports', data, p4['port']['id']) port_req.get_response(self.api) del expected_dhcp_options_rows['v6-' + p4['port']['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) def test_port_dhcp_opts_add_and_remove_extra_dhcp_opts(self): """Orphaned DHCP_Options row. In this test case a port is created with extra DHCP options. Since it has extra DHCP options a new row in the DHCP_Options is created for this port. Next the port is updated to delete the extra DHCP options. After the update, the Logical_Switch_Port.dhcpv4_options for this port should refer to the subnet DHCP_Options and the DHCP_Options row created for this port earlier should be deleted. """ dhcp_mac = {} n1 = self._make_network(self.fmt, 'n1', True) res = self._create_subnet(self.fmt, n1['network']['id'], '10.0.0.0/24') subnet = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet['id']] = self._get_subnet_dhcp_mac(subnet) res = self._create_subnet(self.fmt, n1['network']['id'], 'aef0::/64', ip_version=6) subnet_v6 = self.deserialize(self.fmt, res)['subnet'] dhcp_mac[subnet_v6['id']] = self._get_subnet_dhcp_mac(subnet_v6) expected_dhcp_options_rows = { subnet['id']: { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': str(n1['network']['mtu']), 'router': subnet['gateway_ip']}}, subnet_v6['id']: { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': subnet_v6['id']}, 'options': {'server_id': dhcp_mac[subnet_v6['id']]}}} data = { 'port': {'network_id': n1['network']['id'], 'tenant_id': self._tenant_id, 'device_owner': 'compute:None', 'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'mtu', 'opt_value': '1100'}, {'ip_version': 4, 'opt_name': 'ntp-server', 'opt_value': '8.8.8.8'}, {'ip_version': 6, 'opt_name': 'dns-server', 'opt_value': 'aef0::100'}]}} port_req = self.new_create_request('ports', data, self.fmt) port_res = port_req.get_response(self.api) p1 = self.deserialize(self.fmt, port_res)['port'] expected_dhcp_options_rows['v4-' + p1['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id'], 'port_id': p1['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': '1100', 'router': subnet['gateway_ip'], 'ntp_server': '8.8.8.8'}} expected_dhcp_options_rows['v6-' + p1['id']] = { 'cidr': 'aef0::/64', 'external_ids': {'subnet_id': subnet_v6['id'], 'port_id': p1['id']}, 'options': {'server_id': dhcp_mac[subnet_v6['id']], 'dns_server': 'aef0::100'}} self._verify_dhcp_option_rows(expected_dhcp_options_rows) # The Logical_Switch_Port.dhcp(v4/v6)_options should refer to the # port DHCP options. self._verify_dhcp_option_row_for_port( p1['id'], expected_dhcp_options_rows['v4-' + p1['id']], expected_dhcp_options_rows['v6-' + p1['id']]) # Now update the port to delete the extra DHCP options data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'mtu', 'opt_value': None}, {'ip_version': 4, 'opt_name': 'ntp-server', 'opt_value': None}]}} port_req = self.new_update_request('ports', data, p1['id']) port_req.get_response(self.api) # DHCP_Options row created for the port earlier should have been # deleted. del expected_dhcp_options_rows['v4-' + p1['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # The Logical_Switch_Port.dhcpv4_options for this port should refer to # the subnet DHCP options. self._verify_dhcp_option_row_for_port( p1['id'], expected_dhcp_options_rows[subnet['id']], expected_dhcp_options_rows['v6-' + p1['id']]) # update the port again with extra DHCP options. data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'mtu', 'opt_value': '1200'}, {'ip_version': 4, 'opt_name': 'tftp-server', 'opt_value': '8.8.8.8'}]}} port_req = self.new_update_request('ports', data, p1['id']) port_req.get_response(self.api) expected_dhcp_options_rows['v4-' + p1['id']] = { 'cidr': '10.0.0.0/24', 'external_ids': {'subnet_id': subnet['id'], 'port_id': p1['id']}, 'options': {'server_id': '10.0.0.1', 'server_mac': dhcp_mac[subnet['id']], 'lease_time': str(12 * 60 * 60), 'mtu': '1200', 'router': subnet['gateway_ip'], 'tftp_server': '8.8.8.8'}} self._verify_dhcp_option_rows(expected_dhcp_options_rows) self._verify_dhcp_option_row_for_port( p1['id'], expected_dhcp_options_rows['v4-' + p1['id']], expected_dhcp_options_rows['v6-' + p1['id']]) # Disable DHCPv4 for this port. The DHCP_Options row created for this # port should be get deleted. data = {'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'dhcp_disabled', 'opt_value': 'true'}]}} port_req = self.new_update_request('ports', data, p1['id']) port_req.get_response(self.api) del expected_dhcp_options_rows['v4-' + p1['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # The Logical_Switch_Port.dhcpv4_options for this port should be # empty. self._verify_dhcp_option_row_for_port( p1['id'], {}, expected_dhcp_options_rows['v6-' + p1['id']]) # Disable DHCPv6 for this port. The DHCP_Options row created for this # port should be get deleted. data = {'port': {'extra_dhcp_opts': [{'ip_version': 6, 'opt_name': 'dhcp_disabled', 'opt_value': 'true'}]}} port_req = self.new_update_request('ports', data, p1['id']) port_req.get_response(self.api) del expected_dhcp_options_rows['v6-' + p1['id']] self._verify_dhcp_option_rows(expected_dhcp_options_rows) # The Logical_Switch_Port.dhcpv4_options for this port should be # empty. self._verify_dhcp_option_row_for_port(p1['id'], {}) class TestDNSRecords(base.TestOVNFunctionalBase): _extension_drivers = ['port_security', 'dns'] def _validate_dns_records(self, expected_dns_records): observed_dns_records = [] for dns_row in self.nb_api.tables['DNS'].rows.values(): observed_dns_records.append( {'external_ids': dns_row.external_ids, 'records': dns_row.records}) self.assertItemsEqual(expected_dns_records, observed_dns_records) def _validate_ls_dns_records(self, lswitch_name, expected_dns_records): ls = idlutils.row_by_value(self.nb_api.idl, 'Logical_Switch', 'name', lswitch_name) observed_dns_records = [] for dns_row in ls.dns_records: observed_dns_records.append( {'external_ids': dns_row.external_ids, 'records': dns_row.records}) self.assertItemsEqual(expected_dns_records, observed_dns_records) def setUp(self): ovn_config.cfg.CONF.set_override('dns_domain', 'ovn.test') super(TestDNSRecords, self).setUp() def test_dns_records(self): expected_dns_records = [] nets = [] for n, cidr in [('n1', '10.0.0.0/24'), ('n2', '20.0.0.0/24')]: net_kwargs = {dns_apidef.DNSDOMAIN: 'ovn.test.'} net_kwargs['arg_list'] = (dns_apidef.DNSDOMAIN,) res = self._create_network(self.fmt, n, True, **net_kwargs) net = self.deserialize(self.fmt, res) nets.append(net) res = self._create_subnet(self.fmt, net['network']['id'], cidr) self.deserialize(self.fmt, res) # At this point no dns records should be created n1_lswitch_name = utils.ovn_name(nets[0]['network']['id']) n2_lswitch_name = utils.ovn_name(nets[1]['network']['id']) self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, expected_dns_records) self._validate_ls_dns_records(n2_lswitch_name, expected_dns_records) port_kwargs = {'arg_list': (dns_apidef.DNSNAME,), dns_apidef.DNSNAME: 'n1p1'} res = self._create_port(self.fmt, nets[0]['network']['id'], device_id='n1p1', **port_kwargs) n1p1 = self.deserialize(self.fmt, res) port_ips = " ".join([f['ip_address'] for f in n1p1['port']['fixed_ips']]) expected_dns_records = [ {'external_ids': {'ls_name': n1_lswitch_name}, 'records': {'n1p1': port_ips, 'n1p1.ovn.test.': port_ips}} ] self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) self._validate_ls_dns_records(n2_lswitch_name, []) # Create another port, but don't set dns_name. dns record should not # be updated. res = self._create_port(self.fmt, nets[1]['network']['id'], device_id='n2p1') n2p1 = self.deserialize(self.fmt, res) self._validate_dns_records(expected_dns_records) # Update port p2 with dns_name. The dns record should be updated. body = {'dns_name': 'n2p1'} data = {'port': body} req = self.new_update_request('ports', data, n2p1['port']['id']) res = req.get_response(self.api) self.assertEqual(200, res.status_int) port_ips = " ".join([f['ip_address'] for f in n2p1['port']['fixed_ips']]) expected_dns_records.append( {'external_ids': {'ls_name': n2_lswitch_name}, 'records': {'n2p1': port_ips, 'n2p1.ovn.test.': port_ips}}) self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) self._validate_ls_dns_records(n2_lswitch_name, [expected_dns_records[1]]) # Create n1p2 port_kwargs = {'arg_list': (dns_apidef.DNSNAME,), dns_apidef.DNSNAME: 'n1p2'} res = self._create_port(self.fmt, nets[0]['network']['id'], device_id='n1p1', **port_kwargs) n1p2 = self.deserialize(self.fmt, res) port_ips = " ".join([f['ip_address'] for f in n1p2['port']['fixed_ips']]) expected_dns_records[0]['records']['n1p2'] = port_ips expected_dns_records[0]['records']['n1p2.ovn.test.'] = port_ips self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) self._validate_ls_dns_records(n2_lswitch_name, [expected_dns_records[1]]) # Remove device_id from n1p1 body = {'device_id': ''} data = {'port': body} req = self.new_update_request('ports', data, n1p1['port']['id']) res = req.get_response(self.api) self.assertEqual(200, res.status_int) expected_dns_records[0]['records'].pop('n1p1') expected_dns_records[0]['records'].pop('n1p1.ovn.test.') self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) self._validate_ls_dns_records(n2_lswitch_name, [expected_dns_records[1]]) # Delete n2p1 self._delete('ports', n2p1['port']['id']) expected_dns_records[1]['records'] = {} self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) self._validate_ls_dns_records(n2_lswitch_name, [expected_dns_records[1]]) # Delete n2 self._delete('networks', nets[1]['network']['id']) del expected_dns_records[1] self._validate_dns_records(expected_dns_records) self._validate_ls_dns_records(n1_lswitch_name, [expected_dns_records[0]]) # Delete n1p1 and n1p2 and n1 self._delete('ports', n1p1['port']['id']) self._delete('ports', n1p2['port']['id']) self._delete('networks', nets[0]['network']['id']) self._validate_dns_records([]) class TestNBDbResourcesOverTcp(TestNBDbResources): def get_ovsdb_server_protocol(self): return 'tcp' class TestNBDbResourcesOverSsl(TestNBDbResources): def get_ovsdb_server_protocol(self): return 'ssl' networking-ovn-4.0.0/networking_ovn/tests/functional/requirements.txt0000666000175100017510000000047713245511145026402 0ustar zuulzuul00000000000000# Additional requirements for functional tests # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. psutil>=1.1.1,<2.0.0 psycopg2 PyMySQL>=0.6.2 # MIT License networking-ovn-4.0.0/networking_ovn/tests/functional/test_trunk_driver.py0000666000175100017510000001201113245511145027230 0ustar zuulzuul00000000000000# Copyright 2017 DT Dream Technology 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. import contextlib from networking_ovn.tests.functional import base from neutron.objects import ports as obj_port from neutron.plugins.common import utils from neutron.services.trunk import constants as trunk_consts from neutron.services.trunk import plugin as trunk_plugin from neutron_lib import constants as n_consts from oslo_utils import uuidutils class TestOVNTrunkDriver(base.TestOVNFunctionalBase): def setUp(self): super(TestOVNTrunkDriver, self).setUp() self.trunk_plugin = trunk_plugin.TrunkPlugin() self.trunk_plugin.add_segmentation_type(trunk_consts.VLAN, utils.is_valid_vlan_tag) @contextlib.contextmanager def trunk(self, sub_ports=None): sub_ports = sub_ports or [] with self.network() as network: with self.subnet(network=network) as subnet: with self.port(subnet=subnet) as parent_port: tenant_id = uuidutils.generate_uuid() trunk = {'trunk': { 'port_id': parent_port['port']['id'], 'tenant_id': tenant_id, 'project_id': tenant_id, 'admin_state_up': True, 'name': 'trunk', 'sub_ports': sub_ports}} trunk = self.trunk_plugin.create_trunk(self.context, trunk) yield trunk @contextlib.contextmanager def subport(self): with self.port() as port: sub_port = {'segmentation_type': 'vlan', 'segmentation_id': 1000, 'port_id': port['port']['id']} yield sub_port def _get_ovn_trunk_info(self): ovn_trunk_info = [] for row in self.nb_api.tables[ 'Logical_Switch_Port'].rows.values(): if row.parent_name and row.tag: ovn_trunk_info.append({'port_id': row.name, 'parent_port_id': row.parent_name, 'tag': row.tag}) return ovn_trunk_info def _verify_trunk_info(self, trunk, has_items): ovn_subports_info = self._get_ovn_trunk_info() neutron_subports_info = [] for subport in trunk.get('sub_ports', []): neutron_subports_info.append({'port_id': subport['port_id'], 'parent_port_id': [trunk['port_id']], 'tag': [subport['segmentation_id']]}) # Check that the subport has the binding is active. binding = obj_port.PortBinding.get_object( self.context, port_id=subport['port_id'], host='') self.assertEqual(n_consts.PORT_STATUS_ACTIVE, binding['status']) self.assertItemsEqual(ovn_subports_info, neutron_subports_info) self.assertEqual(has_items, len(neutron_subports_info) != 0) if trunk.get('status'): self.assertEqual(trunk_consts.ACTIVE_STATUS, trunk['status']) def test_trunk_create(self): with self.trunk() as trunk: self._verify_trunk_info(trunk, has_items=False) def test_trunk_create_with_subports(self): with self.subport() as subport: with self.trunk([subport]) as trunk: self._verify_trunk_info(trunk, has_items=True) def test_subport_add(self): with self.subport() as subport: with self.trunk() as trunk: self.trunk_plugin.add_subports(self.context, trunk['id'], {'sub_ports': [subport]}) new_trunk = self.trunk_plugin.get_trunk(self.context, trunk['id']) self._verify_trunk_info(new_trunk, has_items=True) def test_subport_delete(self): with self.subport() as subport: with self.trunk([subport]) as trunk: self.trunk_plugin.remove_subports(self.context, trunk['id'], {'sub_ports': [subport]}) new_trunk = self.trunk_plugin.get_trunk(self.context, trunk['id']) self._verify_trunk_info(new_trunk, has_items=False) def test_trunk_delete(self): with self.trunk() as trunk: self.trunk_plugin.delete_trunk(self.context, trunk['id']) self._verify_trunk_info({}, has_items=False) networking-ovn-4.0.0/networking_ovn/tests/functional/resources/0000775000175100017510000000000013245511554025122 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/resources/__init__.py0000666000175100017510000000000013245511145027217 0ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/functional/resources/process.py0000666000175100017510000001550413245511164027156 0ustar zuulzuul00000000000000# 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. from distutils import spawn import os import fixtures from neutron.agent.linux import utils import psutil import tenacity class OvsdbServer(fixtures.Fixture): def __init__(self, temp_dir, ovs_dir, ovn_nb_db=True, ovn_sb_db=False, protocol='unix'): super(OvsdbServer, self).__init__() self.temp_dir = temp_dir self.ovs_dir = ovs_dir self.ovn_nb_db = ovn_nb_db self.ovn_sb_db = ovn_sb_db # The value of the protocol must be unix or tcp or ssl self.protocol = protocol self.ovsdb_server_processes = [] self.private_key = os.path.join(self.temp_dir, 'ovn-privkey.pem') self.certificate = os.path.join(self.temp_dir, 'ovn-cert.pem') self.ca_cert = os.path.join(self.temp_dir, 'controllerca', 'cacert.pem') def _setUp(self): if self.ovn_nb_db: self.ovsdb_server_processes.append( {'db_path': self.temp_dir + '/ovn_nb.db', 'schema_path': self.ovs_dir + '/ovn-nb.ovsschema', 'remote_path': self.temp_dir + '/ovnnb_db.sock', 'protocol': self.protocol, 'remote_ip': '127.0.0.1', 'remote_port': '0', 'unixctl_path': self.temp_dir + '/ovnnb_db.ctl', 'log_file_path': self.temp_dir + '/ovn_nb.log', 'db_type': 'nb', 'connection': 'db:OVN_Northbound,NB_Global,connections', 'ctl_cmd': 'ovn-nbctl'}) if self.ovn_sb_db: self.ovsdb_server_processes.append( {'db_path': self.temp_dir + '/ovn_sb.db', 'schema_path': self.ovs_dir + '/ovn-sb.ovsschema', 'remote_path': self.temp_dir + '/ovnsb_db.sock', 'protocol': self.protocol, 'remote_ip': '127.0.0.1', 'remote_port': '0', 'unixctl_path': self.temp_dir + '/ovnsb_db.ctl', 'log_file_path': self.temp_dir + '/ovn_sb.log', 'db_type': 'sb', 'connection': 'db:OVN_Southbound,SB_Global,connections', 'ctl_cmd': 'ovn-sbctl'}) self.addCleanup(self.stop) self.start() def _init_ovsdb_pki(self): os.chdir(self.temp_dir) pki_init_cmd = [spawn.find_executable('ovs-pki'), 'init', '-d', self.temp_dir, '-l', os.path.join(self.temp_dir, 'pki.log'), '--force'] utils.execute(pki_init_cmd) pki_req_sign = [spawn.find_executable('ovs-pki'), 'req+sign', 'ovn', 'controller', '-d', self.temp_dir, '-l', os.path.join(self.temp_dir, 'pki.log'), '--force'] utils.execute(pki_req_sign) def start(self): pki_done = False for ovsdb_process in self.ovsdb_server_processes: # create the db from the schema using ovsdb-tool ovsdb_tool_cmd = [spawn.find_executable('ovsdb-tool'), 'create', ovsdb_process['db_path'], ovsdb_process['schema_path']] utils.execute(ovsdb_tool_cmd) # start the ovsdb-server ovsdb_server_cmd = [ spawn.find_executable('ovsdb-server'), '-vconsole:off', '--log-file=%s' % (ovsdb_process['log_file_path']), '--remote=punix:%s' % (ovsdb_process['remote_path']), '--remote=%s' % (ovsdb_process['connection']), '--unixctl=%s' % (ovsdb_process['unixctl_path'])] if ovsdb_process['protocol'] == 'ssl': if not pki_done: pki_done = True self._init_ovsdb_pki() ovsdb_server_cmd.append('--private-key=%s' % self.private_key) ovsdb_server_cmd.append('--certificate=%s' % self.certificate) ovsdb_server_cmd.append('--ca-cert=%s' % self.ca_cert) ovsdb_server_cmd.append(ovsdb_process['db_path']) obj, _ = utils.create_process(ovsdb_server_cmd) conn_cmd = [spawn.find_executable(ovsdb_process['ctl_cmd']), '--db=unix:%s' % ovsdb_process['remote_path'], 'set-connection', 'p%s:%s:%s' % (ovsdb_process['protocol'], ovsdb_process['remote_port'], ovsdb_process['remote_ip']), '--', 'set', 'connection', '.', 'inactivity_probe=60000'] @tenacity.retry(wait=tenacity.wait_exponential(multiplier=0.1), stop=tenacity.stop_after_delay(3), reraise=True) def _set_connection(): utils.execute(conn_cmd) @tenacity.retry( wait=tenacity.wait_exponential(multiplier=0.1), stop=tenacity.stop_after_delay(10), reraise=True) def get_ovsdb_remote_port_retry(pid): process = psutil.Process(pid) for connect in process.connections(): if connect.status == 'LISTEN': return connect.laddr[1] raise Exception(_("Could not find LISTEN port.")) if ovsdb_process['protocol'] != 'unix': _set_connection() ovsdb_process['remote_port'] = \ get_ovsdb_remote_port_retry(obj.pid) def stop(self): for ovsdb_process in self.ovsdb_server_processes: try: stop_cmd = ['ovs-appctl', '-t', ovsdb_process['unixctl_path'], 'exit'] utils.execute(stop_cmd) except Exception: pass def get_ovsdb_connection_path(self, db_type='nb'): for ovsdb_process in self.ovsdb_server_processes: if ovsdb_process['db_type'] == db_type: if ovsdb_process['protocol'] == 'unix': return 'unix:' + ovsdb_process['remote_path'] else: return '%s:%s:%s' % (ovsdb_process['protocol'], ovsdb_process['remote_ip'], ovsdb_process['remote_port']) networking-ovn-4.0.0/networking_ovn/tests/contrib/0000775000175100017510000000000013245511554022406 5ustar zuulzuul00000000000000networking-ovn-4.0.0/networking_ovn/tests/contrib/gate_hook.sh0000666000175100017510000000235013245511164024701 0ustar zuulzuul00000000000000#!/usr/bin/env bash set -ex VENV=${1:-"dsvm-functional"} GATE_DEST=$BASE/new NEUTRON_PATH=$GATE_DEST/neutron DEVSTACK_PATH=$GATE_DEST/devstack GATE_STACK_USER=stack case $VENV in "dsvm-functional"|"dsvm-functional-py35") source $DEVSTACK_PATH/functions source $NEUTRON_PATH/devstack/lib/ovs # NOTE(numans) Functional tests after upgrade to xenial in # the CI are breaking because of missing six package. # Installing the package for now as a workaround # https://bugs.launchpad.net/networking-ovn/+bug/1648670 sudo pip install six # Install SSL dependencies here for now as a workaround # https://bugs.launchpad.net/networking-ovn/+bug/1696713 if is_fedora ; then install_package openssl-devel elif is_ubuntu ; then install_package libssl-dev fi # In order to run functional tests, we want to compile OVS # from sources and installed. We don't need to start ovs services. remove_ovs_packages # compile_ovs expects "DEST" to be defined DEST=$GATE_DEST compile_ovs True /usr/local /var # Make the workspace owned by GATE_STACK_USER sudo chown -R $GATE_STACK_USER:$GATE_STACK_USER $BASE ;; *) echo "Unrecognized environment $VENV". exit 1 esac networking-ovn-4.0.0/networking_ovn/tests/contrib/post_test_hook.sh0000666000175100017510000000364313245511145026012 0ustar zuulzuul00000000000000#!/usr/bin/env bash set -xe NETWORKING_OVN_DIR="$BASE/new/networking-ovn" SCRIPTS_DIR="/usr/os-testr-env/bin/" GATE_STACK_USER=stack venv=${1:-"dsvm-functional"} function generate_testr_results { # Give job user rights to access tox logs sudo -H -u $owner chmod o+rw . sudo -H -u $owner chmod o+rw -R .stestr if [ -f ".stestr/0" ] ; then .tox/$venv/bin/subunit-1to2 < .stestr/0 > ./stestr.subunit $SCRIPTS_DIR/subunit2html ./stestr.subunit testr_results.html gzip -9 ./stestr.subunit gzip -9 ./testr_results.html sudo mv ./*.gz /opt/stack/logs/ fi } function generate_log_index { local xtrace xtrace=$(set +o | grep xtrace) set +o xtrace # honor job flavors like -python35 case $venv in *"dsvm-functional"*) venv="dsvm-functional" ;; *) echo "Unrecognized environment $venv". exit 1 esac virtualenv /tmp/os-log-merger /tmp/os-log-merger/bin/pip install -U os-log-merger==1.1.0 files=$(find /opt/stack/logs/$venv-logs -name '*.txt' -o -name '*.log') # -a3 to truncate common path prefix # || true to avoid the whole run failure because of os-log-merger crashes and such # TODO(ihrachys) remove || true when we have more trust in os-log-merger contents=$(/tmp/os-log-merger/bin/os-log-merger -a3 $files || true) echo "$contents" | sudo tee /opt/stack/logs/$venv-index.txt > /dev/null $xtrace } if [[ "$venv" == dsvm-functional* ]] then owner=$GATE_STACK_USER sudo_env= # Set owner permissions according to job's requirements. cd $NETWORKING_OVN_DIR sudo chown -R $owner:$owner $NETWORKING_OVN_DIR # Run tests echo "Running networking-ovn $venv test suite" set +e sudo -H -u $owner $sudo_env tox -e $venv testr_exit_code=$? set -e # Collect and parse results generate_testr_results generate_log_index exit $testr_exit_code fi networking-ovn-4.0.0/networking_ovn/tests/contrib/README0000666000175100017510000000021313245511145023260 0ustar zuulzuul00000000000000The files in this directory are intended for use by the networking-ovn infra jobs that run the various functional test suites in the gate. networking-ovn-4.0.0/networking_ovn/tests/base.py0000666000175100017510000000254513245511145022236 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_utils import fileutils from oslotest import base from networking_ovn.common import config def setup_test_logging(config_opts, log_dir, log_file_path_template): # Have each test log into its own log file config_opts.set_override('debug', True) fileutils.ensure_tree(log_dir, mode=0o755) log_file = sanitize_log_path( os.path.join(log_dir, log_file_path_template)) config_opts.set_override('log_file', log_file) config.setup_logging() def sanitize_log_path(path): # Sanitize the string so that its log path is shell friendly replace_map = {' ': '-', '(': '_', ')': '_'} for s, r in replace_map.items(): path = path.replace(s, r) return path class TestCase(base.BaseTestCase): """Test case base class for all unit tests.""" networking-ovn-4.0.0/networking_ovn/tests/__init__.py0000666000175100017510000000000013245511145023043 0ustar zuulzuul00000000000000networking-ovn-4.0.0/migration/0000775000175100017510000000000013245511554016524 5ustar zuulzuul00000000000000networking-ovn-4.0.0/migration/README.rst0000666000175100017510000000251313245511145020212 0ustar zuulzuul00000000000000Migration from ML2/OVS to ML2/OVN ================================= Proof-of-concept ansible script for migrating an OpenStack deployment that uses ML2/OVS to OVN. Prerequisites: 1. Ansible 2.2 or greater. 2. ML2/OVS must be using the OVS firewall driver. To use: 1. Create an ansible inventory with the expected set of groups and variables as indicated by the hosts-sample file. 2. Run the playbook:: $ ansible-playbook migrate-to-ovn.yml -i hosts Testing Status: - Tested on an RDO cloud on CentOS 7.3 based on Ocata. - The cloud had 3 controller nodes and 6 compute nodes. - Observed network downtime was 10 seconds. - The "--forks 10" option was used with ansible-playbook to ensure that commands could be run across the entire environment in parallel. MTU: - If migrating an ML2/OVS deployment using VXLAN tenant networks to an OVN deployment using Geneve for tenant networks, we have an unresolved issue around MTU. The VXLAN overhead is 30 bytes. OVN with Geneve has an overhead of 38 bytes. We need the tenant networks MTU adjusted for OVN and then we need all VMs to receive the updated MTU value through DHCP before the migration can take place. For testing purposes, we've just hacked the Neutron code to indicate that the VXLAN overhead was 38 bytes instead of 30, bypassing the issue at migration time. networking-ovn-4.0.0/migration/hosts.sample0000666000175100017510000000232013245511145021062 0ustar zuulzuul00000000000000# All controller nodes running OpenStack control services, particularly # neutron-api. Also indicate which controller you'd like to have run # the OVN central control services. [controller] overcloud-controller-0 ovn_central=true overcloud-controller-1 overcloud-controller-2 # All compute nodes. We will replace the openvswitch agent # with ovn-controller on these nodes. # # The ovn_encap_ip variable should be filled in with the IP # address that other compute hosts should use as the tunnel # endpoint for tunnels to that host. [compute] overcloud-novacompute-0 ovn_encap_ip=192.0.2.10 overcloud-novacompute-1 ovn_encap_ip=192.0.2.11 overcloud-novacompute-2 ovn_encap_ip=192.0.2.12 overcloud-novacompute-3 ovn_encap_ip=192.0.2.13 overcloud-novacompute-4 ovn_encap_ip=192.0.2.14 overcloud-novacompute-5 ovn_encap_ip=192.0.2.15 # Configure bridge mappings to be used on compute hosts. [compute:vars] ovn_bridge_mappings=net1:br-em1 is_compute_node=true [overcloud:children] controller compute # Fill in "ovn_db_ip" with an IP address on a management network # that the controller and compute nodes should reach. This address # should not be reachable otherwise. [overcloud:vars] ovn_db_ip=192.0.2.50 remote_user=heat-admin networking-ovn-4.0.0/migration/migrate-to-ovn.yml0000666000175100017510000002017113245511145022116 0ustar zuulzuul00000000000000# Migrate a Neutron deployment using ML2/OVS to OVN. # # See hosts-sample for expected contents of the ansible inventory. --- - hosts: compute remote_user: "{{ remote_user }}" become: true tasks: - name: Ensure OVN packages are installed on compute nodes. yum: name: openvswitch-ovn-host state: present # TODO to make ansible-lint happy, all of these commands should be conditionally run # only if the config value needs to be changed. - name: Configure ovn-encap-type. command: "ovs-vsctl set open . external_ids:ovn-encap-type=geneve" - name: Configure ovn-encap-ip. command: "ovs-vsctl set open . external_ids:ovn-encap-ip={{ ovn_encap_ip }}" - name: Configure ovn-remote. command: "ovs-vsctl set open . external_ids:ovn-remote=tcp:{{ ovn_db_ip }}:6642" # TODO We could discover the appropriate value for ovn-bridge-mappings based on # the openvswitch agent configuration instead of requiring it to be configured # in the inventory. - name: Configure ovn-bridge-mappings. command: "ovs-vsctl set open . external_ids:ovn-bridge-mappings={{ ovn_bridge_mappings }}" - name: Get hostname shell: hostname -f register: hostname check_mode: no - name: Set host name command: "ovs-vsctl set Open_vSwitch . external-ids:hostname={{ hostname.stdout }}" # TODO ansible has an "iptables" module, but it does not allow you specify a "rule number" # which we require here. - name: Open Geneve UDP port for tunneling. command: iptables -I INPUT 10 -m state --state NEW -p udp --dport 6081 -j ACCEPT - name: Persist our iptables changes after a reboot shell: iptables-save > /etc/sysconfig/iptables.save # TODO Remove this once the metadata API is supported. # https://bugs.launchpad.net/networking-ovn/+bug/1562132 - name: Force config drive until the metadata API is supported. ini_file: dest: /etc/nova/nova.conf section: DEFAULT option: force_config_drive value: true - name: Restart nova-compute service to reflect force_config_drive value. systemd: name: openstack-nova-compute state: restarted enabled: yes - hosts: controller remote_user: "{{ remote_user }}" become: true tasks: - name: Ensure OVN packages are installed on the central OVN host. when: ovn_central is defined yum: name: openvswitch-ovn-central state: present # TODO Set up SSL for OVN databases # TODO ansible has an "iptables" module, but it does not allow you specify a "rule number" # which we require here. - name: Open OVN database ports. command: "iptables -I INPUT 10 -m state --state NEW -p tcp --dport {{ item }} -j ACCEPT" with_items: [ 6641, 6642 ] - name: Persist our iptables changes after a reboot shell: iptables-save > /etc/sysconfig/iptables.save # TODO Integrate HA support for the OVN control services. - name: Start ovn-northd and the OVN databases. when: ovn_central is defined systemd: name: ovn-northd state: started enabled: yes - name: Enable remote access to the northbound database. command: "ovn-nbctl set-connection ptcp:6641:{{ ovn_db_ip }}" when: ovn_central is defined - name: Enable remote access to the southbound database. command: "ovn-sbctl set-connection ptcp:6642:{{ ovn_db_ip }}" when: ovn_central is defined - name: Ensure the Neutron ML2 plugin is installed on neutron-api hosts. yum: name: python-networking-ovn state: present - name: Update Neutron configuration files ini_file: dest={{ item.dest }} section={{ item.section }} option={{ item.option }} value={{ item.value }} with_items: - { dest: '/etc/neutron/neutron.conf', section: 'DEFAULT', option: 'service_plugins', value: 'qos,networking_ovn.l3.l3_ovn.OVNL3RouterPlugin' } - { dest: '/etc/neutron/neutron.conf', section: 'DEFAULT', option: 'notification_drivers', value: 'ovn-qos' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ml2', option: 'mechanism_drivers', value: 'ovn' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ml2', option: 'type_drivers', value: 'geneve,vxlan,vlan,flat' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ml2', option: 'tenant_network_types', value: 'geneve' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ml2_type_geneve', option: 'vni_ranges', value: '1:65536' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ml2_type_geneve', option: 'max_header_size', value: '38' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'ovn_nb_connection', value: '"tcp:{{ ovn_db_ip }}:6641"' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'ovn_sb_connection', value: '"tcp:{{ ovn_db_ip }}:6642"' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'ovsdb_connection_timeout', value: '180' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'neutron_sync_mode', value: 'repair' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'ovn_l3_mode', value: 'true' } - { dest: '/etc/neutron/plugins/ml2/ml2_conf.ini', section: 'ovn', option: 'vif_type', value: 'ovs' } - name: Note that API downtime begins now. debug: msg: NEUTRON API DOWNTIME STARTING NOW FOR THIS HOST - name: Shut down neutron-server so that we can begin data sync to OVN. systemd: name: neutron-server state: stopped - hosts: controller remote_user: "{{ remote_user }}" become: true tasks: - name: Sync Neutron state to OVN. when: ovn_central is defined command: neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/ml2_conf.ini - hosts: overcloud remote_user: "{{ remote_user }}" become: true tasks: - name: Note that data plane imact starts now. debug: msg: DATA PLANE IMPACT BEGINS NOW. - name: Stop metadata, DHCP, L3 and openvswitch agent if needed. systemd: name={{ item.name }} state={{ item.state }} enabled=no with_items: - { name: 'neutron-metadata-agent', state: 'stopped' } - { name: 'neutron-dhcp-agent', state: 'stopped' } - { name: 'neutron-l3-agent', state: 'stopped' } - { name: 'neutron-openvswitch-agent', state: 'stopped' } - hosts: compute remote_user: "{{ remote_user }}" become: true tasks: - name: Note that data plane is being restored. debug: msg: DATA PLANE IS NOW BEING RESTORED. - name: Delete br-tun as it is no longer used. command: "ovs-vsctl del-br br-tun" - name: Reset OpenFlow protocol version before ovn-controller takes over. with_items: [ br-int, br-ex ] command: "ovs-vsctl set Bridge {{ item }} protocols=[]" ignore_errors: True - name: Start ovn-controller. systemd: name: ovn-controller state: started enabled: yes - hosts: controller remote_user: "{{ remote_user }}" become: true tasks: # TODO The sync util scheduling gateway routers depends on this patch: # https://review.openstack.org/#/c/427020/ # If the patch is not merged, this command is harmless, but the gateway # routers won't get scheduled until later when neutron-server starts. - name: Schedule gateway routers by running the sync util. when: ovn_central is defined command: neutron-ovn-db-sync-util --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/ml2_conf.ini - hosts: overcloud remote_user: "{{ remote_user }}" become: true tasks: # TODO Make this smarter so that it only deletes net namespaces that were # # created by neutron. In the simple case, this is fine, but will break # # once containers are in use on the overcloud. - name: Delete network namespaces. command: ip -all netns delete - hosts: controller remote_user: "{{ remote_user }}" become: true tasks: - name: Note that the Neutron API is coming back online. debug: msg: THE NEUTRON API IS NOW BEING RESTORED. - name: Start neutron-server. systemd: name: neutron-server state: started # TODO In our grenade script we had to restart rabbitmq. Is that needed? networking-ovn-4.0.0/requirements.txt0000666000175100017510000000100513245511164020012 0ustar zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. futurist>=1.2.0 # Apache-2.0 netaddr>=0.7.18 # BSD neutron-lib>=1.13.0 # Apache-2.0 oslo.config>=5.1.0 # Apache-2.0 ovs>=2.8.0 # Apache-2.0 ovsdbapp>=0.8.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 pyOpenSSL>=16.2.0 # Apache-2.0 tenacity>=3.2.1 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD six>=1.10.0 # MIT networking-ovn-4.0.0/setup.py0000666000175100017510000000200613245511145016241 0ustar zuulzuul00000000000000# 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. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools # In python < 2.7.4, a lazy loading of package `pbr` will break # setuptools if some other modules registered functions in `atexit`. # solution from: http://bugs.python.org/issue15881#msg170215 try: import multiprocessing # noqa except ImportError: pass setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) networking-ovn-4.0.0/.testr.conf0000666000175100017510000000061213245511145016616 0ustar zuulzuul00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ OS_LOG_CAPTURE=1 \ ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./networking_ovn/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list