Enforcing virtual routers, config/analytics/database nodes

Change-Id: I33f003e6fd9062a04a3c6f69621591ff5757a513
diff --git a/README.rst b/README.rst
index 574d476..4aae3e1 100644
--- a/README.rst
+++ b/README.rst
@@ -703,30 +703,138 @@
           pci: 0000:81:00.1
       ...
 
+Contrail client
+---------------
+
+Basic parameters with identity and host configs
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      identity:
+        user: admin
+        project: admin
+        password: adminpass
+        host: keystone_host
+      config:
+        host: contrail_api_host
+        port: contrail_api_ort
+
+Enforcing virtual routers
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      virtual_router:
+        cmp01:
+          ip_address: 172.16.0.11
+          dpdk_enabled: True
+        cmp02:
+          ip_address: 172.16.0.12
+          dpdk_enabled: True
+
+Enforcing control nodes
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      bgp_router:
+        ntw01:
+          type: control-node
+          ip_address: 172.16.0.11
+        nwt02:
+          type: control-node
+          ip_address: 172.16.0.12
+        nwt03:
+          type: control-node
+          ip_address: 172.16.0.13
+
+
+Enforcing edge BGP routers
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      bgp_router:
+        mx01:
+          type: router
+          ip_address: 172.16.0.21
+          asn: 64512
+        mx02:
+          type: router
+          ip_address: 172.16.0.22
+          asn: 64512
+
+Enforcing config nodes
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      config_node:
+        ctl01:
+          ip_address: 172.16.0.21
+        ctl02:
+          ip_address: 172.16.0.22
+
+Enforcing database nodes
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      database_node:
+        ntw01:
+          ip_address: 172.16.0.21
+        ntw02:
+          ip_address: 172.16.0.22
+
+Enforcing analytics nodes
+
+.. code-block:: bash
+
+  opencontrail:
+    client:
+      ...
+      analytics_node:
+        nal01:
+          ip_address: 172.16.0.31
+        nal02:
+          ip_address: 172.16.0.32
+
+
 Usage
 =====
 
 Basic installation
-==================
+------------------
 
 Add control BGP
-===============
+
+.. code-block:: bash
 
     python /etc/contrail/provision_control.py --api_server_ip 192.168.1.11 --api_server_port 8082 --host_name network1.contrail.domain.com --host_ip 192.168.1.11 --router_asn 64512
 
-Compute node installation
-=========================
+Install compute node
 
-.. code-block:: yaml
+.. code-block:: bash
 
     yum install contrail-vrouter contrail-openstack-vrouter
 
     salt-call state.sls nova,opencontrail
 
 Add virtual router
-==================
 
-.. code-block:: yaml
+.. code-block:: bash
 
     python /etc/contrail/provision_vrouter.py --host_name hostnode1.intra.domain.com --host_ip 10.0.100.101 --api_server_ip 10.0.100.30 --oper add --admin_user admin --admin_password cloudlab --admin_tenant_name admin
 
@@ -735,7 +843,7 @@
     reboot
 
 Service debugging
-=================
+-----------------
 
 Display vhost XMPP connection status
 
@@ -759,31 +867,6 @@
 
 	http://<compute-node>:8085/Snh_SandeshTraceRequest?x=XmppMessageTrace
 
-Documentation and Bugs
-============================
-
-To learn how to deploy OpenStack Salt, consult the documentation available
-online at:
-
-    https://wiki.openstack.org/wiki/OpenStackSalt
-
-In the unfortunate event that bugs are discovered, they should be reported to
-the appropriate bug tracker. If you obtained the software from a 3rd party
-operating system vendor, it is often wise to use their own bug tracker for
-reporting problems. In all other cases use the master OpenStack bug tracker,
-available at:
-
-    http://bugs.launchpad.net/openstack-salt
-
-Developers wishing to work on the OpenStack Salt project should always base
-their work on the latest formulas code, available from the master GIT
-repository at:
-
-    https://git.openstack.org/cgit/openstack/salt-formula-opencontrail
-
-Developers should also join the discussion on the IRC list, at:
-
-    https://wiki.openstack.org/wiki/Meetings/openstack-salt
 
 Documentation and Bugs
 ======================
