Developing juniper contrail plugin, implenting CRUD operations and first version... 86/8886/4
authorlloretgalleg <illoret@indra.es>
Thu, 7 May 2020 15:02:25 +0000 (17:02 +0200)
committerlloretgalleg <illoret@indra.es>
Thu, 21 May 2020 07:13:23 +0000 (09:13 +0200)
Change-Id: I05435d4e5c4fab87184b057472ac12610c1071c9
Signed-off-by: lloretgalleg <illoret@indra.es>
RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/rest_lib.py
RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py [new file with mode: 0644]
RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_assist_juniper_contrail.py

index 18c3bd2..7ec2401 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from io import BytesIO
-import pycurl
+import requests
 import json
 import copy
+import logging
+
+from time import time
+from requests.exceptions import ConnectionError
 
 class HttpException(Exception):
     pass
 
+
 class NotFound(HttpException):
     pass
 
-class Http(object):
 
-    def __init__(self, logger):
-        self._logger = logger
-        self._response_headers = None
+class AuthError(HttpException):
+    pass
+
 
+class DuplicateFound(HttpException):
+    pass
 
-    def _check_http_response(self, http_code, data):
-        if http_code >= 300:
-            resp = ""
-            if data.getvalue():
-                data_text = data.getvalue().decode()
-                self._logger.info("Response {} DATA: {}".format(http_code, data_text))
-                resp = ": " + data_text
-            else:
-                self._logger.info("Response {}".format(http_code))
-            if http_code == 404:
-                raise NotFound("Error {}{}".format(http_code, resp))
-            raise HttpException("Error {}{}".format(http_code, resp))
 
+class ServiceUnavailableException(HttpException):
+    pass
 
-    def _get_curl_cmd(self, url, headers):
-        self._logger.debug("")
-        curl_cmd = pycurl.Curl()
-        curl_cmd.setopt(pycurl.URL, url)
-        curl_cmd.setopt(pycurl.SSL_VERIFYPEER, 0)
-        curl_cmd.setopt(pycurl.SSL_VERIFYHOST, 0)
-        if headers:
-            curl_cmd.setopt(pycurl.HTTPHEADER, headers)
-        return curl_cmd
 
+class ContrailHttp(object):
+
+    def __init__(self, auth_info, logger):
+        self._logger = logger
+        # default don't verify client cert
+        self._ssl_verify = False
+        # auth info: must contain auth_url and auth_dict
+        self.auth_url = auth_info["auth_url"]
+        self.auth_dict = auth_info["auth_dict"]
+
+        self.max_retries = 3
+
+        # Default token timeout
+        self.token_timeout = 3500
+        self.token = None
+        # TODO - improve configuration timeouts
 
     def get_cmd(self, url, headers):
         self._logger.debug("")
