Added modules&states, Extended formula

Change-Id: Ifcf2509e8c21853ed86d5852c3595888c5316edd
diff --git a/_modules/avinetworks.py b/_modules/avinetworks.py
new file mode 100644
index 0000000..e01edd6
--- /dev/null
+++ b/_modules/avinetworks.py
@@ -0,0 +1,385 @@
+#!/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.
+
+# todo:
+# CRUD VirtualService
+# CRUD Pool
+# CRUD
+
+import requests
+import os
+import re
+import json
+
+__opts__ = {}
+
+def _auth(**kwargs):
+    '''
+    Set up Contrail API credentials.
+    '''
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    username = __pillar__['avinetworks']['api']['user']
+    password = __pillar__['avinetworks']['api']['password']
+    login = requests.post('https://' + cluster_ip + '/login',
+                          verify=False,
+                          data={'username': username, 'password': password})
+    return login
+
+
+def _get_input_type(_input):
+    test = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
+    result = test.match(_input)
+    if result:
+        return "V4"
+    return "DNS"
+
+
+def logout(login):
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    requests.post('https://' + cluster_ip + '/logout',
+                  verify=False,
+                  headers={'X-CSRFToken': login.cookies['csrftoken'],
+                           'Referer': 'https://' + cluster_ip},
+                  cookies=login.cookies)
+
+
+def send_request_get(_command):
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    login = _auth()
+    url = "https://" + os.path.join(cluster_ip, "api", _command)
+    try:
+        resp = requests.get(url,
+                            verify=False,
+                            cookies=dict(sessionid=login.cookies['sessionid']))
+    except requests.exceptions.RequestException as ex:
+        print ex
+        return None
+    logout(login)
+    return resp
+
+
+def send_request_post(_command, data):
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    login = _auth()
+    url = "https://" + os.path.join(cluster_ip, "api", _command)
+    try:
+        resp = requests.post(url,
+                             verify=False,
+                             headers={'X-CSRFToken': login.cookies['csrftoken'],
+                                      'Referer': 'https://' + cluster_ip,
+                                      'Content-Type': 'application/json'},
+                             cookies=login.cookies,
+                             data=json.dumps(data))
+    except requests.exceptions.RequestException as ex:
+        print ex
+        return None
+    logout(login)
+    return resp
+
+
+def send_request_put(_command, data):
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    login = _auth()
+    url = "https://" + os.path.join(cluster_ip, "api", _command)
+    try:
+        resp = requests.put(url,
+                            verify=False,
+                            headers={'X-CSRFToken': login.cookies['csrftoken'],
+                                     'Referer': 'https://' + cluster_ip,
+                                     'Content-Type': 'application/json'},
+                            cookies=login.cookies,
+                            data=json.dumps(data))
+    except requests.exceptions.RequestException as ex:
+        print ex
+        return None
+    logout(login)
+    return resp
+
+
+def send_request_delete(_command, uuid, **kwargs):
+    cluster_ip = __pillar__['avinetworks']['api']['ip']
+    login = _auth()
+    url = "https://" + os.path.join(cluster_ip, "api", _command, uuid)
+    try:
+        resp = requests.delete(url,
+                               verify=False,
+                               headers={'X-CSRFToken': login.cookies['csrftoken'],
+                                        'Referer': 'https://' + cluster_ip,
+                                        'Content-Type': 'application/json'},
+                               cookies=login.cookies)
+    except requests.exceptions.RequestException as ex:
+        print ex
+        return None
+    logout(login)
+    return resp
+
+
+def pool_list(**kwargs):
+    command = "pool"
+    ret = send_request_get(command)
+    return ret.json()
+
+
+def pool_get(name, **kwargs):
+    pools = pool_list()
+    for pool in pools['results']:
+        if name == pool['name'] or name == pool['uuid']:
+            return pool
+    return {'result': False,
+            'Error': "Error in the retrieving pool."}
+
+
+def pool_create(name, lb_algorithm='LB_ALGORITHM_ROUND_ROBIN', server_port=80, servers=None, **kwargs):
+    command = 'pool'
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    if not servers:
+        ret['result'] = False
+        ret['comment'] = "Error: Server not defined"
+        return ret
+
+    check = pool_get(name)
+    if 'Error' not in check:
+        ret['comment'] = "Pool " + name + " already exists"
+        return ret
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = "Pool " + name + " will be created"
+        return ret
+
+    data = {}
+    data["lb_algorithm"] = lb_algorithm
+    data["default_server_port"] = server_port
+    data["name"] = name
+    server_data = []
+    for server in servers:
+        servers_type = _get_input_type(server)
+        srv = {'ip': {'type': servers_type,
+                      'addr': server}}
+        server_data.append(srv)
+    data['servers'] = server_data
+    result = send_request_post(command, data)
+    if result:
+        ret['comment'] = "Pool " + name + " has been created"
+        ret['changes'] = {'Pool': {'old': '', 'new': name}}
+    else:
+        ret['result'] = False
+        ret['comment'] = {"Error": "Pool was not created", "reason": result}
+    return ret
+
+
+def pool_delete(name, **kwargs):
+    command = 'pool'
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    check = pool_get(name)
+    if not check:
+        ret['comment'] = "Pool " + name + " is already deleted"
+        return ret
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = "Pool " + name + " will be deleted"
+        return ret
+
+    result = send_request_delete(command, check['uuid'])
+    if result:
+        ret['comment'] = "Pool " + name + " has been deleted"
+        ret['changes'] = {'Pool': {'old': name, 'new': ''}}
+    else:
+        ret['result'] = False
+        ret['comment'] = "Error: Pool was not created"
+    return ret
+
+
+def virtual_service_list():
+    command = "virtualservice"
+    ret = send_request_get(command)
+    return ret.json()
+
+
+def virtual_service_get(name):
+    vservices = virtual_service_list()
+    for vs in vservices['results']:
+        if name == vs['name'] or name == vs['uuid']:
+            return vs
+    return None
+
+
+def virtual_service_create():
+    command = "virtualservice"
+    vip = [{'auto_allocate_floating_ip': True,
+            'auto_allocate_ip': True,
+            'ipam_network_subnet': {
+                'subnet': {
+                    "mask": 24,
+                    "ip_addr": {
+                        "type": "V4",
+                        "addr": "192.168.32.0"
+                    }
+                }
+            }
+            }]
+    service = [{"port": 8080}]
+    data = {'name': "test",
+            'services': service,
+            'vip': vip
+            }
+    ret = send_request_post(command, data)
+    return ret.json()
+
+
+def cloud_list():
+    command = "cloud"
+    ret = send_request_get(command)
+    return ret.json()
+
+
+def cloud_get(name, **kwargs):
+    clouds = cloud_list()
+    for cloud in clouds['results']:
+        if name == cloud['name'] or name == cloud['uuid']:
+            return cloud
+    return {'result': False,
+            'Error': "Error in the retrieving cloud."}
+
+
+def cloud_create(name, mtu=1500, dhcp_enabled=False, openstack=None,  **kwargs):
+    command = 'cloud'
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    check = cloud_get(name)
+    if 'Error' not in check:
+        ret['comment'] = "Cloud " + name + " already exists"
+        return ret
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = "Cloud " + name + " will be created"
+        return ret
+
+    data = {}
+    data["name"] = name
+    data["vtype"] = "CLOUD_OPENSTACK"
+    data["license_type"] = "LIC_CORES"
+    data["mtu"] = mtu
+    data["dhcp_enabled"] = dhcp_enabled
+
+    if openstack:
+        openstack_conf = {}
+        openstack_conf['username'] = openstack['username']
+        openstack_conf['password'] = openstack['password']
+        openstack_conf['admin_tenant'] = openstack['admin_tenant']
+        openstack_conf['auth_url'] = openstack['auth_url']
+        openstack_conf['mgmt_network_name'] = openstack['mgmt_network_name']
+        openstack_conf['privilege'] = 'WRITE_ACCESS'
+        openstack_conf['region'] = openstack['region']
+        openstack_conf['hypervisor'] = 'KVM'
+        openstack_conf['free_floatingips'] = openstack['free_floatingips']
+        openstack_conf['img_format'] = 'OS_IMG_FMT_QCOW2'
+        openstack_conf['use_internal_endpoints'] = openstack['use_internal_endpoints']
+        openstack_conf['insecure'] = openstack['insecure']
+        openstack_conf['contrail_endpoint'] = openstack['contrail_endpoint']
+        role_mapping = {'os_role': '*', 'avi_role': openstack['avi_role']}
+        openstack_conf['role_mapping'] = role_mapping
+        data["openstack_configuration"] = openstack_conf
+    result = send_request_post(command, data)
+    if result:
+        ret['comment'] = "Cloud " + name + " has been created"
+        ret['changes'] = {'Cloud': {'old': '', 'new': name}}
+    else:
+        ret['result'] = False
+        ret['comment'] = {"Error": "Cloud was not created", "reason": result}
+    return ret
+
+
+def cloud_delete(name, **kwargs):
+    command = 'cloud'
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    check = cloud_get(name)
+    if not check:
+        ret['comment'] = "Cloud " + name + " is already deleted"
+        return ret
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = "Cloud " + name + " will be deleted"
+        return ret
+
+    result = send_request_delete(command, check['uuid'])
+    if result:
+        ret['comment'] = "Cloud " + name + " has been deleted"
+        ret['changes'] = {'Cloud': {'old': name, 'new': ''}}
+    else:
+        ret['result'] = False
+        ret['comment'] = "Error: Cloud was not created"
+    return ret
+
+
+def cluster_get():
+    command = "cluster"
+    ret = send_request_get(command)
+    return ret.json()
+
+
+def cluster_update(name, nodes, virtual_ip=None, **kwargs):
+    command = 'cluster'
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = "Cluster " + name + " will be updated"
+        return ret
+
+    data = {}
+    data["name"] = name
+
+    vip = {'addr': virtual_ip,
+           'type': _get_input_type(virtual_ip)}
+    data['virtual_ip'] = vip
+    nodes_data = []
+    for node in nodes:
+        node_type = _get_input_type(node['addr'])
+        n = {'ip': {'type': node_type,
+                    'addr': node['addr']},
+             'name': node['name']}
+        nodes_data.append(n)
+    data['nodes'] = nodes_data
+
+    result = send_request_put(command, data)
+    if result:
+        ret['comment'] = "Cluster " + name + " has been updated"
+        ret['changes'] = {'Pool': {'old': 'unknown', 'new': name}}
+    else:
+        ret['result'] = False
+        ret['comment'] = {"Error": "Cluster was not updates", "reason": result.json()['error']}
+    return ret
diff --git a/_states/avinetworks.py b/_states/avinetworks.py
new file mode 100644
index 0000000..9b7cba2
--- /dev/null
+++ b/_states/avinetworks.py
@@ -0,0 +1,215 @@
+#!/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 pool existence
+--------------------------
+
+.. code-block:: yaml
+
+    create pool:
+      avinetworks.pool_present:
+        - name: testing_pool
+        - lb_algorithm: LB_ALGORITHM_ROUND_ROBIN
+        - server_port: 8080
+        - servers:
+          - "192.168.0.122"
+          - "192.168.0.123"
+          - "192.168.0.124"
+
+
+Enforce the pool absence
+------------------------
+
+.. code-block:: yaml
+
+    delete pool:
+      avinetworks.pool_absent:
+        - name: testing_pool
+
+
+Enforce the cloud existence
+---------------------------
+
+.. code-block:: yaml
+
+    create cloud:
+      avinetworks.cloud_present:
+        - name: testing_cloud
+        - mtu: 1450
+        - dhcp_enabled: True
+        - openstack:
+            username: admin
+            password: password1*
+            admin_tenant: avi-networks
+            auth_url: http://10.167.4.100:5000/v2.0
+            mgmt_network_name: avi-net
+            privilege: WRITE_ACCESS
+            region: RegionOne
+            hypervisor: KVM
+            free_floatingips: True
+            img_format: OS_IMG_FMT_QCOW2
+            use_internal_endpoints: True
+            insecure: False
+            contrail_endpoint: http://10.167.4.200:9100
+            os_role: '*'
+            avi_role: Tenant-Admin
+
+
+Enforce the cloud absence
+-------------------------
+
+.. code-block:: yaml
+
+    delete cloud:
+      avinetworks.cloud_absent:
+        - name: testing_cloud
+
+
+Enforce the cluster present
+---------------------------
+
+.. code-block:: yaml
+
+    update cluster:
+      avinetworks.cluster_present:
+        - name: my_cluster
+        - virtual_ip: 172.17.32.252
+        - nodes:
+          - name: avi01
+            addr: 172.17.32.228
+          - name: avi02
+            addr: 172.17.32.235
+          - name: avi03
+            addr: 172.17.32.232
+
+'''
+
+
+def __virtual__():
+    '''
+    Load Avinetworks module
+    '''
+    return 'avinetworks'
+
+
+def pool_present(name, lb_algorithm='LB_ALGORITHM_ROUND_ROBIN', server_port=80, servers=None, **kwargs):
+    '''
+    Ensures that the Avinetworks pool exists.
+
+    :param name:          Pool name
+    :param server_port:   Traffic sent to servers will use this destination server port unless overridden by the server's specific port attribute.
+    :param lb_algorithm:  The load balancing algorithm will pick a server within the pool's list of available servers
+    :param servers:       The pool directs load balanced traffic to this list of destination servers. The servers can be configured by IP address, name, network or via IP Address
+
+    lb_algorithm choices:
+      - LB_ALGORITHM_ROUND_ROBIN
+      - LB_ALGORITHM_LEAST_LOAD
+      - LB_ALGORITHM_FEWEST_TASKS
+      - LB_ALGORITHM_RANDOM
+      - LB_ALGORITHM_FEWEST_SERVERS
+      - LB_ALGORITHM_CONSISTENT_HASH
+      - LB_ALGORITHM_FASTEST_RESPONSE
+      - LB_ALGORITHM_LEAST_CONNECTIONS
+    '''
+    ret = __salt__['avinetworks.pool_create'](name, lb_algorithm, server_port, servers, **kwargs)
+    if len(ret['changes']) == 0:
+        pass
+    return ret
+
+
+def pool_absent(name, **kwargs):
+    '''
+    Ensure that the Avinetworks pool doesn't exist
+
+    :param name: The name of the pool that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Pool "{0}" is already absent'.format(name)}
+    result = __salt__['avinetworks.pool_get'](name, **kwargs)
+    if 'Error' not in result:
+        ret = __salt__['avinetworks.pool_delete'](name, **kwargs)
+    return ret
+
+
+def cloud_present(name, mtu=1500, dhcp_enabled=False, openstack=None, **kwargs):
+    '''
+    Ensures that the Avinetworks Cloud exists.
+
+    :param name:            Cloud name [string]
+    :param mtu:             MTU setting for the cloud [uint32]
+    :param dhcp_enabled:    Select the IP address management scheme [bool]
+    :param openstack:       The OpenStack configuration [dict]
+
+    openstack_params:
+        :param username:                The username Avi Vantage will use when authenticating to Keystone. [string]
+        :param password:                The password Avi Vantage will use when authenticating to Keystone. [string]
+        :param admin_tenant:            OpenStack admin tenant (or project) information. [string]
+        :param auth_url:                Auth URL for connecting to keystone. If this is specified, any value provided for keystone_host is ignored. [string]
+        :param mgmt_network_name:       Avi Management network name or cidr [string]
+        :param privilege:               Access privilege. [enum] {WRITE_ACCESS, READ_ACCESS, NO_ACCESS}
+        :param region:                  Region name [string]
+        :param hypervisor:              Default hypervisor type. [enum] {DEFAULT, VMWARE_VSAN, VMWARE_ESX, KVM}
+        :param free_floatingips:        Free unused floating IPs. [bool]
+        :param img_format:              If OS_IMG_FMT_RAW, use RAW images else use QCOW2 or streamOptimized/flat VMDK as appropriate. [enum]
+        :param use_internal_endpoints:  Use internalURL for OpenStack endpoints instead of the default publicURL [bool]
+        :param insecure:                Allow self-signed certificates when communicating with https service endpoints. [bool]
+        :param contrail_endpoint:       Contrail VNC endpoint url (example http://10.10.10.100:8082). [string]
+        :param os_role:                 Role name in OpenStack [string]
+        :param avi_role:                Role name in Avi [string]
+
+    '''
+
+    ret = __salt__['avinetworks.cloud_create'](name, mtu, dhcp_enabled, openstack, **kwargs)
+    if len(ret['changes']) == 0:
+        pass
+    return ret
+
+def cloud_absent(name, **kwargs):
+    '''
+    Ensure that the Avinetworks cloud doesn't exist
+
+    :param name: The name of the cloud that should not exist
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Cloud "{0}" is already absent'.format(name)}
+    result = __salt__['avinetworks.cloud_get'](name, **kwargs)
+    if 'Error' not in result:
+        ret = __salt__['avinetworks.cloud_delete'](name, **kwargs)
+    return ret
+
+
+def cluster_present(name, nodes, virtual_ip=None, **kwargs):
+    '''
+    Ensures that the Avinetworks pool exists.
+
+    :param name:          Pool name
+    :param server_port:   Traffic sent to servers will use this destination server port unless overridden by the server's specific port attribute.
+    :param lb_algorithm:  The load balancing algorithm will pick a server within the pool's list of available servers
+    :param servers:       The pool directs load balanced traffic to this list of destination servers. The servers can be configured by IP address, name, network or via IP Address
+    '''
+    ret = __salt__['avinetworks.cluster_update'](name, nodes, virtual_ip, **kwargs)
+    if len(ret['changes']) == 0:
+        pass
+    return ret
diff --git a/avinetworks/contrail.sls b/avinetworks/contrail.sls
new file mode 100644
index 0000000..b6147ef
--- /dev/null
+++ b/avinetworks/contrail.sls
@@ -0,0 +1,18 @@
+{%- if pillar.avinetworks is defined %}
+{%- if pillar.avinetworks.get('module','') == 'contrail' %}
+
+{%- set service_appliance = pillar.avinetworks.service_appliance %}
+
+Avinetworks_create_contrail_service_appliance:
+  contrail.service_appliance_set_present:
+    - name: {{ service_appliance.name }}
+    - driver: {{ service_appliance.driver }}
+    - properties:
+        address:  {{ service_appliance.address }}
+        user: {{ service_appliance.user }}
+        password: {{ service_appliance.password }}
+        cloud: {{ service_appliance.cloud }}
+    - ha_mode: {{ service_appliance.ha_mode }}
+
+{%- endif %}
+{%- endif %}
diff --git a/metadata/service/contrail.yml b/metadata/service/contrail.yml
new file mode 100644
index 0000000..f6c4fb6
--- /dev/null
+++ b/metadata/service/contrail.yml
@@ -0,0 +1,18 @@
+applications:
+- avinetworks.contrail
+
+parameters:
+  _param:
+    avinetworks_service_appliance_name: avi_adc_v2
+    avinetworks_service_appliance_driver: 'neutron_lbaas.drivers.avi.avi_ocdriver.OpencontrailAviLoadbalancerDriver'
+    avinetworks_service_appliance_cloud: Default-Cloud
+    avinetworks_service_appliance_ha_mode: active-backup
+  avinetworks:
+    service_appliance:
+      name: ${_param:avinetworks_service_appliance_name}
+      driver: ${_param:avinetworks_service_appliance_driver}
+      address: ${avinetworks:api:ip}
+      user: ${avinetworks:api:user}
+      password: ${avinetworks:api:password}
+      cloud: ${_param:avinetworks_service_appliance_cloud}
+      ha_mode: ${_param:avinetworks_service_appliance_ha_mode}
diff --git a/metadata/service/control.yml b/metadata/service/control.yml
new file mode 100644
index 0000000..229ee78
--- /dev/null
+++ b/metadata/service/control.yml
@@ -0,0 +1,2 @@
+applications:
+- avinetworks.salt