summaryrefslogtreecommitdiff
path: root/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/impl_idl_ovn.py
blob: be5e7db3673e6c37756e6a668ae0dad544134d19 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    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 functools
import socket
import uuid

from neutron_lib import exceptions as n_exc
from neutron_lib.utils import helpers
from oslo_log import log
from oslo_utils import uuidutils
from ovs import socket_util
from ovs import stream
from ovsdbapp.backend import ovs_idl
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
import tenacity

from neutron._i18n import _
from neutron.common.ovn import constants as ovn_const
from neutron.common.ovn import exceptions as ovn_exc
from neutron.common.ovn import utils
from neutron.common import utils as n_utils
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf as cfg
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import commands as cmd
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import ovsdb_monitor
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import worker
from neutron.services.portforwarding import constants as pf_const


LOG = log.getLogger(__name__)


# Override wait_for_change to not use a timeout so we always try to reconnect
def wait_for_change(idl_, timeout, seqno=None):
    if seqno is None:
        seqno = idl_.change_seqno
    while idl_.change_seqno == seqno and not idl_.run():
        poller = idlutils.poller.Poller()
        idl_.wait(poller)
        poller.block()


idlutils.wait_for_change = wait_for_change


class OvnNbTransaction(idl_trans.Transaction):

    def __init__(self, *args, **kwargs):
        # NOTE(lucasagomes): The bump_nb_cfg parameter is only used by
        # the agents health status check
        self.bump_nb_cfg = kwargs.pop('bump_nb_cfg', False)
        super(OvnNbTransaction, self).__init__(*args, **kwargs)

    def pre_commit(self, txn):
        if not self.bump_nb_cfg:
            return
        self.api.nb_global.increment('nb_cfg')


def add_keepalives(fn):
    @functools.wraps(fn)
    def _open(*args, **kwargs):
        error, sock = fn(*args, **kwargs)
        try:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        except socket.error as e:
            sock.close()
            return socket_util.get_exception_errno(e), None
        return error, sock
    return _open


class NoProbesMixin:
    @staticmethod
    def needs_probes():
        # If we are using keepalives, we can force probe_interval=0
        return False


class TCPStream(stream.TCPStream, NoProbesMixin):
    @classmethod
    @add_keepalives
    def _open(cls, suffix, dscp):
        return super()._open(suffix, dscp)


class SSLStream(stream.SSLStream, NoProbesMixin):
    @classmethod
    @add_keepalives
    def _open(cls, suffix, dscp):
        return super()._open(suffix, dscp)


# Overwriting globals in a library is clearly a good idea
stream.Stream.register_method("tcp", TCPStream)
stream.Stream.register_method("ssl", SSLStream)


# 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 = {}
    ovsdb_connection = None

    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

    @n_utils.classproperty
    def connection_string(cls):
        raise NotImplementedError()

    @n_utils.classproperty
    def schema_helper(cls):
        # SchemaHelper.get_idl_schema() sets schema_json to None which is
        # called in Idl.__init__(), so if we've done that return new helper
        try:
            if cls._schema_helper.schema_json:
                return cls._schema_helper
        except AttributeError:
            pass

        ovsdb_monitor._check_and_set_ssl_files(cls.schema)
        cls._schema_helper = idlutils.get_schema_helper(cls.connection_string,
                                                        cls.schema)
        return cls._schema_helper

    @classmethod
    def schema_has_table(cls, table_name):
        return table_name in cls.schema_helper.schema_json['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 is_col_supports_value(self, table_name, col_name, value):
        if not self.is_col_present(table_name, col_name):
            return False
        enum = self._tables[table_name].columns[col_name].type.key.enum
        if not enum:
            return False
        return value in {k.value for k in enum.values}

    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)

    # 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)


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 'max_interval' seconds, then interval will be fixed
# to 'max_interval' seconds afterwards. The default 'max_interval' is 180.
def get_ovn_idls(driver, trigger):
    @tenacity.retry(
        wait=tenacity.wait_exponential(
            max=cfg.get_ovn_ovsdb_retry_max_interval()),
        reraise=True)
    def get_ovn_idl_retry(api_cls):
        trigger_class = utils.get_method_class(trigger)
        LOG.info('Getting %(cls)s for %(trigger)s with retry',
                 {'cls': api_cls.__name__, 'trigger': trigger_class.__name__})
        return api_cls.from_worker(trigger_class, 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))