-        data = BytesIO()
-        curl_cmd = self._get_curl_cmd(url, headers)
-        curl_cmd.setopt(pycurl.HTTPGET, 1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        self._logger.info("Request METHOD: {} URL: {}".format("GET", url))
-        curl_cmd.perform()
-        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
-        self._logger.info("Response HTTPCODE: {}".format(http_code))
-        curl_cmd.close()
-        if data.getvalue():
-            data_text = data.getvalue().decode()
-            self._logger.debug("Response DATA: {}".format(data_text))
-            return http_code, data_text
-        return http_code, None
-
+        resp = self._request("GET", url, headers)
+        return resp.json()
 
-    def delete_cmd(self, url, headers):
+    def post_headers_cmd(self, url, headers, post_fields_dict=None):
         self._logger.debug("")
-        data = BytesIO()
-        curl_cmd = self._get_curl_cmd(url, headers)
-        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        self._logger.info("Request METHOD: {} URL: {}".format("DELETE", url))
-        curl_cmd.perform()
-        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
-        self._logger.info("Response HTTPCODE: {}".format(http_code))
-        curl_cmd.close()
-        self._check_http_response(http_code, data)
-        # TODO 202 accepted should be returned somehow
-        if data.getvalue():
-            data_text = data.getvalue().decode()
-            self._logger.debug("Response DATA: {}".format(data_text))
-            return http_code, data_text
+        # obfuscate password before logging dict
+        if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'):
+            post_fields_dict_copy = copy.deepcopy(post_fields_dict)
+            post_fields_dict['auth']['identity']['password']['user']['password'] = '******'
+            json_data_log = post_fields_dict_copy
         else:
-            self._logger.debug("Response DATA: NONE")
-            return http_code, None
-
-
-    def header_function(self, header_line):
-        header_line = header_line.decode('iso-8859-1')
-        if ':' not in header_line:
-            return
-        name, value = header_line.split(':', 1)
-        name = name.strip()
-        value = value.strip()
-        name = name.lower()
-        self._response_headers[name] = value
-
-
-    def post_cmd(self, url, headers, postfields_dict=None, return_header=None):
-        self._logger.debug('url: {}, headers: {}, postfields_dict: {}, return_header: {}'.format(url, headers, postfields_dict, return_header))
-        data = BytesIO()
-        curl_cmd = self._get_curl_cmd(url, headers)
-        curl_cmd.setopt(pycurl.POST, 1)
-        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
-        if return_header:
-            self._response_headers = {}
-            curl_cmd.setopt(pycurl.HEADERFUNCTION, self.header_function)
-
-        jsondata = json.dumps(postfields_dict)
-        if postfields_dict.get('auth',{}).get('identity',{}).get('password',{}).get('user',{}).get('password'):
-            postfields_dict_copy = copy.deepcopy(postfields_dict)
-            postfields_dict_copy['auth']['identity']['password']['user']['password'] = '******'
-            jsondata_log = json.dumps(postfields_dict_copy)
+            json_data_log = post_fields_dict
+        self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
+        resp = self._request("POST_HEADERS", url, headers, data=post_fields_dict)
+        return resp.text
+
+    def post_cmd(self, url, headers, post_fields_dict=None):
+        self._logger.debug("")
+        # obfuscate password before logging dict
+        if post_fields_dict.get('auth', {}).get('identity', {}).get('password', {}).get('user', {}).get('password'):
+            post_fields_dict_copy = copy.deepcopy(post_fields_dict)
+            post_fields_dict['auth']['identity']['password']['user']['password'] = '******'
+            json_data_log = post_fields_dict_copy
         else:
-            jsondata_log = jsondata
-        self._logger.debug("Request POSTFIELDS: {}".format(jsondata_log))
-        curl_cmd.setopt(pycurl.POSTFIELDS, jsondata)
-
-        self._logger.info("Request METHOD: {} URL: {}".format("POST", url))
-        curl_cmd.perform()
-        http_code = curl_cmd.getinfo(pycurl.HTTP_CODE)
-        self._logger.info("Response HTTPCODE: {}".format(http_code))
-        curl_cmd.close()
-        if return_header:
-            data_text = self._response_headers.get(return_header)
-            self._logger.debug("Response HEADER: {}".format(data_text))
-            return http_code, data_text
-        if data.getvalue():
-            data_text = data.getvalue().decode()
-            self._logger.debug("Response DATA: {}".format(data_text))
-            return http_code, data_text
+            json_data_log = post_fields_dict
+        self._logger.debug("Request POSTFIELDS: {}".format(json.dumps(json_data_log)))
+        resp = self._request("POST", url, headers, data=post_fields_dict)
+        return resp.text
+
+    def delete_cmd(self, url, headers):
+        self._logger.debug("")
+        resp = self._request("DELETE", url, headers)
+        return resp.text
+
+    def _get_token(self, headers):
+        self._logger.debug('Current Token:'.format(self.token))
+        auth_url = self.auth_url + 'auth/tokens'
+        if self.token is None or self._token_expired():
+            if not self.auth_url:
+                self.token = ""
+            resp = self._request_noauth(url=auth_url, op="POST", headers=headers,
+                                                 data=self.auth_dict)
+            self.token = resp.headers.get('x-subject-token')
+            self.last_token_time = time.time()
+            self._logger.debug('Obtained token: '.format(self.token))
+
+            return self.token
+
+    def _token_expired(self):
+        current_time = time.time()
+        if self.last_token_time and (current_time - self.last_token_time < self.token_timeout):
+            return False
         else:
-            return http_code, None
+            return True
+
+    def _request(self, op, url, http_headers, data=None, retry_auth_error=True):
+        headers = http_headers.copy()
+
+        # Get authorization (include authentication headers)
+        # todo - aƱadir token de nuevo
+        #token = self._get_token(headers)
+        token = None
+        if token:
+            headers['X-Auth-Token'] = token
+        try:
+            return self._request_noauth(op, url, headers, data)
+        except AuthError:
+            # If there is an auth error retry just once
+            if retry_auth_error:
+                return self._request(self, op, url, headers, data, retry_auth_error=False)
+
+    def _request_noauth(self, op, url, headers, data=None):
+        # Method to execute http requests with error control
+        # Authentication error, always make just one retry
+        # ConnectionError or ServiceUnavailable make configured retries with sleep between them
+        # Other errors to raise:
+        # - NotFound
+        # - Conflict
+
+        retry = 0
+        while retry < self.max_retries:
+            retry += 1
+
+            # Execute operation
+            try:
+                self._logger.info("Request METHOD: {} URL: {}".format(op, url))
+                if (op == "GET"):
+                    resp = self._http_get(url, headers, query_params=data)
+                elif (op == "POST"):
+                    resp = self._http_post(url, headers, json_data=data)
+                elif (op == "POST_HEADERS"):
+                    resp = self._http_post_headers(url, headers, json_data=data)
+                elif (op == "DELETE"):
+                    resp = self._http_delete(url, headers, json_data=data)
+                else:
+                    raise HttpException("Unsupported operation: {}".format(op))
+                self._logger.info("Response HTTPCODE: {}".format(resp.status_code))
+
+                # Check http return code
+                if resp:
+                    return resp
+                else:
+                    status_code = resp.status_code
+                    if status_code == 401:
+                        # Auth Error - set token to None to reload it and raise AuthError
+                        self.token = None
+                        raise AuthError("Auth error executing operation")
+                    elif status_code == 409:
+                        raise DuplicateFound("Duplicate resource url: {}, response: {}".format(url, resp.text))
+                    elif status_code == 404:
+                        raise NotFound("Not found resource url: {}, response: {}".format(url, resp.text))
+                    elif resp.status_code in [502, 503]:
+                        if not self.max_retries or retry >= self.max_retries:
+                            raise ServiceUnavailableException("Service unavailable error url: {}".format(url))
+
+                        continue
+                    else:
+                        raise HttpException("Error status_code: {}, error_text: {}".format(resp.status_code, resp.text))
+
+            except ConnectionError as e:
+                self._logger.error("Connection error executing request: {}".format(repr(e)))
+                if not self.max_retries or retry >= self.max_retries:
+                    raise ConnectionError
+                continue
+            except Exception as e:
+                self._logger.error("Error executing request: {}".format(repr(e)))
+                raise e
+
+    def _http_get(self, url, headers, query_params=None):
+        return requests.get(url, headers=headers, params=query_params)
+
+    def _http_post_headers(self, url, headers, json_data=None):
+        return requests.head(url, json=json_data, headers=headers, verify=False)
+
+    def _http_post(self, url, headers, json_data=None):
+        return requests.post(url, json=json_data, headers=headers, verify=False)
+
+    def _http_delete(self, url, headers, json_data=None):
+        return requests.delete(url, json=json_data, headers=headers)
 
diff --git a/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py b/RO-SDN-juniper_contrail/osm_rosdn_juniper_contrail/sdn_api.py
new file mode 100644 (file)
index 0000000..1c5b528
--- /dev/null
@@ -0,0 +1,305 @@
+# Copyright 2020 ETSI
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import logging
+import json
+
+from osm_ro.wim.sdnconn import SdnConnectorError
+from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp
+from osm_rosdn_juniper_contrail.rest_lib import NotFound
+from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound
+from osm_rosdn_juniper_contrail.rest_lib import HttpException
+
+class UnderlayApi:
+    """ Class with CRUD operations for the underlay API """
+
+    def __init__(self, url, config=None, user=None, password=None, logger=None):
+
+        self.logger = logger or logging.getLogger("openmano.sdnconn.junipercontrail.sdnapi")
+        self.controller_url = url
+
+        if not url:
+            raise SdnConnectorError("'url' must be provided")
+        if not url.startswith("http"):
+            url = "http://" + url
+        if not url.endswith("/"):
+            url = url + "/"
+        self.url = url
+
+        auth_url = None
+        self.project = None
+        self.domain = None
+        self.asn = None
+        self.fabric = None
+        if config:
+            self.auth_url = config.get("auth_url")
+            self.project = config.get("project")
+            self.domain = config.get("domain")
+            self.asn = config.get("asn")
+            self.fabric = config.get("fabric")
+
+        # Init http headers for all requests
+        self.http_header = {'Content-Type': 'application/json'}
+
+        if user:
+            self.user = user
+
+        if password:
+            self.password = password
+
+        auth_dict = {}
+        auth_dict['auth'] = {}
+        auth_dict['auth']['scope'] = {}
+        auth_dict['auth']['scope']['project'] = {}
+        auth_dict['auth']['scope']['project']['domain'] = {}
+        auth_dict['auth']['scope']['project']['domain']["id"] = self.domain
+        auth_dict['auth']['scope']['project']['name'] = self.project
+        auth_dict['auth']['identity'] = {}
+        auth_dict['auth']['identity']['methods'] = ['password']
+        auth_dict['auth']['identity']['password'] = {}
+        auth_dict['auth']['identity']['password']['user'] = {}
+        auth_dict['auth']['identity']['password']['user']['name'] = self.user
+        auth_dict['auth']['identity']['password']['user']['password'] = self.password
+        auth_dict['auth']['identity']['password']['user']['domain'] = {}
+        auth_dict['auth']['identity']['password']['user']['domain']['id'] = self.domain
+        self.auth_dict = auth_dict
+
+        # Init http lib
+        auth_info = {"auth_url": self.auth_url, "auth_dict": auth_dict}
+        self.http = ContrailHttp(auth_info, self.logger)
+
+    def check_auth(self):
+        response = self.http.get_cmd(url=self.auth_url, headers=self.http_header)
+        return response
+
+    # Helper methods for CRUD operations
+    def get_all_by_type(self, controller_url, type):
+        endpoint = controller_url + type
+        response = self.http.get_cmd(url=endpoint, headers=self.http_header)
+        return response.get(type)
+
+    def get_by_uuid(self, type, uuid):
+        try:
+            endpoint = self.controller_url + type + "/{}".format(uuid)
+            response = self.http.get_cmd(url=endpoint, headers=self.http_header)
+            return response.get(type)
+        except NotFound:
+            return None
+
+    def delete_by_uuid(self, controller_url, type, uuid):
+        endpoint = controller_url + type + "/{}".format(uuid)
+        self.http.delete_cmd(url=endpoint, headers=self.http_header)
+
+    def get_uuid_from_fqname(self, type, fq_name):
+        """
+        Obtain uuid from fqname
+        Returns: If resource not found returns None
+        In case of error raises an Exception
+        """
+        payload = {
+            "type": type,
+            "fq_name": fq_name
+        }
+        try:
+            endpoint = self.controller_url + "fqname-to-id"
+            resp = self.http.post_cmd(url=endpoint,
+                                      headers=self.http_header,
+                                      post_fields_dict=payload)
+            return json.loads(resp).get("uuid")
+        except NotFound:
+            return None
+
+    def get_by_fq_name(self, type, fq_name):
+        # Obtain uuid by fqdn and then get data by uuid
+        uuid = self.get_uuid_from_fqname(type, fq_name)
+        if uuid:
+            return self.get_by_uuid(type, uuid)
+        else:
+            return None
+
+
+    # Aux methods to avoid code duplication of name conventions
+    def get_vpg_name(self, switch_id, switch_port):
+        return "{}_{}".format(switch_id, switch_port)
+
+    def get_vmi_name(self, switch_id, switch_port, vlan):
+        return "{}_{}-{}".format(switch_id, switch_port, vlan)
+
+    # Virtual network operations
+
+    def create_virtual_network(self, name, vni):
+        self.logger.debug("create vname, name: {}, vni: {}".format(name, vni))
+        routetarget = '{}:{}'.format(self.asn, vni)
+        vnet_dict = {
+            "virtual-network": {
+                "virtual_network_properties": {
+                    "vxlan_network_identifier": vni,
+                },
+                "parent_type": "project",
+                "fq_name": [
+                    self.domain,
+                    self.project,
+                    name
+                ],
+                "route_target_list": {
+                    "route_target": [
+                        "target:" + routetarget
+                    ]
+                }
+            }
+        }
+        endpoint = self.controller_url + 'virtual-networks'
+        resp = self.http.post_cmd(url=endpoint,
+                                  headers=self.http_header,
+                                  post_fields_dict=vnet_dict)
+        if not resp:
+            raise SdnConnectorError('Error creating virtual network: empty response')
+        vnet_info = json.loads(resp)
+        self.logger.debug("created vnet, vnet_info: {}".format(vnet_info))
+        return vnet_info.get("virtual-network").get('uuid'), vnet_info.get("virtual-network")
+
+    def get_virtual_networks(self):
+        return self.get_all_by_type('virtual-networks')
+
+    def get_virtual_network(self, network_id):
+        return self.get_by_uuid('virtual-network', network_id)
+
+    def delete_virtual_network(self, network_id):
+        self.logger.debug("delete vnet uuid: {}".format(network_id))
+        self.delete_by_uuid(self.controller_url, 'virtual-network', network_id)
+        self.logger.debug("deleted vnet uuid: {}".format(network_id))
+
+    # Vpg operations
+
+    def create_vpg(self, switch_id, switch_port):
+        self.logger.debug("create vpg, switch_id: {}, switch_port: {}".format(switch_id, switch_port))
+        vpg_name = self.get_vpg_name(switch_id, switch_port)
+        vpg_dict = {
+            "virtual-port-group": {
+                "parent_type": "fabric",
+                "fq_name": [
+                    "default-global-system-config",
+                    self.fabric,
+                    vpg_name
+                ]
+            }
+        }
+        endpoint = self.controller_url + 'virtual-port-groups'
+        resp = self.http.post_cmd(url=endpoint,
+                                  headers=self.http_header,
+                                  post_fields_dict=vpg_dict)
+        if not resp:
+            raise SdnConnectorError('Error creating virtual port group: empty response')
+        vpg_info = json.loads(resp)
+        self.logger.debug("created vpg, vpg_info: {}".format(vpg_info))
+        return vpg_info.get("virtual-port-group").get('uuid'), vpg_info.get("virtual-port-group")
+
+    def get_vpgs(self):
+        return self.get_all_by_type(self.controller_url, 'virtual-port-groups')
+
+    def get_vpg(self, vpg_id):
+        return self.get_by_uuid(self.controller_url, "virtual-port-group", vpg_id)
+
+    def get_vpg_by_name(self, vpg_name):
+        fq_name = [
+                    "default-global-system-config",
+                    self.fabric,
+                    vpg_name
+                ]
+        return self.get_by_fq_name("virtual-port-group", fq_name)
+
+    def delete_vpg(self, vpg_id):
+        self.logger.debug("delete vpg, uuid: {}".format(vpg_id))
+        self.delete_by_uuid(self.controller_url, 'virtual-port-group', vpg_id)
+        self.logger.debug("deleted vpg, uuid: {}".format(vpg_id))
+
+    def create_vmi(self, switch_id, switch_port, network, vlan):
+        self.logger.debug("create vmi, switch_id: {}, switch_port: {}, network: {}, vlan: {}".format(
+            switch_id, switch_port, network, vlan))
+        vmi_name = self.get_vmi_name(switch_id, switch_port, vlan)
+        vpg_name = self.get_vpg_name(switch_id, switch_port)
+        profile_dict = {
+            "local_link_information": [
+                {
+                    "port_id": switch_port,
+                    "switch_id": switch_port,
+                    "switch_info": switch_id,
+                    "fabric": self.fabric
+                }
+            ]
+
+        }
+        vmi_dict = {
+            "virtual-machine-interface": {
+                "parent_type": "project",
+                "fq_name": [
+                    self.domain,
+                    self.project,
+                    vmi_name
+                ],
+                "virtual_network_refs": [
+                    {
+                        "to": [
+                            self.domain,
+                            self.project,
+                            network
+                        ]
+                    }
+                ],
+                "virtual_machine_interface_properties": {
+                    "sub_interface_vlan_tag": vlan
+                },
+                "virtual_machine_interface_bindings": {
+                    "key_value_pair": [
+                        {
+                            "key": "vnic_type",
+                            "value": "baremetal"
+                        },
+                        {
+                            "key": "vif_type",
+                            "value": "vrouter"
+                        },
+                        {
+                            "key": "vpg",
+                            "value": vpg_name
+                        },
+                        {
+                            "key": "profile",
+                            "value": json.dumps(profile_dict)
+                        }
+                    ]
+                }
+            }
+        }
+        endpoint = self.controller_url + 'virtual-machine-interfaces'
+        self.logger.debug("vmi_dict: {}".format(vmi_dict))
+        resp = self.http.post_cmd(url=endpoint,
+                                  headers=self.http_header,
+                                  post_fields_dict=vmi_dict)
+        if not resp:
+            raise SdnConnectorError('Error creating vmi: empty response')
+        vmi_info = json.loads(resp)
+        self.logger.debug("created vmi, info: {}".format(vmi_info))
+        return vmi_info.get("virtual-machine-interface").get('uuid'), vmi_info.get("virtual-machine-interface")
+
+    def get_vmi(self, vmi_uuid):
+        return self.get_by_uuid(self.controller_url, 'virtual-machine-interface', vmi_uuid)
+
+    def delete_vmi(self, uuid):
+        self.logger.debug("delete vmi uuid: {}".format(uuid))
+        self.delete_by_uuid(self.controller_url, 'virtual-machine-interface', uuid)
+        self.logger.debug("deleted vmi: {}".format(uuid))
+
index 32b80dd..2ba53bd 100644 (file)
 #
 
 import logging
-import uuid
-import copy
 import json
 import yaml
 
-#import requests
-#from requests.auth import HTTPBasicAuth
 from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError
-from osm_rosdn_juniper_contrail.rest_lib import Http
+from osm_rosdn_juniper_contrail.rest_lib import ContrailHttp
+from osm_rosdn_juniper_contrail.rest_lib import NotFound
+from osm_rosdn_juniper_contrail.rest_lib import DuplicateFound
+from osm_rosdn_juniper_contrail.rest_lib import HttpException
 
-from io import BytesIO
-import pycurl
+from osm_rosdn_juniper_contrail.sdn_api import UnderlayApi
 
 
 class JuniperContrail(SdnConnectorBase):
@@ -73,21 +71,21 @@ class JuniperContrail(SdnConnectorBase):
         self.user = wim_account.get("user")
         self.password = wim_account.get("password")
 
-        url = wim.get("wim_url")
+        url = wim.get("wim_url") # underlay url
         auth_url = None
-        overlay_url = None
         self.project = None
         self.domain = None
         self.asn = None
         self.fabric = None
+        overlay_url = None
         self.vni_range = None
         if config:
             auth_url = config.get("auth_url")
-            overlay_url = config.get("overlay_url")
             self.project = config.get("project")
             self.domain = config.get("domain")
             self.asn = config.get("asn")
             self.fabric = config.get("fabric")
+            self.overlay_url = config.get("overlay_url")
             self.vni_range = config.get("vni_range")
 
         if not url:
@@ -98,29 +96,11 @@ class JuniperContrail(SdnConnectorBase):
             url = url + "/"
         self.url = url
 
-        if not self.project:
-            raise SdnConnectorError("'project' must be provided")
-        if not self.asn:
-            # TODO: Get ASN from controller config; otherwise raise ERROR for the moment
-            raise SdnConnectorError("'asn' was not provided and was not possible to obtain it")
-        if not self.fabric:
-            # TODO: Get FABRIC from controller config; otherwise raise ERROR for the moment
-            raise SdnConnectorError("'fabric' was not provided and was not possible to obtain it")
-        if not self.domain:
-            self.domain = 'default'
-            self.logger.info("No domain was provided. Using 'default'")
         if not self.vni_range:
             self.vni_range = ['1000001-2000000']
             self.logger.info("No vni_range was provided. Using ['1000001-2000000']")
         self.used_vni = set()
 
-        if overlay_url:
-            if not overlay_url.startswith("http"):
-                overlay_url = "http://" + overlay_url
-            if not overlay_url.endswith("/"):
-                overlay_url = overlay_url + "/"
-        self.overlay_url = overlay_url
-
         if auth_url:
             if not auth_url.startswith("http"):
                 auth_url = "http://" + auth_url
@@ -128,34 +108,36 @@ class JuniperContrail(SdnConnectorBase):
                 auth_url = auth_url + "/"
         self.auth_url = auth_url
 
-        # Init http lib
-        self.http = Http(self.logger)
-
-        # Init http headers for all requests
-        self.headers = {'Content-Type': 'application/json'}
-        self.http_header = ['{}: {}'.format(key, val)
-                           for (key, val) in list(self.headers.items())]
-
-        auth_dict = {}
-        auth_dict['auth'] = {}
-        auth_dict['auth']['scope'] = {}
-        auth_dict['auth']['scope']['project'] = {}
-        auth_dict['auth']['scope']['project']['domain'] = {}
-        auth_dict['auth']['scope']['project']['domain']["id"] = self.domain
-        auth_dict['auth']['scope']['project']['name'] = self.project
-        auth_dict['auth']['identity'] = {}
-        auth_dict['auth']['identity']['methods'] = ['password']
-        auth_dict['auth']['identity']['password'] = {}
-        auth_dict['auth']['identity']['password']['user'] = {}
-        auth_dict['auth']['identity']['password']['user']['name'] = self.user
-        auth_dict['auth']['identity']['password']['user']['password'] = self.password
-        auth_dict['auth']['identity']['password']['user']['domain'] = {}
-        auth_dict['auth']['identity']['password']['user']['domain']['id'] = self.domain
-        self.auth_dict = auth_dict
-        self.token = None
+        if overlay_url:
+            if not overlay_url.startswith("http"):
+                overlay_url = "http://" + overlay_url
+            if not overlay_url.endswith("/"):
+                overlay_url = overlay_url + "/"
+        self.overlay_url = overlay_url
 
-        self.logger.info("Juniper Contrail Connector Initialized.")
+        if not self.project:
+            raise SdnConnectorError("'project' must be provided")
+        if not self.asn:
+            # TODO: Get ASN from controller config; otherwise raise ERROR for the moment
+            raise SdnConnectorError("'asn' was not provided and it was not possible to obtain it")
+        if not self.fabric:
+            # TODO: Get FABRIC from controller config; otherwise raise ERROR for the moment
+            raise SdnConnectorError("'fabric' was not provided and was not possible to obtain it")
+        if not self.domain:
+            self.domain = 'default-domain'
+            self.logger.info("No domain was provided. Using 'default-domain'")
+
+        underlay_api_config = {
+            "auth_url": self.auth_url,
+            "project": self.project,
+            "domain": self.domain,
+            "asn": self.asn,
+            "fabric": self.fabric
+        }
+        self.underlay_api = UnderlayApi(url, underlay_api_config, user=self.user, password=self.password, logger=logger)
 
+        self._max_duplicate_retry = 2
+        self.logger.info("Juniper Contrail Connector Initialized.")
 
     def _generate_vni(self):
         """
@@ -178,154 +160,65 @@ class JuniperContrail(SdnConnectorBase):
             raise SdnConnectorError("Unable to create the virtual network."\
                 " All VNI in VNI range {} are in use.".format(self.vni_range))
 
-
-    def _get_token(self):
-        self.logger.debug('Current Token:'.format(str(self.token)))
-        auth_url = self.auth_url + 'auth/tokens'
-        if self.token is None:
-            if not self.auth_url:
-                self.token = ""
-            http_code, resp = self.http.post_cmd(url=auth_url, headers=self.http_header,
-                                                 postfields_dict=self.auth_dict,
-                                                 return_header = 'x-subject-token')
-            self.token = resp
-            self.logger.debug('Token: '.format(self.token))
-
-            if self.token:
-                self.headers['X-Auth-Token'] = self.token
-            else:
-                self.headers.pop('X-Auth-Token', None)
-            http_header = ['{}: {}'.format(key, val)
-                           for (key, val) in list(self.headers.items())]
-
     
     # Aux functions for testing
     def get_url(self):
         return self.url
 
-
     def get_overlay_url(self):
         return self.overlay_url
 
-    # Virtual network operations
-
-    def _create_virtual_network(self, controller_url, name, vni):
-        routetarget = '{}:{}'.format(self.asn,vni)
-        vnet_dict = {
-                        "virtual-network": {
-                            "virtual_network_properties": {
-                                "vxlan_network_identifier": vni,
-                            },
-                            "parent_type": "project",
-                            "fq_name": [
-                                "default-domain",
-                                "admin",
-                                name
-                            ],
-                            "route_target_list": {
-                                "route_target": [
-                                    "target:" + routetarget
-                                ]
-                            }
-                        }
-                    }
-        self._get_token()
-        endpoint = controller_url + 'virtual-networks'
-        http_code, resp = self.http.post_cmd(url = endpoint,
-                                             headers = self.http_header,
-                                             postfields_dict = vnet_dict)
-        if http_code not in (200, 201, 202, 204) or not resp:
-           raise SdnConnectorError('Unexpected http status code, or empty response')
-        vnet_info = json.loads(resp)
-        self.logger.debug("vnet_info: {}".format(vnet_info))
-        return vnet_info.get("virtual-network").get('uuid'), vnet_info.get("virtual-network")
-
-
-    def _get_virtual_networks(self, controller_url):
-        self._get_token()
-        endpoint = controller_url + 'virtual-networks'
-        print(endpoint)
-        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204) or not resp:
-            raise SdnConnectorError('Unexpected http status code, or empty response')
-        vnets_info = json.loads(resp)
-        self.logger.debug("vnets_info: {}".format(vnets_info))
-        return vnets_info.get('virtual-networks')
-
-
-    def _get_virtual_network(self, controller_url, network_id):
-        self._get_token()
-        endpoint = controller_url + 'virtual-network/{}'.format(network_id)
-        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204) or not resp:
-            if http_code == 404:
-                return None
-            raise SdnConnectorError('Unexpected http status code, or empty response')
-        vnet_info = json.loads(resp)
-        self.logger.debug("vnet_info: {}".format(vnet_info))
-        return vnet_info.get("virtual-network")
-
-
-    def _delete_virtual_network(self, controller_url, network_id):
-        self._get_token()
-        endpoint = controller_url + 'virtual-network/{}'.format(network_id)
-        http_code, _ = self.http.delete_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204):
-            raise SdnConnectorError('Unexpected http status code')
-        return
-
-
-    # Virtual port group operations
-
-    def _create_vpg(self, controller_url, switch_id, switch_port, network, vlan):
-        vpg_dict = {
-                       "virtual-port-group": {
-                       }
-                   }
-        self._get_token()
-        endpoint = controller_url + 'virtual-port-groups'
-        http_code, resp = self.http.post_cmd(url = endpoint,
-                                               headers = self.http_header,
-                                               postfields_dict = vpg_dict)
-        if http_code not in (200, 201, 202, 204) or not resp:
-           raise SdnConnectorError('Unexpected http status code, or empty response')
-        vpg_info = json.loads(resp)
-        self.logger.debug("vpg_info: {}".format(vpg_info))
-        return vpg_info.get("virtual-port-group").get('uuid'), vpg_info.get("virtual-port-group")
-
-
-    def _get_vpgs(self, controller_url):
-        self._get_token()
-        endpoint = controller_url + 'virtual-port-groups'
-        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204) or not resp:
-            raise SdnConnectorError('Unexpected http status code, or empty response')
-        vpgs_info = json.loads(resp)
-        self.logger.debug("vpgs_info: {}".format(vpgs_info))
-        return vpgs_info.get('virtual-port-groups')
-
-
-    def _get_vpg(self, controller_url, vpg_id):
-        self._get_token()
-        endpoint = controller_url + 'virtual-port-group/{}'.format(vpg_id)
-        http_code, resp = self.http.get_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204) or not resp:
-            if http_code == 404:
-                return None
-            raise SdnConnectorError('Unexpected http status code, or empty response')
-        vpg_info = json.loads(resp)
-        self.logger.debug("vpg_info: {}".format(vpg_info))
-        return vpg_info.get("virtual-port-group")
-
-
-    def _delete_vpg(self, controller_url, vpg_id):
-        self._get_token()
-        endpoint = controller_url + 'virtual-port-group/{}'.format(vpg_id)
-        http_code, resp = self.http.delete_cmd(url=endpoint, headers=self.http_header)
-        if http_code not in (200, 201, 202, 204):
-            raise SdnConnectorError('Unexpected http status code')
-        return
+    def _create_port(self, switch_id, switch_port, network, vlan):
+        """
+        1 - Look for virtual port groups for provided switch_id, switch_port using name
+        2 - It the virtual port group does not exist, create it
+        3 - Create virtual machine interface for the indicated network and vlan
+        """
+        self.logger.debug("create_port: switch_id: {}, switch_port: {}, network: {}, vlan: {}".format(
+            switch_id, switch_port, network, vlan))
+
+        # 1 - Check if the vpg exists
+        vpg_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
+        vpg = self.underlay_api.get_vpg_by_name(vpg_name)
+        if not vpg:
+            # 2 - If it does not exist create it
+            vpg_id, _ = self.underlay_api.create_vpg(switch_id, switch_port)
+        else:
+            # Assign vpg_id from vpg
+            vpg_id = vpg.get("uuid")
+
+        # 3 - Create vmi
+        vmi_id, _ = self.underlay_api.create_vmi(switch_id, switch_port, network, vlan)
+        self.logger.debug("port created")
+
+        return vpg_id, vmi_id
+
+    def _delete_port(self, vpg_id, vmi_id):
+        self.logger.debug("delete port, vpg_id: {}, vmi_id: {}".format(vpg_id, vmi_id))
 
+        # 1 - Obtain vpg by id (if not vpg_id must have been error creating ig, nothing to be done)
+        if vpg_id:
+            vpg = self.underlay_api.get_by_uuid("virtual-port-group", vpg_id)
+            if not vpg:
+                self.logger.warning("vpg: {} to be deleted not found".format(vpg_id))
+            else:
+                # 2 - Get vmi interfaces from vpg
+                vmi_list = vpg.get("virtual_machine_interface_refs")
+                if not vmi_list:
+                    # must have been an error during port creation when vmi is created
+                    # may happen if there has been an error during creation
+                    self.logger.warning("vpg: {} has not vmi, will delete nothing".format(vpg))
+                else:
+                    num_vmis = len(vmi_list)
+                    for vmi in vmi_list:
+                        uuid = vmi.get("uuid")
+                        if uuid == vmi_id:
+                            self.underlay_api.delete_vmi(vmi.get("uuid"))
+                            num_vmis = num_vmis - 1
+
+                # 3 - If there are no more vmi delete the vpg
+                if not vmi_list or num_vmis == 0:
+                    self.underlay_api.delete_vpg(vpg.get("uuid"))
 
     def check_credentials(self):
         """Check if the connector itself can access the SDN/WIM with the provided url (wim.wim_url),
@@ -336,15 +229,13 @@ class JuniperContrail(SdnConnectorBase):
                 external URLs, etc are detected.
         """
         self.logger.debug("")
-        self._get_token()
         try:
-            http_code, resp = self.http.get_cmd(url=self.auth_url, headers=self.http_header)
-            if http_code not in (200, 201, 202, 204) or not resp:
-                raise SdnConnectorError('Unexpected http status code, or empty response')
+            resp = self.underlay_api.check_auth()
+            if not resp:
+                raise SdnConnectorError('Empty response')
         except Exception as e:
             self.logger.error('Error checking credentials')
-            raise SdnConnectorError('Error checking credentials', http_code=http_code)
-
+            raise SdnConnectorError('Error checking credentials: {}'.format(str(e)))
 
     def get_connectivity_service_status(self, service_uuid, conn_info=None):
         """Monitor the status of the connectivity service established
@@ -384,18 +275,27 @@ class JuniperContrail(SdnConnectorBase):
                 new information available for the connectivity service.
         """
         self.logger.debug("")
-        self._get_token()
         try:
-            http_code, resp = self.http.get_cmd(endpoint='virtual-network/{}'.format(service_uuid))
-            if http_code not in (200, 201, 202, 204) or not resp:
-                raise SdnConnectorError('Unexpected http status code, or empty response')
+            resp = self.http.get_cmd(endpoint='virtual-network/{}'.format(service_uuid))
+            if not resp:
+                raise SdnConnectorError('Empty response')
             if resp:
                 vnet_info = json.loads(resp)
-                return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info['virtual-network']}
+
+                # Check if conn_info reports error
+                if conn_info.get("sdn_status") == "ERROR":
+                    return {'sdn_status': 'ACTIVE', 'sdn_info': "conn_info indicates pending error"}
+                else:
+                    return {'sdn_status': 'ACTIVE', 'sdn_info': vnet_info['virtual-network']}
             else:
                 return {'sdn_status': 'ERROR', 'sdn_info': 'not found'}
+        except SdnConnectorError:
+            raise
+        except HttpException as e:
+            self.logger.error("Error getting connectivity service: {}".format(e))
+            raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
         except Exception as e:
-            self.logger.error('Exception getting connectivity service info: %s', e)
+            self.logger.error('Exception getting connectivity service info: %s', e, exc_info=True)
             return {'sdn_status': 'ERROR', 'error_msg': str(e)}
 
 
@@ -435,11 +335,6 @@ class JuniperContrail(SdnConnectorBase):
         :raises: SdnConnectorException: In case of error. Nothing should be created in this case.
             Provide the parameter http_code
         """
-        self.logger.debug("create_connectivity_service, service_type: {}, connection_points: {}".
-                          format(service_type, connection_points))
-        if service_type.lower() != 'elan':
-            raise SdnConnectorError('Only ELAN network type is supported by Juniper Contrail.')
-
         # Step 1. Check in the overlay controller the virtual network created by the VIM
         #   Best option: get network id of the VIM as param (if the VIM already created the network),
         #    and do a request to the controller of the virtual networks whose VIM network id is the provided
@@ -457,17 +352,131 @@ class JuniperContrail(SdnConnectorBase):
         #         name = 'osm-plugin-' + overlay_name
         #      Else:
         #         name = 'osm-plugin-' + VNI
+        self.logger.info("create_connectivity_service, service_type: {}, connection_points: {}".
+                         format(service_type, connection_points))
+        if service_type.lower() != 'elan':
+            raise SdnConnectorError('Only ELAN network type is supported by Juniper Contrail.')
+
+        try:
+            # Initialize data
+            conn_info = None
+            conn_info_cp = {}
+
+            # 1 - Obtain VLAN-ID
+            vlan = self._get_vlan(connection_points)
+            self.logger.debug("Provided vlan: {}".format(vlan))
+
+            # 2 - Obtain free VNI
+            vni = self._generate_vni()
+            self.logger.debug("VNI: {}".format(vni))
+
+            # 3 - Create virtual network (name, VNI, RT), by the moment the name will use VNI
+            retry = 0
+            while retry < self._max_duplicate_retry:
+                try:
+                    vnet_name = 'osm-plugin-' + str(vni)
+                    vnet_id, _ = self.underlay_api.create_virtual_network(vnet_name, vni)
+                    self.used_vni.add(vni)
+                    break
+                except DuplicateFound as e:
+                    self.logger.debug("Duplicate error for vnet_name: {}".format(vnet_name))
+                    self.used_vni.add(vni)
+                    retry += 1
+                    if retry >= self._max_duplicate_retry:
+                        raise e
+                    else:
+                        # Try to obtain a new vni
+                        vni = self._generate_vni()
+                        continue
+            conn_info = {
+                "vnet": {
+                    "uuid": vnet_id,
+                    "name": vnet_name
+                },
+                "connection_points": conn_info_cp # dict with port_name as key
+            }
+
+            # 4 - Create a port for each endpoint
+            for cp in connection_points:
+                switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                vpg_id, vmi_id = self._create_port(switch_id, switch_port, vnet_name, vlan)
+                cp_added = cp.copy()
+                cp_added["vpg_id"] = vpg_id
+                cp_added["vmi_id"] = vmi_id
+                conn_info_cp[self.underlay_api.get_vpg_name(switch_id, switch_port)] = cp_added
+
+            return vnet_id, conn_info
+            self.logger.info("created connectivity service, uuid: {}, name: {}".format(vnet_id, vnet_name))
+        except Exception as e:
+            # Log error
+            if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
+                self.logger.error("Error creating connectivity service: {}".format(e))
+            else:
+                self.logger.error("Error creating connectivity service: {}".format(e), exc_info=True)
+
+
+            # If nothing is created raise error else return what has been created and mask as error
+            if not conn_info:
+                raise SdnConnectorError("Exception create connectivity service: {}".format(str(e)))
+            else:
+                conn_info["sdn_status"] = "ERROR"
+                # iterate over not added connection_points and add but marking them as error
+                for cp in connection_points[len(conn_info_cp):]:
+                    cp_error = cp.copy()
+                    cp_error["sdn_status"] = "ERROR"
+                    switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                    switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                    conn_info_cp[self.underlay_api.get_vpg_name(switch_id, switch_port)] = cp_error
+                return vnet_id, conn_info
+
+    def delete_connectivity_service(self, service_uuid, conn_info=None):
+        """
+        Disconnect multi-site endpoints previously connected
+
+        :param service_uuid: The one returned by create_connectivity_service
+        :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
+            if they do not return None
+        :return: None
+        :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
+        """
+        self.logger.info("delete_connectivity_service vnet_name: {}, connection_points: {}".
+                          format(service_uuid, conn_info))
+
         try:
-            name = 'test-test-1'
-            vni = 999999
-            network_id, network_info = self._create_virtual_network(self.url, name, vni)
+            vnet_uuid = service_uuid
+            vnet_name = conn_info["vnet"]["name"]   # always should exist as the network is the first thing created
+            connection_points = conn_info["connection_points"].values()
+            vlan = self._get_vlan(connection_points)
+
+            # 1: For each connection point delete vlan from vpg and it is is the
+            # last one, delete vpg
+            for cp in connection_points:
+                self._delete_port(cp.get("vpg_id"), cp.get("vmi_id"))
+
+            # 2: Delete vnet
+            self.underlay_api.delete_virtual_network(vnet_uuid)
         except SdnConnectorError:
-            raise SdnConnectorError('Failed to create connectivity service {}'.format(name))
+            raise
+        except HttpException as e:
+            self.logger.error("Error deleting connectivity service: {}".format(e))
+            raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
         except Exception as e:
-            self.logger.error('Exception creating connection_service: %s', e, exc_info=True)
-            raise SdnConnectorError("Exception creating connectivity service: {}".format(str(e)))
-        return service_id
+            self.logger.error("Error deleting connectivity service: {}".format(e), exc_info=True)
+            raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
 
+    # Helper methods
+    @staticmethod
+    def _get_vlan(connection_points):
+        vlan = None
+        for cp in connection_points:
+            cp_vlan = cp.get("service_endpoint_encapsulation_info").get("vlan")
+            if not vlan:
+                vlan = cp_vlan
+            else:
+                if vlan != cp_vlan:
+                    raise SdnConnectorError("More that one cp provided")
+        return vlan
 
     def edit_connectivity_service(self, service_uuid, conn_info = None, connection_points = None, **kwargs):
         """ Change an existing connectivity service.
@@ -487,122 +496,134 @@ class JuniperContrail(SdnConnectorBase):
         Raises:
             SdnConnectorException: In case of error.
         """
-        #TODO: to be done. This comes from ONOS VPLS plugin
-        self.logger.debug("edit connectivity service, service_uuid: {}, conn_info: {}, "
+        # 0 - Check if there are connection_points marked as error and delete them
+        # 1 - Compare conn_info (old connection points) and connection_points (new ones to be applied):
+        #     Obtain list of connection points to be added and to be deleted
+        #     Obtain vlan and check it has not changed
+        # 2 - Obtain network: Check vnet exists and obtain name
+        # 3 - Delete unnecesary ports
+        # 4 - Add new ports
+        self.logger.info("edit connectivity service, service_uuid: {}, conn_info: {}, "
                           "connection points: {} ".format(service_uuid, conn_info, connection_points))
 
-        conn_info = conn_info or []
-        # Obtain current configuration
-        config_orig = self._get_onos_netconfig()
-        config = copy.deepcopy(config_orig)
-
-        # get current service data and check if it does not exists
-        #TODO: update
-        for vpls in config.get('apps', {}).get('org.onosproject.vpls', {}).get('vpls', {}).get('vplsList', {}):
-            if vpls['name'] == service_uuid:
-                self.logger.debug("service exists")
-                curr_interfaces = vpls.get("interfaces", [])
-                curr_encapsulation = vpls.get("encapsulation")
-                break
-        else:
-            raise SdnConnectorError("service uuid: {} does not exist".format(service_uuid))
-
-        self.logger.debug("current interfaces: {}".format(curr_interfaces))
-        self.logger.debug("current encapsulation: {}".format(curr_encapsulation))
-
-        # new interfaces names
-        new_interfaces = [port['service_endpoint_id'] for port in new_connection_points]
-
-        # obtain interfaces to delete, list will contain port
-        ifs_delete = list(set(curr_interfaces) - set(new_interfaces))
-        ifs_add = list(set(new_interfaces) - set(curr_interfaces))
-        self.logger.debug("interfaces to delete: {}".format(ifs_delete))
-        self.logger.debug("interfaces to add: {}".format(ifs_add))
-
-        # check if some data of the interfaces that already existed has changed
-        # in that case delete it and add it again
-        ifs_remain = list(set(new_interfaces) & set(curr_interfaces))
-        for port in connection_points:
-            if port['service_endpoint_id'] in ifs_remain:
-                # check if there are some changes
-                curr_port_name, curr_vlan = self._get_current_port_data(config, port['service_endpoint_id'])
-                new_port_name = 'of:{}/{}'.format(port['service_endpoint_encapsulation_info']['switch_dpid'],
-                                        port['service_endpoint_encapsulation_info']['switch_port'])
-                new_vlan = port['service_endpoint_encapsulation_info']['vlan']
-                if (curr_port_name != new_port_name or curr_vlan != new_vlan):
-                    self.logger.debug("TODO: must update data interface: {}".format(port['service_endpoint_id']))
-                    ifs_delete.append(port['service_endpoint_id'])
-                    ifs_add.append(port['service_endpoint_id'])
-
-        new_encapsulation = self._get_encapsulation(connection_points)
+        # conn_info should always exist and have connection_points and vnet elements
+        old_cp = conn_info.get("connection_points", {})
+        old_vlan = self._get_vlan(old_cp)
 
+        # Check if an element of old_cp is marked as error, in case it is delete it
+        # Not return a new conn_info in this case because it is only partial information
+        # Current conn_info already marks ports as error
         try:
-            # Delete interfaces, only will delete interfaces that are in provided conn_info
-            # because these are the ones that have been created for this service
-            if ifs_delete:
-                for port in config['ports'].values():
-                    for port_interface in port['interfaces']:
-                        interface_name = port_interface['name']
-                        self.logger.debug("interface name: {}".format(port_interface['name']))
-                        if interface_name in ifs_delete and interface_name in conn_info:
-                            self.logger.debug("delete interface name: {}".format(interface_name))
-                            port['interfaces'].remove(port_interface)
-                            conn_info.remove(interface_name)
-
-            # Add new interfaces
-            for port in connection_points:
-                if port['service_endpoint_id'] in ifs_add:
-                    created_ifz = self._append_port_to_config(port, config)
-                    if created_ifz:
-                        conn_info.append(created_ifz[1])
-            self._pop_last_update_time(config)
-            self._post_netconfig(config)
-
-            self.logger.debug("contrail config after updating interfaces: {}".format(config))
-            self.logger.debug("conn_info after updating interfaces: {}".format(conn_info))
-
-            # Update interfaces list in vpls service
-            for vpls in config.get('apps', {}).get('org.onosproject.vpls', {}).get('vpls', {}).get('vplsList', {}):
-                if vpls['name'] == service_uuid:
-                    vpls['interfaces'] = new_interfaces
-                    vpls['encapsulation'] = new_encapsulation
-
-            self._pop_last_update_time(config)
-            self._post_netconfig(config)
-            return conn_info
+            delete_conn_info = []
+            for cp in old_cp:
+                if cp.get("sdn_status") == "ERROR":
+                    switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                    switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                    self._delete_port(switch_id, switch_port, old_vlan)
+                    delete_conn_info.append(self.underlay_api.get_vpg_name(switch_id, switch_port))
+
+            for i in delete_conn_info:
+                del old_cp[i]
+
+            # Delete vnet status if exists (possibly marked as error)
+            if conn_info.get("vnet",{}).get("sdn_status"):
+                del conn_info["vnet"]["sdn_status"]
+        except HttpException as e:
+            self.logger.error("Error trying to delete old ports marked as error: {}".format(e))
+            raise SdnConnectorError(e)
+        except SdnConnectorError as e:
+            self.logger.error("Error trying to delete old ports marked as error: {}".format(e))
+            raise
         except Exception as e:
-            self.logger.error('Exception add connection_service: %s', e)
-            # try to rollback push original config
+            self.logger.error("Error trying to delete old ports marked as error: {}".format(e), exc_info=True)
+            raise SdnConnectorError("Error trying to delete old ports marked as error: {}".format(e))
+
+        if connection_points:
+
+            # Check and obtain what should be added and deleted, if there is an error here raise an exception
             try:
-                self._post_netconfig(config_orig)
-            except Exception as e2:
-                self.logger.error('Exception rolling back to original config: %s', e2)
-            # raise exception
-            if isinstance(e, SdnConnectorError):
+
+                vlan = self._get_vlan(connection_points)
+
+                old_port_list = ["{}_{}".format(cp["service_endpoint_encapsulation_info"]["switch_dpid"],
+                                          cp["service_endpoint_encapsulation_info"]["switch_port"])
+                           for cp in old_cp.values()]
+                port_list = ["{}_{}".format(cp["service_endpoint_encapsulation_info"]["switch_dpid"],
+                                          cp["service_endpoint_encapsulation_info"]["switch_port"])
+                           for cp in connection_points]
+                to_delete_ports = list(set(old_port_list) - set(port_list))
+                to_add_ports = list(set(port_list) - set(old_port_list))
+
+                # Obtain network
+                vnet = self.underlay_api.get_virtual_network(self.get_url(), service_uuid)
+                vnet_name = vnet["name"]
+
+            except SdnConnectorError:
                 raise
-            else:
-                raise SdnConnectorError("Exception create_connectivity_service: {}".format(e))
+            except Exception as e:
+                self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True)
+                raise SdnConnectorError("Exception edit connectivity service: {}".format(str(e)))
 
 
-    def delete_connectivity_service(self, service_uuid, conn_info=None):
-        """
-        Disconnect multi-site endpoints previously connected
+            # Delete unneeded ports and add new ones: if there is an error return conn_info
+            try:
+                # Connection points returned in con_info should reflect what has (and should as ERROR) be done
+                # Start with old cp dictionary and modify it as we work
+                conn_info_cp = old_cp
+
+                # Delete unneeded ports
+                for port_name in conn_info_cp.keys():
+                    if port_name in to_delete_ports:
+                        cp = conn_info_cp[port_name]
+                        switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                        switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                        self.logger.debug("delete port switch_id, switch_port: {}".format(switch_id, switch_port))
+                        self._delete_port(switch_id, switch_port, vlan)
+                        del conn_info_cp[port_name]
+
+                # Add needed ports
+                for cp in connection_points:
+                    if port_name in to_add_ports:
+                        switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                        switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                        self.logger.debug("add port switch_id, switch_port: {}".format(switch_id, switch_port))
+                        self._create_port(switch_id, switch_port, vnet_name, vlan)
+                        conn_info_cp[port_name]
+
+                conn_info["connection_points"] = conn_info_cp
+                return conn_info
+
+            except Exception as e:
+                # Log error
+                if isinstance(e, SdnConnectorError) or isinstance(e, HttpException):
+                    self.logger.error("Error edit connectivity service: {}".format(e), exc_info=True)
+                else:
+                    self.logger.error("Error edit connectivity service: {}".format(e))
+
+                # There has been an error mount conn_info_cp marking as error cp that should
+                # have been deleted but have not or should have been added
+                for port_name, cp in conn_info_cp.items():
+                    if port_name in to_delete_ports:
+                        cp["sdn_status"] = "ERROR"
+
+                for cp in connection_points:
+                    switch_id = cp.get("service_endpoint_encapsulation_info").get("switch_dpid")
+                    switch_port = cp.get("service_endpoint_encapsulation_info").get("switch_port")
+                    port_name = self.underlay_api.get_vpg_name(switch_id, switch_port)
+                    if port_name in to_add_ports:
+                        cp_error = cp.copy()
+                        cp_error["sdn_status"] = "ERROR"
+                        conn_info_cp[port_name] = cp_error
+
+                conn_info["sdn_status"] = "ERROR"
+                conn_info["connection_points"] = conn_info_cp
+                return conn_info
 
-        :param service_uuid: The one returned by create_connectivity_service
-        :param conn_info: The one returned by last call to 'create_connectivity_service' or 'edit_connectivity_service'
-            if they do not return None
-        :return: None
-        :raises: SdnConnectorException: In case of error. The parameter http_code must be filled
-        """
-        self.logger.debug("delete_connectivity_service uuid: {}".format(service_uuid))
-        try:
-            #TO DO: check if virtual port groups have to be deleted
-            self._delete_virtual_network(self.url, service_uuid)
-        except SdnConnectorError:
-            raise SdnConnectorError('Failed to delete service uuid {}'.format(service_uuid))
-        except Exception as e:
-            self.logger.error('Exception deleting connection_service: %s', e, exc_info=True)
-            raise SdnConnectorError("Exception deleting connectivity service: {}".format(str(e)))
+
+        else:
+            # Connection points have not changed, so do nothing
+            self.logger.info("no new connection_points provided, nothing to be done")
+            return
 
 
 if __name__ == '__main__':
@@ -613,8 +634,8 @@ if __name__ == '__main__':
     handler.setFormatter(log_formatter)
     logger = logging.getLogger('openmano.sdnconn.junipercontrail')
     #logger.setLevel(level=logging.ERROR)
-    logger.setLevel(level=logging.INFO)
-    #logger.setLevel(level=logging.DEBUG)
+    #logger.setLevel(level=logging.INFO)
+    logger.setLevel(level=logging.DEBUG)
     logger.addHandler(handler)
 
     # Read config
@@ -645,6 +666,17 @@ if __name__ == '__main__':
 
     underlay_url = juniper_contrail.get_url()
     overlay_url = juniper_contrail.get_overlay_url()
+    # Generate VNI
+    for i in range(5):
+        vni = juniper_contrail._generate_vni()
+        juniper_contrail.used_vni.add(vni)
+    print(juniper_contrail.used_vni)
+    juniper_contrail.used_vni.remove(1000003)
+    print(juniper_contrail.used_vni)
+    for i in range(2):
+        vni = juniper_contrail._generate_vni()
+        juniper_contrail.used_vni.add(vni)
+    print(juniper_contrail.used_vni)
     # 1. Read virtual networks from overlay controller
     print('1. Read virtual networks from overlay controller')
     try:
@@ -653,8 +685,7 @@ if __name__ == '__main__':
         print('OK')
     except Exception as e:
         logger.error('Exception reading virtual networks from overlay controller: %s', e)
-        print('FAILED')
-
+        print('FAILED')    
     # 2. Read virtual networks from underlay controller
     print('2. Read virtual networks from underlay controller')
     vnets = juniper_contrail._get_virtual_networks(underlay_url)
@@ -703,66 +734,87 @@ if __name__ == '__main__':
         logger.info('Exception reading virtual networks from overlay controller: %s', e)
     exit(0)
 
+    # Test CRUD:
+    net_name = "gerardo"
+    net_vni = "2000"
+    net_vlan = "501"
+    switch_1 = "LEAF-2"
+    port_1 = "xe-0/0/18"
+    switch_2 = "LEAF-1"
+    port_2 = "xe-0/0/18"
+
+    # 1 - Create a new virtual network
+    vnet2_id, vnet2_created = juniper_contrail._create_virtual_network(underlay_url, net_name, net_vni)
+    print("Created virtual network:")
+    print(vnet2_id)
+    print(yaml.safe_dump(vnet2_created, indent=4, default_flow_style=False))
+    print("Get virtual network:")
+    vnet2_info = juniper_contrail._get_virtual_network(underlay_url, vnet2_id)
+    print(json.dumps(vnet2_info, indent=4))
+    print('OK')
+
+    # 2 - Create a new virtual port group
+    vpg_id, vpg_info = juniper_contrail._create_vpg(underlay_url, switch_1, port_1, net_name, net_vlan)
+    print("Created virtual port group:")
+    print(vpg_id)
+    print(json.dumps(vpg_info, indent=4))
 
-    #TODO: to be deleted (it comes from ONOS VPLS plugin)
-    service_type = 'ELAN'
-    conn_point_0 = {
-        "service_endpoint_id": "switch1:ifz1",
-        "service_endpoint_encapsulation_type": "dot1q",
-        "service_endpoint_encapsulation_info": {
-            "switch_dpid": "0000000000000011",
-            "switch_port": "1",
-            "vlan": "600"
-        }
-    }
-    conn_point_1 = {
-        "service_endpoint_id": "switch3:ifz1",
-        "service_endpoint_encapsulation_type": "dot1q",
-        "service_endpoint_encapsulation_info": {
-            "switch_dpid": "0000000000000031",
-            "switch_port": "3",
-            "vlan": "600"
-        }
-    }
-    connection_points = [conn_point_0, conn_point_1]
-    # service_uuid, created_items = juniper_contrail.create_connectivity_service(service_type, connection_points)
-    #print(service_uuid)
-    #print(created_items)
-    #sleep(10)
-    #juniper_contrail.delete_connectivity_service("5496dfea-27dc-457d-970d-b82bac266e5c"))
-
-
-    conn_info = None
-    conn_info = ['switch1:ifz1', 'switch3_ifz3']
-    juniper_contrail.delete_connectivity_service("f7afc4de-556d-4b5a-8a12-12b5ef97d269", conn_info)
-
-    conn_point_0 = {
-        "service_endpoint_id": "switch1:ifz1",
-        "service_endpoint_encapsulation_type": "dot1q",
-        "service_endpoint_encapsulation_info": {
-            "switch_dpid": "0000000000000011",
-            "switch_port": "1",
-            "vlan": "500"
-        }
-    }
-    conn_point_2 = {
-        "service_endpoint_id": "switch1:ifz3",
-        "service_endpoint_encapsulation_type": "dot1q",
-        "service_endpoint_encapsulation_info": {
-            "switch_dpid": "0000000000000011",
-            "switch_port": "3",
-            "vlan": "500"
-        }
-    }
-    conn_point_3 = {
-        "service_endpoint_id": "switch3_ifz3",
-        "service_endpoint_encapsulation_type": "dot1q",
-        "service_endpoint_encapsulation_info": {
-            "switch_dpid": "0000000000000033",
-            "switch_port": "3",
-            "vlan": "500"
-        }
-    }
-    new_connection_points = [conn_point_0, conn_point_3]
-    #conn_info = juniper_contrail.edit_connectivity_service("f7afc4de-556d-4b5a-8a12-12b5ef97d269", conn_info, new_connection_points)
-    #print(conn_info)
+    print("Get virtual network:")
+    vnet2_info = juniper_contrail._get_virtual_network(underlay_url, vnet2_id)
+    print(yaml.safe_dump(vnet2_info, indent=4, default_flow_style=False))
+    print('OK')
+
+    # 3 - Create a new virtual machine interface
+    vmi_id, vmi_info = juniper_contrail._create_vmi(underlay_url, switch_1, port_1, net_name, net_vlan)
+    print("Created virtual machine interface:")
+    print(vmi_id)
+    print(yaml.safe_dump(vmi_info, indent=4, default_flow_style=False))
+
+    # 4 - Create a second virtual port group
+    # 5 - Create a second virtual machine interface
+
+    ### Test rapido de modificaciĆ³n de requests:
+    # Ver que metodos siguen funcionando y cuales no e irlos corrigiendo
+
+    """
+    vnets = juniper_contrail._get_virtual_networks(underlay_url)
+    logger.debug("Virtual networks:")
+    logger.debug(json.dumps(vnets, indent=2))
+
+    vpgs = juniper_contrail._get_vpgs(underlay_url)
+    logger.debug("Virtual port groups:")
+    logger.debug(json.dumps(vpgs, indent=2))
+    """
+    # Get by uuid
+
+    """
+    # 3 - Get vmi
+    vmi_uuid = "dbfd2099-b895-459e-98af-882d77d968c1"
+    vmi = juniper_contrail._get_vmi(underlay_url, vmi_uuid)
+    logger.debug("Virtual machine interface:")
+    logger.debug(json.dumps(vmi, indent=2))
+
+    # Delete vmi    
+    logger.debug("Delete vmi")
+    juniper_contrail._delete_vmi(underlay_url, vmi_uuid)
+    """
+
+    """
+    # 2 - Get vpg
+    vpg_uuid = "85156474-d1a5-44c0-9d8b-8f690f39d27e"
+    vpg = juniper_contrail._get_vpg(underlay_url, vpg_uuid)
+    logger.debug("Virtual port group:")
+    logger.debug(json.dumps(vpg, indent=2))
+    # Delete vpg
+    vpg = juniper_contrail._delete_vpg(underlay_url, vpg_uuid)
+    """
+
+    # 1 - Obtain virtual network
+    """
+    vnet_uuid = "68457d61-6558-4d38-a03d-369a9de803ea"
+    vnet = juniper_contrail._get_virtual_network(underlay_url, vnet_uuid)
+    logger.debug("Virtual network:")
+    logger.debug(json.dumps(vnet, indent=2))
+    # Delete virtual network
+    juniper_contrail._delete_virtual_network(underlay_url, vnet_uuid)
+    """