diff --git a/_modules/contrail.py b/_modules/contrail.py
new file mode 100644
index 0000000..2cd1747
--- /dev/null
+++ b/_modules/contrail.py
@@ -0,0 +1,506 @@
+#!/usr/bin/python
+# Copyright 2017 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from netaddr import IPNetwork
+
+try:
+    from vnc_api import vnc_api
+    from vnc_api.gen.resource_client import VirtualRouter, AnalyticsNode, \
+        ConfigNode, DatabaseNode, BgpRouter
+    from vnc_api.gen.resource_xsd import AddressFamilies, BgpSessionAttributes, \
+        BgpSession, BgpPeeringAttributes, BgpRouterParams
+    HAS_CONTRAIL = True
+except ImportError:
+    HAS_CONTRAIL = False
+
+__opts__ = {}
+
+
+def __virtual__():
+    '''
+    Only load this module if vnc_api library is installed.
+    '''
+    if HAS_CONTRAIL:
+        return 'contrail'
+
+    return False
+
+
+def _auth(**kwargs):
+    '''
+    Set up Contrail API credentials.
+    '''
+    user = kwargs.get('user')
+    password = kwargs.get('password')
+    tenant_name = kwargs.get('project')
+    api_host = kwargs.get('api_server_ip')
+    api_port = kwargs.get('api_server_port')
+    api_base_url = kwargs.get('api_base_url')
+    use_ssl = False
+    auth_host = kwargs.get('auth_host_ip')
+    vnc_lib = vnc_api.VncApi(user, password, tenant_name,
+        api_host, api_port, api_base_url, wait_for_connect=True,
+        api_server_use_ssl=use_ssl, auth_host=auth_host)
+
+    return vnc_lib
+
+
+def _get_config(vnc_client, global_system_config = 'default-global-system-config'):
+    try:
+        gsc_obj = vnc_client.global_system_config_read(id=global_system_config)
+    except vnc_api.NoIdError:
+        gsc_obj = vnc_client.global_system_config_read(fq_name_str=global_system_config)
+    except:
+        gsc_obj = None
+
+    return gsc_obj
+
+
+def _get_rt_inst_obj(vnc_client):
+
+    # TODO pick fqname hardcode from common
+    rt_inst_obj = vnc_client.routing_instance_read(
+        fq_name=['default-domain', 'default-project',
+                 'ip-fabric', '__default__'])
+
+    return rt_inst_obj
+
+
+def _get_ip(ip_w_pfx):
+    return str(IPNetwork(ip_w_pfx).ip)
+
+
+
+def virtual_router_list(**kwargs):
+    '''
+    Return a list of all Contrail virtual routers
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.virtual_router_list
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    vrouter_objs = vnc_client._objects_list('virtual-router', detail=True)
+    for vrouter_obj in vrouter_objs:
+        ret[vrouter_obj.name] = {
+            'ip_address': vrouter_obj.virtual_router_ip_address,
+            'dpdk_enabled': vrouter_obj.virtual_router_dpdk_enabled
+        }
+    return ret
+
+
+def virtual_router_get(name, **kwargs):
+    '''
+    Return a specific Contrail virtual router
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.virtual_router_get cmp01
+    '''
+    ret = {}
+    vrouter_objs = virtual_router_list(**kwargs)
+    if name in vrouter_objs:
+        ret[name] = vrouter_objs.get(name)
+    if len(ret) == 0:
+        return {'Error': 'Error in retrieving virtual router.'}
+    return ret
+
+
+def virtual_router_create(name, ip_address, dpdk_enabled=False, **kwargs):
+    '''
+    Create specific Contrail virtual router
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.virtual_router_create cmp02 10.10.10.102
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    vrouter_objs = virtual_router_list(**kwargs)
+    if name in vrouter_objs:
+        return {'Error': 'Virtual router %s already exists' % name}
+    else:
+        vrouter_obj = VirtualRouter(
+            name, gsc_obj,
+            virtual_router_ip_address=ip_address)
+        vrouter_obj.set_virtual_router_dpdk_enabled(dpdk_enabled)
+        vnc_client.virtual_router_create(vrouter_obj)
+    ret = virtual_router_list(**kwargs)
+    return ret[name]
+
+
+def virtual_router_delete(name, **kwargs):
+    '''
+    Delete specific Contrail virtual router
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.virtual_router_delete cmp01
+    '''
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    vrouter_obj = VirtualRouter(name, gsc_obj)
+    vnc_client.virtual_router_delete(
+        fq_name=vrouter_obj.get_fq_name())
+
+
+def analytics_node_list(**kwargs):
+    '''
+    Return a list of all Contrail analytics nodes
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.analytics_node_list
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    node_objs = vnc_client._objects_list('analytics-node', detail=True)
+    for node_obj in node_objs:
+        ret[node_obj.name] = node_obj.__dict__
+    return ret
+
+
+def analytics_node_get(name, **kwargs):
+    '''
+    Return a specific Contrail analytics node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.analytics_node_get nal01
+    '''
+    ret = {}
+    vrouter_objs = analytics_node_list(**kwargs)
+    if name in vrouter_objs:
+        ret[name] = vrouter_objs.get(name)
+    if len(ret) == 0:
+        return {'Error': 'Error in retrieving analytics node.'}
+    return ret
+
+
+def analytics_node_create(name, ip_address, **kwargs):
+    '''
+    Create specific Contrail analytics node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.analytics_node_create ntw03 10.10.10.103
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    analytics_node_objs = analytics_node_list(**kwargs)
+    if name in analytics_node_objs:
+        return {'Error': 'Analytics node %s already exists' % name}
+    else:
+        analytics_node_obj = AnalyticsNode(
+            name, gsc_obj,
+            analytics_node_ip_address=ip_address)
+        vnc_client.analytics_node_create(analytics_node_obj)
+    ret = analytics_node_list(**kwargs)
+    return ret[name]
+
+
+def analytics_node_delete(name, **kwargs):
+    '''
+    Delete specific Contrail analytics node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.analytics_node_delete cmp01
+    '''
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    analytics_node_obj = AnalyticsNode(name, gsc_obj)
+    vnc_client.analytics_node_delete(
+        fq_name=analytics_node_obj.get_fq_name())
+
+
+def config_node_list(**kwargs):
+    '''
+    Return a list of all Contrail config nodes
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.config_node_list
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    node_objs = vnc_client._objects_list('config-node', detail=True)
+    for node_obj in node_objs:
+        ret[node_obj.name] = node_obj.__dict__
+    return ret
+
+
+def config_node_get(name, **kwargs):
+    '''
+    Return a specific Contrail config node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.config_node_get nal01
+    '''
+    ret = {}
+    vrouter_objs = config_node_list(**kwargs)
+    if name in vrouter_objs:
+        ret[name] = vrouter_objs.get(name)
+    if len(ret) == 0:
+        return {'Error': 'Error in retrieving config node.'}
+    return ret
+
+
+def config_node_create(name, ip_address, **kwargs):
+    '''
+    Create specific Contrail config node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.config_node_create ntw03 10.10.10.103
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    config_node_objs = config_node_list(**kwargs)
+    if name in config_node_objs:
+        return {'Error': 'Config node %s already exists' % name}
+    else:
+        config_node_obj = ConfigNode(
+            name, gsc_obj,
+            config_node_ip_address=ip_address)
+        vnc_client.config_node_create(config_node_obj)
+    ret = config_node_list(**kwargs)
+    return ret[name]
+
+
+def config_node_delete(name, **kwargs):
+    '''
+    Delete specific Contrail config node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.config_node_delete cmp01
+    '''
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    config_node_obj = ConfigNode(name, gsc_obj)
+    vnc_client.config_node_delete(
+        fq_name=config_node_obj.get_fq_name())
+
+
+def bgp_router_list(**kwargs):
+    '''
+    Return a list of all Contrail BGP routers
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.bgp_router_list
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    bgp_router_objs = vnc_client._objects_list('bgp-router', detail=True)
+    for bgp_router_obj in bgp_router_objs:
+        ret[bgp_router_obj.name] = bgp_router_obj.__dict__
+    return ret
+
+
+def bgp_router_get(name, **kwargs):
+    '''
+    Return a specific Contrail BGP router
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.bgp_router_get nal01
+    '''
+    ret = {}
+    bgp_router_objs = bgp_router_list(**kwargs)
+    if name in bgp_router_objs:
+        ret[name] = bgp_router_objs.get(name)
+    if len(ret) == 0:
+        return {'Error': 'Error in retrieving BGP router.'}
+    return ret
+
+
+def bgp_router_create(name, type, ip_address, asn=64512, **kwargs):
+    '''
+    Create specific Contrail control node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.bgp_router_create ntw03 control-node 10.10.10.103
+        salt '*' contrail.bgp_router_create mx01 router 10.10.10.105
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+
+    bgp_router_objs = bgp_router_list(**kwargs)
+    if name in bgp_router_objs:
+        return {'Error': 'control node %s already exists' % name}
+    else:
+        address_families = ['route-target', 'inet-vpn', 'e-vpn', 'erm-vpn',
+                            'inet6-vpn']
+        if type != 'control-node':
+            address_families.remove('erm-vpn')
+
+        bgp_addr_fams = AddressFamilies(address_families)
+        bgp_sess_attrs = [
+            BgpSessionAttributes(address_families=bgp_addr_fams)]
+        bgp_sessions = [BgpSession(attributes=bgp_sess_attrs)]
+        bgp_peering_attrs = BgpPeeringAttributes(session=bgp_sessions)
+        rt_inst_obj = _get_rt_inst_obj(vnc_client)
+
+        if type == 'control-node':
+            vendor = 'contrail'
+        elif type == 'router':
+            vendor = 'mx'
+        else:
+            vendor = 'unknown'
+
+        router_params = BgpRouterParams(router_type=type,
+            vendor=vendor, autonomous_system=int(asn),
+            identifier=_get_ip(ip_address),
+            address=_get_ip(ip_address),
+            port=179, address_families=bgp_addr_fams)
+        bgp_router_obj = BgpRouter(name, rt_inst_obj,
+            bgp_router_parameters=router_params)
+        vnc_client.bgp_router_create(bgp_router_obj)
+    ret = bgp_router_list(**kwargs)
+    return ret[name]
+
+
+def bgp_router_delete(name, **kwargs):
+    '''
+    Delete specific Contrail control node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.bgp_router_delete mx01
+    '''
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_control(vnc_client)
+    bgp_router_obj = BgpRouter(name, gsc_obj)
+    vnc_client.bgp_router_delete(
+        fq_name=bgp_router_obj.get_fq_name())
+
+
+def database_node_list(**kwargs):
+    '''
+    Return a list of all Contrail database nodes
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.database_node_list
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    node_objs = vnc_client._objects_list('database-node', detail=True)
+    for node_obj in node_objs:
+        ret[node_obj.name] = node_obj.__dict__
+    return ret
+
+
+def database_node_get(name, **kwargs):
+    '''
+    Return a specific Contrail database node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.database_node_get nal01
+    '''
+    ret = {}
+    vrouter_objs = database_node_list(**kwargs)
+    if name in vrouter_objs:
+        ret[name] = vrouter_objs.get(name)
+    if len(ret) == 0:
+        return {'Error': 'Error in retrieving database node.'}
+    return ret
+
+
+def database_node_create(name, ip_address, **kwargs):
+    '''
+    Create specific Contrail database node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.database_node_create ntw03 10.10.10.103
+    '''
+    ret = {}
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_config(vnc_client)
+    database_node_objs = database_node_list(**kwargs)
+    if name in database_node_objs:
+        return {'Error': 'Database node %s already exists' % name}
+    else:
+        database_node_obj = DatabaseNode(
+            name, gsc_obj,
+            database_node_ip_address=ip_address)
+        vnc_client.database_node_create(database_node_obj)
+    ret = database_node_list(**kwargs)
+    return ret[name]
+
+
+def database_node_delete(name, **kwargs):
+    '''
+    Delete specific Contrail database node
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' contrail.database_node_delete cmp01
+    '''
+    vnc_client = _auth(**kwargs)
+    gsc_obj = _get_database(vnc_client)
+    database_node_obj = databaseNode(name, gsc_obj)
+    vnc_client.database_node_delete(
+        fq_name=database_node_obj.get_fq_name())
diff --git a/_states/contrail.py b/_states/contrail.py
new file mode 100644
index 0000000..f117667
--- /dev/null
+++ b/_states/contrail.py
@@ -0,0 +1,202 @@
+#!/usr/bin/python
+# Copyright 2017 Mirantis, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+'''
+Management of Contrail resources
+================================
+
+:depends:   - vnc_api Python module
+
+
+Enforce the virtual router existence
+------------------------------------
+
+.. code-block:: yaml
+
+    virtual_router:
+      contrail.virtual_router_present:
+        name: cmp01
+        ip_address: 10.0.0.23
+        dpdk_enabled: False
+
+
+Enforce the virtual router absence
+----------------------------------
+
+.. code-block:: yaml
+
+    virtual_router_cmp01:
+      contrail.virtual_router_absent:
+        name: cmp01
+
+
+Enforce the analytics node existence
+------------------------------------
+
+.. code-block:: yaml
+
+    analytics_node01:
+      contrail.analytics_node_present:
+        name: nal01
+        ip_address: 10.0.0.13
+
+
+Enforce the config node existence
+---------------------------------
+
+.. code-block:: yaml
+
+    config_node01:
+      contrail.config_node_present:
+        name: ntw01
+        ip_address: 10.0.0.23
+
+
+Enforce the database node existence
+-----------------------------------
+
+.. code-block:: yaml
+
+    config_node01:
+      contrail.database_node_present:
+        name: ntw01
+        ip_address: 10.0.0.33
+
+'''
+
+def __virtual__():
+    '''
+    Load Contrail module
+    '''
+    return 'contrail'
+
+
+def virtual_router_present(name, ip_address, dpdk_enabled=False, **kwargs):
+    '''
+    Ensures that the Contrail virtual router exists.
+
+    :param name:        Virtual router name
+    :param ip_address:  Virtual router IP address
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Virtual router "{0}" already exists'.format(name)}
+    virtual_router = __salt__['contrail.virtual_router_get'](name, **kwargs)
+    if 'Error' not in virtual_router:
+        pass
+    else:
+        __salt__['contrail.virtual_router_create'](name, ip_address, dpdk_enabled, **kwargs)
+        ret['comment'] = 'Virtual router {0} has been created'.format(name)
+        ret['changes']['VirtualRouter'] = 'Created'
+    return ret
+
+
+def virtual_router_absent(name, **kwargs):
+    '''
+    Ensure that the Contrail virtual router doesn't exist
+
+    :param name: The name of the virtual router that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Virtual router "{0}" is already absent'.format(name)}
+    virtual_router = __salt__['contrail.virtual_router_get'](name, **kwargs)
+    if 'Error' not in virtual_router:
+        __salt__['contrail.virtual_router_delete'](name, **kwargs)
+        ret['comment'] = 'Virtual router {0} has been deleted'.format(name)
+        ret['changes']['VirtualRouter'] = 'Deleted'
+
+    return ret
+
+
+def analytics_node_present(name, ip_address, **kwargs):
+    '''
+    Ensures that the Contrail analytics node exists.
+
+    :param name:        Analytics node name
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Analytics node {0} already exists'.format(name)}
+    analytics_node = __salt__['contrail.analytics_node_get'](name, **kwargs)
+    if 'Error' not in analytics_node:
+        pass
+    else:
+        __salt__['contrail.analytics_node_create'](name, ip_address, **kwargs)
+        ret['comment'] = 'Analytics node {0} has been created'.format(name)
+        ret['changes']['AnalyticsNode'] = 'Created'
+    return ret
+
+
+def config_node_present(name, ip_address, **kwargs):
+    '''
+    Ensures that the Contrail config node exists.
+
+    :param name:        Config node name
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Config node {0} already exists'.format(name)}
+    config_node = __salt__['contrail.config_node_get'](name, **kwargs)
+    if 'Error' not in config_node:
+        pass
+    else:
+        __salt__['contrail.config_node_create'](name, ip_address, **kwargs)
+        ret['comment'] = 'Config node {0} has been created'.format(name)
+        ret['changes']['ConfigNode'] = 'Created'
+    return ret
+
+
+def bgp_router_present(name, type, ip_address, asn=64512, **kwargs):
+    '''
+    Ensures that the Contrail BGP router exists.
+
+    :param name:        BGP router name
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'BGP router {0} already exists'.format(name)}
+    bgp_router = __salt__['contrail.bgp_router_get'](name, **kwargs)
+    if 'Error' not in bgp_router:
+        pass
+    else:
+        __salt__['contrail.bgp_router_create'](name, type, ip_address, asn, **kwargs)
+        ret['comment'] = 'BGP router {0} has been created'.format(name)
+        ret['changes']['BgpRouter'] = 'Created'
+    return ret
+
+
+def database_node_present(name, ip_address, **kwargs):
+    '''
+    Ensures that the Contrail database node exists.
+
+    :param name:        Database node name
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Database node {0} already exists'.format(name)}
+    database_node = __salt__['contrail.database_node_get'](name, **kwargs)
+    if 'Error' not in database_node:
+        pass
+    else:
+        __salt__['contrail.database_node_create'](name, ip_address, **kwargs)
+        ret['comment'] = 'Database node {0} has been created'.format(name)
+        ret['changes']['DatabaseNode'] = 'Created'
+    return ret
diff --git a/metadata/service/client/cluster.yml b/metadata/service/client/cluster.yml
index 169367b..fabf22c 100644
--- a/metadata/service/client/cluster.yml
+++ b/metadata/service/client/cluster.yml
@@ -2,18 +2,19 @@
 - opencontrail
 parameters:
   _param:
-    opencontrail_version: 2.2
+    opencontrail_version: 3.0
   opencontrail:
     client:
+      enabled: True
       version: ${_param:opencontrail_version}
       identity:
         engine: keystone
-        host: ${_param:cluster_vip_address}
+        host: ${_param:openstack_control_address}
         port: 35357
         tenant: admin
         user: admin
         password: ${_param:keystone_admin_password}
       api:
         engine: contrail
-        host: ${_param:cluster_vip_address}
+        host: ${_param:opencontrail_control_address}
         port: 8082
diff --git a/metadata/service/client/single.yml b/metadata/service/client/single.yml
index fb9abd7..67038c2 100644
--- a/metadata/service/client/single.yml
+++ b/metadata/service/client/single.yml
@@ -2,9 +2,10 @@
 - opencontrail
 parameters:
   _param:
-    opencontrail_version: 2.2
+    opencontrail_version: 3.0
   opencontrail:
     client:
+      enabled: True
       version: ${_param:opencontrail_version}
       identity:
         engine: keystone
diff --git a/opencontrail/client.sls b/opencontrail/client.sls
index bece6f1..2b4334d 100644
--- a/opencontrail/client.sls
+++ b/opencontrail/client.sls
@@ -14,4 +14,87 @@
     - pkg: opencontrail_client_packages
 {%- endif %}
 
-{%- endif %}
\ No newline at end of file
+{%- for virtual_router_name, virtual_router in client.get('virtual_router', {}).items() %}
+
+opencontrail_client_virtual_router_{{ virtual_router_name }}:
+  contrail.virtual_router_present:
+  - name: {{ virtual_router.get('name', virtual_router_name) }}
+  - ip_address: {{ virtual_router.ip_address }}
+  - dpdk_enabled: {{ virtual_router.get('dpdk_enabled', False) }}
+  - user: {{ client.identity.user }}
+  - password: {{ client.identity.password }}
+  - project: {{ client.identity.tenant }}
+  - auth_host_ip: {{ client.identity.host }}
+  - api_server_ip: {{ client.api.host }}
+  - api_server_port: {{ client.api.port }}
+  - api_base_url: '/'
+
+{%- endfor %}
+
+{%- for config_node_name, config_node in client.get('config_node', {}).items() %}
+
+opencontrail_client_config_node_{{ config_node_name }}:
+  contrail.config_node_present:
+  - name: {{ config_node.get('name', config_node_name) }}
+  - ip_address: {{ config_node.ip_address }}
+  - user: {{ client.identity.user }}
+  - password: {{ client.identity.password }}
+  - project: {{ client.identity.tenant }}
+  - auth_host_ip: {{ client.identity.host }}
+  - api_server_ip: {{ client.api.host }}
+  - api_server_port: {{ client.api.port }}
+  - api_base_url: '/'
+
+{%- endfor %}
+
+{%- for bgp_router_name, bgp_router in client.get('bgp_router', {}).items() %}
+
+opencontrail_client_bgp_router_{{ bgp_router_name }}:
+  contrail.bgp_router_present:
+  - name: {{ bgp_router.get('name', bgp_router_name) }}
+  - ip_address: {{ bgp_router.ip_address }}
+  - type: {{ bgp_router.type }}
+  - asn: {{ bgp_router.get('asn', 64512) }}
+  - user: {{ client.identity.user }}
+  - password: {{ client.identity.password }}
+  - project: {{ client.identity.tenant }}
+  - auth_host_ip: {{ client.identity.host }}
+  - api_server_ip: {{ client.api.host }}
+  - api_server_port: {{ client.api.port }}
+  - api_base_url: '/'
+
+{%- endfor %}
+
+{%- for analytics_node_name, analytics_node in client.get('analytics_node', {}).items() %}
+
+opencontrail_client_analytics_node_{{ analytics_node_name }}:
+  contrail.analytics_node_present:
+  - name: {{ analytics_node.get('name', analytics_node_name) }}
+  - ip_address: {{ analytics_node.ip_address }}
+  - user: {{ client.identity.user }}
+  - password: {{ client.identity.password }}
+  - project: {{ client.identity.tenant }}
+  - auth_host_ip: {{ client.identity.host }}
+  - api_server_ip: {{ client.api.host }}
+  - api_server_port: {{ client.api.port }}
+  - api_base_url: '/'
+
+{%- endfor %}
+
+{%- for database_node_name, database_node in client.get('database_node', {}).items() %}
+
+opencontrail_client_database_node_{{ database_node_name }}:
+  contrail.database_node_present:
+  - name: {{ database_node.get('name', database_node_name) }}
+  - ip_address: {{ database_node.ip_address }}
+  - user: {{ client.identity.user }}
+  - password: {{ client.identity.password }}
+  - project: {{ client.identity.tenant }}
+  - auth_host_ip: {{ client.identity.host }}
+  - api_server_ip: {{ client.api.host }}
+  - api_server_port: {{ client.api.port }}
+  - api_base_url: '/'
+
+{%- endfor %}
+
+{%- endif %}