group-based-policy/gbpservice/contrib/nfp/configurator/drivers/firewall/vyos/vyos_fw_driver.py

697 lines
27 KiB
Python

# 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 requests
import time
from oslo_serialization import jsonutils
from gbpservice.contrib.nfp.configurator.drivers.base import base_driver
from gbpservice.contrib.nfp.configurator.drivers.firewall.vyos import (
vyos_fw_constants as const)
from gbpservice.contrib.nfp.configurator.lib import (
generic_config_constants as gen_cfg_const)
from gbpservice.contrib.nfp.configurator.lib import constants as common_const
from gbpservice.contrib.nfp.configurator.lib import data_parser
from gbpservice.contrib.nfp.configurator.lib import fw_constants as fw_const
from gbpservice.nfp.core import log as nfp_logging
LOG = nfp_logging.getLogger(__name__)
class RestApi(object):
""" Issues REST calls to the Service VMs
REST API wrapper class that provides POST method to
communicate with the Service VM.
"""
def __init__(self, timeout):
self.timeout = timeout
def request_type_to_api_map(self, url, data, request_type, headers):
return getattr(requests, request_type)(url,
data=data, timeout=self.timeout,
headers=headers)
def fire(self, url, data, request_type, headers):
""" Invokes REST POST call to the Service VM.
:param url: URL to connect.
:param data: data to be sent.
:param request_type: POST/PUT/DELETE
Returns: SUCCESS/Error message
"""
try:
msg = ("SENDING CURL request to URL: %s, request_type:%s, "
"vm with data %s"
% (url, request_type, data))
LOG.debug(msg)
resp = self.request_type_to_api_map(url, data,
request_type.lower(), headers)
except requests.exceptions.ConnectionError as err:
msg = ("Failed to establish connection to the service at URL: %r. "
"ERROR: %r" % (url, str(err).capitalize()))
return msg
except Exception as err:
msg = ("Failed to issue %r call "
"to service. URL: %r, Data: %r. Error: %r" %
(request_type.upper(), url, data, str(err).capitalize()))
return msg
try:
result = resp.json()
except ValueError as err:
msg = ("Unable to parse response, invalid JSON. URL: "
"%r. %r" % (url, str(err).capitalize()))
return msg
if resp.status_code not in common_const.SUCCESS_CODES or (
result.get('status') is False):
return result
return common_const.STATUS_SUCCESS
class FwGenericConfigDriver(base_driver.BaseDriver):
""" Implements device configuration requests.
Firewall generic configuration driver for handling device
configuration requests from Orchestrator.
"""
def __init__(self):
self.parse = data_parser.DataParser()
def _parse_vm_context(self, context):
try:
username = str(context['service_vm_context'][
'vyos']['username'])
password = str(context['service_vm_context'][
'vyos']['password'])
headers = {'Content-Type': 'application/json',
'username': username,
'password': password}
return headers
except Exception as e:
msg = ("Failed to get header from context. ERROR: %s" % e)
LOG.error(msg)
raise Exception(msg)
def configure_healthmonitor(self, context, resource_data):
vm_status = super(FwGenericConfigDriver, self).configure_healthmonitor(
context, resource_data)
if resource_data['nfds'][0]['periodicity'] == gen_cfg_const.INITIAL:
if vm_status == common_const.SUCCESS:
try:
resp = self.configure_user(context, resource_data)
if resp != common_const.STATUS_SUCCESS:
return common_const.FAILURE
except Exception as e:
msg = ("Failed to configure user. ERROR: %s" % e)
LOG.error(msg)
return common_const.FAILURE
return vm_status
def configure_user(self, context, resource_data):
headers = self._parse_vm_context(context)
resource_data = self.parse.parse_data(common_const.HEALTHMONITOR,
resource_data)
mgmt_ip = resource_data['mgmt_ip']
url = const.request_url % (mgmt_ip,
self.port,
'change_auth')
data = {}
LOG.info("Initiating POST request to configure Authentication "
"service at mgmt ip:%(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Change Auth POST request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.POST, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Configured user authentication successfully"
" for vyos service at %r." % mgmt_ip)
LOG.info(msg)
return resp
err_msg += (("Failed to change Authentication para Status code "
"Status code: %r, Reason: %r" %
(resp['status'], resp['reason']))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def _configure_static_ips(self, context, resource_data):
""" Configure static IPs for provider and stitching interfaces
of service VM.
Issues REST call to service VM for configuration of static IPs.
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
static_ips_info = dict(
provider_ip=resource_data.get('provider_ip'),
provider_cidr=resource_data.get('provider_cidr'),
provider_mac=resource_data.get('provider_mac'),
stitching_ip=resource_data.get('stitching_ip'),
stitching_cidr=resource_data.get('stitching_cidr'),
stitching_mac=resource_data.get('stitching_mac'))
mgmt_ip = resource_data['mgmt_ip']
url = const.request_url % (mgmt_ip,
self.port,
'add_static_ip')
data = jsonutils.dumps(static_ips_info)
LOG.info("Initiating POST request to add static IPs for primary "
"service at mgmt ip:%(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Static IP POST request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.POST, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Static IPs successfully added for service at %r." % url)
LOG.info(msg)
return resp
err_msg += (("Status code: %r, Reason: %r" %
(resp['status'], resp['reason']))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def configure_interfaces(self, context, resource_data):
""" Configure interfaces for the service VM.
Calls static IP configuration function and implements
persistent rule addition in the service VM.
Issues REST call to service VM for configuration of interfaces.
:param context: neutron context
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
resource_data = self.parse.parse_data(common_const.INTERFACES,
resource_data)
mgmt_ip = resource_data['mgmt_ip']
try:
result_log_forward = self._configure_log_forwarding(
const.request_url, mgmt_ip, self.port, headers)
except Exception as err:
msg = ("Failed to configure log forwarding for service at %s. "
"Error: %s" % (mgmt_ip, err))
LOG.error(msg)
else:
if result_log_forward == common_const.UNHANDLED:
pass
elif result_log_forward != common_const.STATUS_SUCCESS:
# Failure in log forward configuration won't break chain
# creation. However, error will be logged for detecting
# failure.
msg = ("Failed to configure log forwarding for service at %s."
" Error: %s" % (mgmt_ip, result_log_forward))
LOG.error(msg)
try:
result_static_ips = self._configure_static_ips(context,
resource_data)
except Exception as err:
msg = ("Failed to add static IPs. Error: %s" % err)
LOG.error(msg)
return msg
else:
if result_static_ips != common_const.STATUS_SUCCESS:
return result_static_ips
rule_info = dict(
provider_mac=resource_data['provider_mac'],
stitching_mac=resource_data['stitching_mac'])
url = const.request_url % (mgmt_ip,
self.port, 'add_rule')
data = jsonutils.dumps(rule_info)
LOG.info("Initiating POST request to add persistent rule to "
"primary service at mgmt ip: %(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Add persistent rule POST request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.POST, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Persistent rule successfully added for "
"service at %r." % url)
LOG.info(msg)
# wait for 10secs for the ip address to get configured. Sometimes
# observed that 'set_routes' fail with 'ip not configured' error.
time.sleep(10)
return resp
err_msg += (("Status code: %r" % resp['status'])
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def _clear_static_ips(self, context, resource_data):
""" Clear static IPs for provider and stitching
interfaces of the service VM.
Issues REST call to service VM for deletion of static IPs.
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
static_ips_info = dict(
provider_ip=resource_data.get('provider_ip'),
provider_cidr=resource_data.get('provider_cidr'),
provider_mac=resource_data.get('provider_mac'),
stitching_ip=resource_data.get('stitching_ip'),
stitching_cidr=resource_data.get('stitching_cidr'),
stitching_mac=resource_data.get('stitching_mac'))
mgmt_ip = resource_data['mgmt_ip']
url = const.request_url % (mgmt_ip,
self.port,
'del_static_ip')
data = jsonutils.dumps(static_ips_info)
LOG.info("Initiating POST request to remove static IPs for "
"primary service at mgmt ip: %(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Static IP DELETE request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.DELETE, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Static IPs successfully removed for service at %r." % url)
LOG.info(msg)
return resp
err_msg += (("Status code: %r, Reason: %r" %
(resp['status'], resp['reason']))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def clear_interfaces(self, context, resource_data):
""" Clear interfaces for the service VM.
Calls static IP clear function and implements
persistent rule deletion in the service VM.
Issues REST call to service VM for deletion of interfaces.
:param context: neutron context
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
resource_data = self.parse.parse_data(common_const.INTERFACES,
resource_data)
try:
result_static_ips = self._clear_static_ips(context, resource_data)
except Exception as err:
msg = ("Failed to remove static IPs. Error: %s" % err)
LOG.error(msg)
return msg
else:
if result_static_ips != common_const.STATUS_SUCCESS:
return result_static_ips
else:
LOG.info("Successfully removed static IPs. "
"Result: %(result_static_ips)s",
{'result_static_ips': result_static_ips})
rule_info = dict(
provider_mac=resource_data['provider_mac'],
stitching_mac=resource_data['stitching_mac'])
mgmt_ip = resource_data['mgmt_ip']
LOG.info("Initiating DELETE persistent rule for primary "
"service at mgmt ip: %(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
url = const.request_url % (mgmt_ip, self.port, 'delete_rule')
data = jsonutils.dumps(rule_info)
err_msg = ("Persistent rule DELETE request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.DELETE, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Persistent rules successfully deleted "
"for service at %r." % url)
LOG.info(msg)
return resp
err_msg += (("Status code: %r." % resp['status'])
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def configure_routes(self, context, resource_data):
""" Configure routes for the service VM.
Issues REST call to service VM for configuration of routes.
:param context: neutron context
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
forward_routes = resource_data.get('forward_route')
resource_data = self.parse.parse_data(common_const.ROUTES,
resource_data)
mgmt_ip = resource_data.get('mgmt_ip')
gateway_ip = resource_data.get('stitching_gw_ip')
# checking whether VPN service is present in the chain
# if yes, just configure the stitching pbr else
# configure both stitching and provider pbrs.
if not forward_routes:
source_cidrs = [resource_data.get('stitching_cidr')]
else:
source_cidrs = [resource_data.get('provider_cidr'),
resource_data.get('stitching_cidr')]
url = const.request_url % (mgmt_ip, self.port,
'add-source-route')
route_info = []
for source_cidr in source_cidrs:
route_info.append({'source_cidr': source_cidr,
'gateway_ip': gateway_ip})
data = jsonutils.dumps(route_info)
LOG.info("Initiating POST request to configure route of primary "
"service at mgmt ip: %(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Configure routes POST request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.POST, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Configured routes successfully for service at %r." % url)
LOG.info(msg)
return resp
err_msg += (("Status code: %r, Reason: %r" %
(resp['status'], resp['reason']))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
def clear_routes(self, context, resource_data):
""" Clear routes for the service VM.
Issues REST call to service VM for deletion of routes.
:param context: neutron context
:param resource_data: a dictionary of firewall rules and objects
send by neutron plugin
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context)
resource_data = self.parse.parse_data(common_const.ROUTES,
resource_data)
mgmt_ip = resource_data.get('mgmt_ip')
source_cidrs = [resource_data.get('provider_cidr'),
resource_data.get('stitching_cidr')]
url = const.request_url % (mgmt_ip, self.port,
'delete-source-route')
route_info = []
for source_cidr in source_cidrs:
route_info.append({'source_cidr': source_cidr})
data = jsonutils.dumps(route_info)
LOG.info("Initiating Delete route to primary "
"service at mgmt ip: %(mgmt_ip)s",
{'mgmt_ip': mgmt_ip})
err_msg = ("Routes DELETE request to the VyOS firewall "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.DELETE, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return err_msg
if resp is common_const.STATUS_SUCCESS:
msg = ("Routes successfully removed for service at %r." % url)
LOG.info(msg)
return resp
err_msg += (("Status code: %r, Reason: %r" %
(resp['status'], resp['reason']))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return err_msg
@base_driver.set_class_attr(SERVICE_TYPE=fw_const.SERVICE_TYPE,
SERVICE_VENDOR=const.VYOS)
class FwaasDriver(FwGenericConfigDriver):
""" Firewall as a service driver for handling firewall
service configuration requests.
We initialize service type in this class because agent loads
class object only for those driver classes that have service type
initialized. Also, only this driver class is exposed to the agent.
"""
def __init__(self, conf):
self.conf = conf
self.timeout = const.REST_TIMEOUT
self.rest_api = RestApi(self.timeout)
self.host = self.conf.host
self.port = const.CONFIGURATION_SERVER_PORT
super(FwaasDriver, self).__init__()
def create_firewall(self, context, firewall, host):
""" Implements firewall creation
Issues REST call to service VM for firewall creation
:param context: Neutron context
:param firewall: Firewall resource object from neutron fwaas plugin
:param host: Name of the host machine
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context['agent_info']['context'])
resource_data = self.parse.parse_data(common_const.FIREWALL, context)
LOG.info("Processing request 'Create Firewall' in FWaaS Driver "
"for Firewall ID: %(f_id)s",
{'f_id': firewall['id']})
mgmt_ip = resource_data.get('mgmt_ip')
url = const.request_url % (mgmt_ip,
self.port,
'configure-firewall-rule')
msg = ("Initiating POST request for FIREWALL ID: %r Tenant ID:"
" %r. URL: %s" % (firewall['id'], firewall['tenant_id'], url))
LOG.debug(msg)
data = jsonutils.dumps(firewall)
err_msg = ("Configure firewall POST request to the VyOS "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.POST, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return common_const.STATUS_ERROR
if resp is common_const.STATUS_SUCCESS:
LOG.info("Configured firewall successfully at URL: %(url)s ",
{'url': url})
return common_const.STATUS_ACTIVE
err_msg += (("Reason: %r, Response Content: %r" %
(resp.pop('message'), resp))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return common_const.STATUS_ERROR
def update_firewall(self, context, firewall, host):
""" Implements firewall updation
Issues REST call to service VM for firewall updation
:param context: Neutron context
:param firewall: Firewall resource object from neutron fwaas plugin
:param host: Name of the host machine
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context['agent_info']['context'])
LOG.info("Processing request 'Update Firewall' in FWaaS Driver "
"for Firewall ID:%(f_id)s",
{'f_id': firewall['id']})
resource_data = self.parse.parse_data(common_const.FIREWALL, context)
mgmt_ip = resource_data.get('mgmt_ip')
url = const.request_url % (mgmt_ip,
self.port,
'update-firewall-rule')
msg = ("Initiating UPDATE request. URL: %s" % url)
LOG.debug(msg)
data = jsonutils.dumps(firewall)
err_msg = ("Update firewall POST request to the VyOS "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.PUT, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return common_const.STATUS_ERROR
if resp is common_const.STATUS_SUCCESS:
msg = ("Updated firewall successfully for service at %r." % url)
LOG.debug(msg)
return common_const.STATUS_ACTIVE
err_msg += (("Reason: %r, Response Content: %r" %
(resp.pop('message'), resp))
if type(resp) is dict
else ("Reason: " + resp))
LOG.error(err_msg)
return common_const.STATUS_ERROR
def delete_firewall(self, context, firewall, host):
""" Implements firewall deletion
Issues REST call to service VM for firewall deletion
:param context: Neutron context
:param firewall: Firewall resource object from neutron fwaas plugin
:param host: Name of the host machine
Returns: SUCCESS/Failure message with reason.
"""
headers = self._parse_vm_context(context['agent_info']['context'])
LOG.info("Processing request 'Delete Firewall' in FWaaS Driver "
"for Firewall ID:%(f_id)s",
{'f_id': firewall['id']})
resource_data = self.parse.parse_data(common_const.FIREWALL, context)
mgmt_ip = resource_data.get('mgmt_ip')
url = const.request_url % (mgmt_ip,
self.port,
'delete-firewall-rule')
msg = ("Initiating DELETE request. URL: %s" % url)
LOG.info(msg)
data = jsonutils.dumps(firewall)
err_msg = ("Delete firewall POST request to the VyOS "
"service at %s failed. " % url)
try:
resp = self.rest_api.fire(url, data, common_const.DELETE, headers)
except Exception as err:
err_msg += ("Reason: %r" % str(err).capitalize())
LOG.error(err_msg)
return common_const.STATUS_SUCCESS
if resp is common_const.STATUS_SUCCESS:
msg = ("Deleted firewall successfully for service at %r." % url)
LOG.info(msg)
return common_const.STATUS_DELETED
if type(resp) is dict:
if not resp.get('delete_success') and (
resp.get('message') == const.INTERFACE_NOT_FOUND):
err_msg += ("Firewall was not deleted as interface was not "
"available in the firewall. It might have got "
"detached. So marking this delete as SUCCESS. "
"URL: %r, Response Content: %r" %
(url, resp.content))
LOG.error(err_msg)
return common_const.STATUS_SUCCESS
else:
err_msg += ("Response Content: %r" % resp)
else:
err_msg += ("Reason: " + resp)
LOG.error(err_msg)
msg = ("Firewall deletion has failed, but still sending"
"status as firewall deleted success from configurator")
LOG.info(msg)
return common_const.STATUS_DELETED