class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
    def __init__(self, connection):
        super(OvsdbNbOvnIdl, self).__init__(connection)

    @n_utils.classproperty
    def connection_string(cls):
        return cfg.get_ovn_nb_connection()

    @classmethod
    def from_worker(cls, worker_class, driver=None):
        args = (cls.connection_string, cls.schema_helper)
        if worker_class == worker.MaintenanceWorker:
            idl_ = ovsdb_monitor.BaseOvnIdl.from_server(*args)
        else:
            idl_ = ovsdb_monitor.OvnNbIdl.from_server(*args, driver=driver)
        conn = connection.Connection(idl_, timeout=cfg.get_ovn_ovsdb_timeout())
        return cls(conn)

    @property
    def nb_global(self):
        return next(iter(self.db_list_rows('NB_Global').execute(
            check_error=True)))

    def create_transaction(self, check_error=False, log_errors=True,
                           bump_nb_cfg=False):
        return OvnNbTransaction(
            self, self.ovsdb_connection, self.ovsdb_connection.timeout,
            check_error, log_errors, bump_nb_cfg=bump_nb_cfg)

    @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.
        """
        revision_mismatch_raise = kwargs.pop('revision_mismatch_raise', False)
        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)
            if revision_mismatch_raise:
                raise e

    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, external_ids_update=None,
                         if_exists=True, **columns):
        return cmd.SetLSwitchPortCommand(self, lport_name, external_ids_update,
                                         if_exists, **columns)

    def update_lswitch_qos_options(self, port, if_exists=True, **qos):
        return cmd.UpdateLSwitchPortQosOptionsCommand(self, port, if_exists,
                                                      **qos)

    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_stateless_fip_nats(self):
        cmd = self.db_find(
            'NAT',
            ('external_ids', '!=', {ovn_const.OVN_FIP_EXT_ID_KEY: ''}),
            ('options', '=', {'stateless': 'true'}),
            ('type', '=', 'dnat_and_snat'))
        return cmd.execute(check_error=True)

    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_ports = []
            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_ports.append(lport.name)
            result.append({'name': lswitch.name,
                           'ports': ports,
                           'provnet_ports': provnet_ports})
        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.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,
            lsp_address=ovn_const.DEFAULT_ADDR_FOR_LSP_WITH_PEER):
        return cmd.SetLRouterPortInLSwitchPortCommand(self, lswitch_port,
                                                      lrouter_port, is_gw_port,
                                                      if_exists,
                                                      lsp_address)

    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 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 delete_address_set(self, name, if_exists=True, **columns):
        return cmd.DelAddrSetCommand(self, name, if_exists)

    def _get_logical_router_port_gateway_chassis(self, lrp):
        """Get the list of chassis hosting this gateway port.

        @param   lrp: logical router port
        @type    lrp: Logical_Router_Port row
        @return: List of tuples (chassis_name, priority) sorted by priority
        """
        # 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:
                chassis.append((gwc.chassis_name, gwc.priority))
        else:
            rc = lrp.options.get(ovn_const.OVN_GATEWAY_CHASSIS_KEY)
            if rc:
                chassis.append((rc, 0))
        # make sure that chassis are sorted by priority
        return sorted(chassis, reverse=True, key=lambda x: x[1])

    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, prio 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, prio))
        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)
            chassis_list = self._get_logical_router_port_gateway_chassis(lrp)
            return [chassis for chassis, prio in chassis_list]
        except idlutils.RowNotFound:
            return []

    def get_gateway_chassis_az_hints(self, gateway_name):
        lrp = self.lookup('Logical_Router_Port', gateway_name,
                          default=None)
        if not lrp:
            return []
        router_id = lrp.external_ids.get(
            ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY, "")
        lrouter = self.lookup('Logical_Router', utils.ovn_name(router_id),
                              default=None)
        if not lrouter:
            return []
        az_string = lrouter.external_ids.get(
            ovn_const.OVN_AZ_HINTS_EXT_ID_KEY, "")
        if not az_string:
            return []
        return az_string.split(",")

    def get_chassis_gateways(self, chassis_name):
        gw_chassis = self.db_find_rows(
            'Gateway_Chassis', ('chassis_name', '=', chassis_name))
        return gw_chassis.execute(check_error=True)

    def get_unhosted_gateways(self, port_physnet_dict, chassis_with_physnets,
                              all_gw_chassis, chassis_with_azs):
        unhosted_gateways = set()
        for port, physnet in port_physnet_dict.items():
            lrp_name = '%s%s' % (ovn_const.LRP_PREFIX, port)
            original_state = self.get_gateway_chassis_binding(lrp_name)
            az_hints = self.get_gateway_chassis_az_hints(lrp_name)
            # Filter out chassis that lost physnet, the cms option,
            # or has been deleted.
            actual_gw_chassis = [
                chassis for chassis in original_state
                if not utils.is_gateway_chassis_invalid(
                    chassis, all_gw_chassis, physnet, chassis_with_physnets,
                    az_hints, chassis_with_azs)]

            # Check if gw ports are fully scheduled.
            if len(actual_gw_chassis) >= ovn_const.MAX_GW_CHASSIS:
                continue

            # If there are no gateways with 'enable-chassis-as-gw' cms option
            # then try to schedule on all gateways with physnets connected,
            # and filter required physnet.
            available_chassis = {
                c for c in all_gw_chassis or chassis_with_physnets.keys()
                if not utils.is_gateway_chassis_invalid(
                    c, all_gw_chassis, physnet, chassis_with_physnets,
                    az_hints, chassis_with_azs)}

            if available_chassis == set(original_state):
                # The same situation as was before. Nothing
                # to be rescheduled.
                continue
            if not available_chassis:
                # There is no chassis that could host
                # this gateway.
                continue
            unhosted_gateways.add(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 = {}
        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():
            if not (ovn_const.OVN_SG_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 = 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 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)

    def get_router_floatingip_lbs(self, lrouter_name):
        rc = self.db_find_rows('Load_Balancer', (
            'external_ids', '=',
            {ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY:
             pf_const.PORT_FORWARDING_PLUGIN,
             ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: lrouter_name}))
        return [ovn_obj for ovn_obj in rc.execute(check_error=True)
                if ovn_const.OVN_FIP_EXT_ID_KEY in ovn_obj.external_ids]

    def get_floatingip_in_nat_or_lb(self, fip_id):
        fip = self.get_floatingip(fip_id)
        if fip:
            return fip
        result = self.db_find('Load_Balancer', (
            'external_ids', '=',
            {ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY:
             pf_const.PORT_FORWARDING_PLUGIN,
             ovn_const.OVN_FIP_EXT_ID_KEY: fip_id})).execute(check_error=True)
        return result[0] if result else None

    def get_floatingip(self, fip_id):
        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 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)
        try:
            return self.lr_get(lrouter_name).execute(log_errors=False,
                                                     check_error=True)
        except idlutils.RowNotFound:
            return None

    def get_lrouter_port(self, lrp_name):
        # TODO(mangelajo): Implement lrp_get() ovsdbapp and use from here
        if uuidutils.is_uuid_like(lrp_name):
            lrp_name = utils.ovn_lrouter_port_name(lrp_name)
        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)

    def get_port_group(self, pg_name):
        if uuidutils.is_uuid_like(pg_name):
            pg_name = utils.ovn_port_group_name(pg_name)
        return self.lookup('Port_Group', pg_name, default=None)

    def get_sg_port_groups(self):
        """Returns OVN port groups used as Neutron Security Groups.

        This method will return all port group entries in OVN that map to
        a Security Group. Even though neutron_pg_drop is used to assist on
        SGs, it will also not be returned.
        """
        port_groups = {}
        for row in self._tables['Port_Group'].rows.values():
            name = getattr(row, 'name')
            if (ovn_const.OVN_SG_EXT_ID_KEY not in row.external_ids or
                    name == ovn_const.OVN_DROP_PORT_GROUP_NAME):
                continue
            data = {}
            for row_key in getattr(row, "_data", {}):
                data[row_key] = getattr(row, row_key)
            port_groups[name] = data
        return port_groups

    def check_liveness(self):
        return cmd.CheckLivenessCommand(self)

    def set_lswitch_port_to_virtual_type(self, lport_name, vip,
                                         virtual_parent, if_exists=True):
        return cmd.SetLSwitchPortToVirtualTypeCommand(
            self, lport_name, vip, virtual_parent, if_exists)

    def unset_lswitch_port_to_virtual_type(self, lport_name,
                                           virtual_parent, if_exists=True):
        return cmd.UnsetLSwitchPortToVirtualTypeCommand(
            self, lport_name, virtual_parent, if_exists)

    def update_lb_external_ids(self, lb_name, values, if_exists=True):
        return cmd.UpdateLbExternalIds(self, lb_name, values, if_exists)


class OvsdbSbOvnIdl(sb_impl_idl.OvnSbApiIdlImpl, Backend):
    def __init__(self, connection):
        super(OvsdbSbOvnIdl, self).__init__(connection)

    @n_utils.classproperty
    def connection_string(cls):
        return cfg.get_ovn_sb_connection()

    @classmethod
    def from_worker(cls, worker_class, driver=None):
        args = (cls.connection_string, cls.schema_helper)
        if worker_class == worker.MaintenanceWorker:
            idl_ = ovsdb_monitor.BaseOvnSbIdl.from_server(*args)
        else:
            idl_ = ovsdb_monitor.OvnSbIdl.from_server(*args, driver=driver)
        conn = connection.Connection(idl_, timeout=cfg.get_ovn_ovsdb_timeout())
        return cls(conn)

    def _get_chassis_physnets(self, chassis):
        other_config = utils.get_ovn_chassis_other_config(chassis)
        bridge_mappings = other_config.get('ovn-bridge-mappings', '')
        mapping_dict = helpers.parse_mappings(bridge_mappings.split(','))
        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_gateway_chassis_from_cms_options(self, name_only=True):
        return [ch.name if name_only else ch
                for ch in self.chassis_list().execute(check_error=True)
                if ovn_const.CMS_OPT_CHASSIS_AS_GW in
                utils.get_ovn_chassis_other_config(ch).get(
                    ovn_const.OVN_CMS_OPTIONS, '').split(',')]

    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_chassis_and_azs(self):
        chassis_azs = {}
        for ch in self.chassis_list().execute(check_error=True):
            chassis_azs[ch.name] = utils.get_chassis_availability_zones(ch)
        return chassis_azs

    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_by_card_serial_from_cms_options(self,
                                                    card_serial_number):
        for ch in self.chassis_list().execute(check_error=True):
            if ('{}={}'
                    .format(ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
                            card_serial_number)
                    in utils.get_ovn_chassis_other_config(ch).get(
                        ovn_const.OVN_CMS_OPTIONS, '').split(',')):
                return ch
        msg = _('Chassis with %s %s %s does not exist'
                ) % (ovn_const.OVN_CMS_OPTIONS,
                     ovn_const.CMS_OPT_CARD_SERIAL_NUMBER,
                     card_serial_number)
        raise RuntimeError(msg)

    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', '=', ovn_const.LSP_TYPE_LOCALPORT))
        return next(iter(cmd.execute(check_error=True)), None)

    def set_chassis_neutron_description(self, chassis, description,
                                        agent_type):
        desc_key = (ovn_const.OVN_AGENT_METADATA_DESC_KEY
                    if agent_type == ovn_const.OVN_METADATA_AGENT else
                    ovn_const.OVN_AGENT_DESC_KEY)
        return cmd.UpdateChassisExtIdsCommand(
            self, chassis, {desc_key: description}, if_exists=False)

    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

        def check_net_and_ip(port):
            # If the port is not bound to any chassis it is not relevant
            if not port.chassis:
                return False

            is_in_network = utils.get_network_name_from_datapath(
                port.datapath) == network
            return (port.mac and
                    is_in_network and
                    (ip_address in port.mac[0].split(' ')))

        return [r for r in rows if check_net_and_ip(r)]

    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})

    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]