[Python-modules-commits] [pyfg] 01/03: Imported Upstream version 0.47

Vincent Bernat bernat at moszumanska.debian.org
Sat May 28 13:33:50 UTC 2016


This is an automated email from the git hooks/post-receive script.

bernat pushed a commit to branch master
in repository pyfg.

commit 1e05821e37e31145c47fde134d2bb0e8c9969a88
Author: Vincent Bernat <bernat at debian.org>
Date:   Sat May 28 15:31:06 2016 +0200

    Imported Upstream version 0.47
---
 MANIFEST.in                        |   1 +
 PKG-INFO                           |  10 +
 pyFG/__init__.py                   |   2 +
 pyFG/ansible_helpers.py            |  35 ++++
 pyFG/exceptions.py                 |  11 ++
 pyFG/forticonfig.py                | 383 ++++++++++++++++++++++++++++++++++++
 pyFG/fortios.py                    | 392 +++++++++++++++++++++++++++++++++++++
 pyfg.egg-info/PKG-INFO             |  10 +
 pyfg.egg-info/SOURCES.txt          |  14 ++
 pyfg.egg-info/dependency_links.txt |   1 +
 pyfg.egg-info/pbr.json             |   1 +
 pyfg.egg-info/requires.txt         |   1 +
 pyfg.egg-info/top_level.txt        |   1 +
 requirements.txt                   |   1 +
 setup.cfg                          |   5 +
 setup.py                           |  23 +++
 16 files changed, 891 insertions(+)

diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..f9bd145
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include requirements.txt
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..c18fd87
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: pyfg
+Version: 0.47
+Summary: Python API for fortigate
+Home-page: https://github.com/spotify/pyfg
+Author: XNET
+Author-email: dbarroso at spotify.com
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/pyFG/__init__.py b/pyFG/__init__.py
new file mode 100644
index 0000000..8cc7c45
--- /dev/null
+++ b/pyFG/__init__.py
@@ -0,0 +1,2 @@
+from fortios import FortiOS
+from forticonfig import FortiConfig
diff --git a/pyFG/ansible_helpers.py b/pyFG/ansible_helpers.py
new file mode 100644
index 0000000..6d6c7a5
--- /dev/null
+++ b/pyFG/ansible_helpers.py
@@ -0,0 +1,35 @@
+import ast
+import logging
+
+
+def string_to_dict(string):
+    if string is not None:
+        try:
+            return ast.literal_eval(string)
+        except ValueError:
+            return string
+        except SyntaxError:
+            return string
+    else:
+        return None
+
+
+def set_logging(log_path, log_level):
+    formatter = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
+    if log_path is not None:
+        handler = logging.FileHandler(log_path)
+    else:
+        handler = logging.NullHandler()
+    handler.setFormatter(formatter)
+
+    logger = logging.getLogger()
+    logger.setLevel(logging.getLevelName(log_level))
+    logger.name = 'fortios:'
+    logger.addHandler(handler)
+    return logger
+
+
+def save_text_generator_to_file(file_name, text):
+    with open(file_name, "w") as text_file:
+        for line in text:
+            text_file.write('%s\n' % line)
\ No newline at end of file
diff --git a/pyFG/exceptions.py b/pyFG/exceptions.py
new file mode 100644
index 0000000..95bfc0d
--- /dev/null
+++ b/pyFG/exceptions.py
@@ -0,0 +1,11 @@
+
+class CommandExecutionException(Exception):
+    pass
+
+
+class FailedCommit(Exception):
+    pass
+
+
+class ForcedCommit(Exception):
+    pass
diff --git a/pyFG/forticonfig.py b/pyFG/forticonfig.py
new file mode 100644
index 0000000..486e31e
--- /dev/null
+++ b/pyFG/forticonfig.py
@@ -0,0 +1,383 @@
+import re
+from collections import OrderedDict
+
+
+class FortiConfig(object):
+    def __init__(self, name='', config_type='', parent=None, vdom=None):
+        """
+        This object represents a block of config. For example::
+
+            config system interface
+                edit "port1"
+                    set vdom "root"
+                    set mode dhcp
+                    set allowaccess ping
+                    set type physical
+                    set snmp-index 1
+                next
+            end
+
+
+        It can contain parameters and sub_blocks.
+
+        Args:
+            * **name** (string) -- The path for the current block, for example *system interface*
+            * **config_type** (string) -- The type of block, it can either be *config" or *edit*.
+            * **parent** (string) -- If you are creating a subblock you can specify the parent block here.
+            * **vdom** (string) -- If this block belongs to a vdom you have to specify it. This has to be specified\
+                only in the root blocks. For example, on the 'system interface' block. You don't have to specify it\
+                on the *port1* block.
+        """
+        self.name = name
+        self.config_type = config_type
+        self.parent = parent
+        self.vdom = vdom
+        self.paths = list()
+
+        if config_type == 'edit':
+            self.rel_path_fwd = 'edit %s\n' % name
+            self.rel_path_bwd = 'next\n'
+        elif config_type == 'config':
+            self.rel_path_fwd = 'config %s\n' % name
+            self.rel_path_bwd = 'end\n'
+
+        if self.parent is None:
+            self.rel_path_fwd = ''
+            self.rel_path_bwd = ''
+            self.full_path_fwd = self.rel_path_fwd
+            self.full_path_bwd = self.rel_path_bwd
+        else:
+            self.full_path_fwd = '%s%s' % (self.get_parent().full_path_fwd, self.rel_path_fwd)
+            self.full_path_bwd = '%s%s' % (self.rel_path_bwd, self.get_parent().full_path_bwd)
+
+        self.sub_blocks = OrderedDict()
+        self.new_sub_blocks = OrderedDict()
+        self.parameters = dict()
+        self.new_parameters = dict()
+
+    def __repr__(self):
+        return 'Config Block: %s' % self.get_name()
+
+    def __str__(self):
+        return '%s %s' % (self.config_type, self.name)
+
+    def __getitem__(self, item):
+        """
+        By overriding this method we can access sub blocks of config easily. For example:
+
+        config['router bgp']['neighbor']['10.1.1.1']
+
+        """
+        return self.sub_blocks[item]
+
+    def __setitem__(self, key, value):
+        """
+        By overriding this method we can set sub blocks of config easily. For example:
+
+        config['router bgp']['neighbor']['10.1.1.1'] = neighbor_sub_block
+        """
+        self.sub_blocks[key] = value
+        value.set_parent(self)
+
+    def get_name(self):
+        """
+
+        Returns:
+            The name of the object.
+        """
+        return self.name
+
+    def set_name(self, name):
+        """
+        Sets the name of the object.
+
+        Args:
+            * **name** (string) - The name you want for the object.
+        """
+        self.name = name
+
+    def compare_config(self, target, init=True, indent_level=0):
+        """
+        This method will return all the necessary commands to get from the config we are in to the target
+        config.
+
+        Args:
+            * **target** (:class:`~pyFG.forticonfig.FortiConfig`) - Target config.
+            * **init** (bool) - This tells to the method if this is the first call to the method or if we are inside\
+                                the recursion. You can ignore this parameter.
+            * **indent_level** (int) - This tells the method how deep you are in the recursion. You can ignore it.
+
+        Returns:
+            A string containing all the necessary commands to reach the target config.
+        """
+
+        if init:
+            fwd = self.full_path_fwd
+            bwd = self.full_path_bwd
+        else:
+            fwd = self.rel_path_fwd
+            bwd = self.rel_path_bwd
+
+        indent = 4*indent_level*' '
+
+        if indent_level == 0 and self.vdom is not None:
+            if self.vdom == 'global':
+                pre = 'conf global\n'
+            else:
+                pre = 'conf vdom\n  edit %s\n' % self.vdom
+            post = 'end'
+        else:
+            pre = ''
+            post = ''
+
+        pre_block = '%s%s' % (indent, fwd)
+        post_block = '%s%s' % (indent, bwd)
+
+        my_params = self.parameters.keys()
+        ot_params = target.parameters.keys()
+
+        text = ''
+
+        for param in my_params:
+            if param not in ot_params:
+                text += '  %sunset %s\n' % (indent, param)
+            else:
+                # We ignore quotes when comparing values
+                if str(self.get_param(param)).replace('"', '') != str(target.get_param(param)).replace('"', ''):
+                    text += '  %sset %s %s\n' % (indent, param, target.get_param(param))
+
+        for param in ot_params:
+            if param not in my_params:
+                text += '  %sset %s %s\n' % (indent, param, target.get_param(param))
+
+        my_blocks = self.sub_blocks.keys()
+        ot_blocks = target.sub_blocks.keys()
+
+        for block_name in my_blocks:
+            if block_name not in ot_blocks:
+                text += "    %sdelete %s\n" % (indent, block_name)
+            else:
+                text += self[block_name].compare_config(target[block_name], False, indent_level+1)
+
+        for block_name in ot_blocks:
+            if block_name not in my_blocks:
+                text += target[block_name].to_text(True, indent_level+1, True)
+
+        if text == '':
+            return ''
+        else:
+            return '%s%s%s%s%s' % (pre, pre_block, text, post_block, post)
+
+    def iterparams(self):
+        """
+        Allows you to iterate over the parameters of the block. For example:
+
+        >>> conf = FortiConfig('router bgp')
+        >>> conf.parse_config_output('here comes a srting with the config of a device')
+        >>> for p_name, p_value in conf['router bgp']['neighbor']['172.20.213.23']:
+        ...     print p_name, p_value
+        remote_as 65101
+        route-map-in "filter_inbound"
+        route-map-out "filter_outbound"
+
+        Yields:
+            parameter_name, parameter_value
+        """
+        for key, value in self.parameters.iteritems():
+            yield key, value
+
+    def iterblocks(self):
+        """
+        Allows you to iterate over the sub_blocks of the block. For example:
+
+        >>> conf = FortiConfig('router bgp')
+        >>> conf.parse_config_output('here comes a srting with the config of a device')
+        >>> for b_name, b_value in conf['router bgp']['neighbor'].iterblocks():
+        ...     print b_name, b_value
+        ...
+        172.20.213.23 edit 172.20.213.23
+        2.2.2.2 edit 2.2.2.2
+
+        Yields:
+            sub_block_name, sub_block_value
+        """
+        for key, data in self.sub_blocks.iteritems():
+            yield key, data
+
+    def get_parameter_names(self):
+        """
+        Returns:
+            A list of strings. Each string is the name of a parameter for that block.
+        """
+        return self.parameters.keys()
+
+    def get_block_names(self):
+        """
+        Returns:
+            A list of strings. Each string is the name of a sub_block for that block.
+        """
+        return self.sub_blocks.keys()
+
+    def set_parent(self, parent):
+        """
+        Args:
+            - **parent** ((:class:`~pyFG.forticonfig.FortiConfig`): FortiConfig object you want to set as parent.
+        """
+        self.parent = parent
+
+        if self.config_type == 'edit':
+            self.rel_path_fwd = 'edit %s\n' % self.get_name()
+            self.rel_path_bwd = 'next\n'
+        elif self.config_type == 'config':
+            self.rel_path_fwd = 'config %s\n' % self.get_name()
+            self.rel_path_bwd = 'end\n'
+
+        self.full_path_fwd = '%s%s' % (self.get_parent().full_path_fwd, self.rel_path_fwd)
+        self.full_path_bwd = '%s%s' % (self.rel_path_bwd, self.get_parent().full_path_bwd)
+
+    def get_parent(self):
+        """
+        Returns:
+            (:class:`~pyFG.forticonfig.FortiConfig`) object that is assigned as parent
+        """
+        return self.parent
+
+    def get_param(self, param):
+        """
+        Args:
+            - **param** (string): Parameter name you want to get
+        Returns:
+            Parameter value
+        """
+        try:
+            return self.parameters[param]
+        except KeyError:
+            return None
+
+    def set_param(self, param, value):
+        """
+        When setting a parameter it is important that you don't forget the quotes if they are needed. For example,
+        if you are setting a comment.
+
+        Args:
+            - **param** (string): Parameter name you want to set
+            - **value** (string): Value you want to set
+        """
+
+        self.parameters[param] = str(value)
+
+    def del_param(self, param):
+        """
+        Args:
+            - **param** (string): Parameter name you want to delete
+        """
+        self.parameters.pop(param, None)
+
+    def get_paths(self):
+        """
+        Returns:
+            All the queries that were needed to get to this model of the config. This is useful in case
+            you want to reload the running config.
+        """
+        if len(self.paths) > 0:
+            return self.paths
+        else:
+            return self.get_block_names()
+
+    def add_path(self, path):
+        """
+        Args:
+            - **path** (string) - The path you want to set, for example 'system interfaces' or 'router bgp'.
+        """
+        self.paths.append(path)
+
+    def del_block(self, block_name):
+        """
+        Args:
+            - **block_name** (string): Sub_block name you want to delete
+        """
+        self.sub_blocks.pop(block_name, None)
+
+    def to_text(self, relative=False, indent_level=0, clean_empty_block=False):
+        """
+        This method returns the object model in text format. You should be able to copy&paste this text into any
+        device running a supported version of FortiOS.
+
+        Args:
+            - **relative** (bool):
+                * If ``True``  the text returned will assume that you are one block away
+                * If ``False`` the text returned will contain instructions to reach the block from the root.
+            - **indent_level** (int): This value is for aesthetics only. It will help format the text in blocks to\
+                increase readability.
+            - **clean_empty_block** (bool):
+                * If ``True`` a block without parameters or with sub_blocks without parameters will return an empty\
+                    string
+                * If ``False`` a block without parameters will still return how to create it.
+        """
+        if relative:
+            fwd = self.rel_path_fwd
+            bwd = self.rel_path_bwd
+        else:
+            fwd = self.full_path_fwd
+            bwd = self.full_path_bwd
+
+        indent = 4*indent_level*' '
+        pre = '%s%s' % (indent, fwd)
+        post = '%s%s' % (indent, bwd)
+
+        text = ''
+        for param, value in self.iterparams():
+            text += '  %sset %s %s\n' % (indent, param, value)
+
+        for key, block in self.iterblocks():
+            text += block.to_text(True, indent_level+1)
+
+        if len(text) > 0 or not clean_empty_block:
+            text = '%s%s%s' % (pre, text, post)
+        return text
+
+    def parse_config_output(self, output):
+        """
+        This method will parse a string containing FortiOS config and will load it into the current
+        :class:`~pyFG.forticonfig.FortiConfig` object.
+
+        Args:
+            - **output** (string) - A string containing a supported version of FortiOS config
+        """
+        regexp = re.compile('^(config |edit |set |end$|next$)(.*)')
+        current_block = self
+
+        if output.__class__ is str or output.__class__ is unicode:
+            output = output.splitlines()
+
+        for line in output:
+            if 'uuid' in line:
+                continue
+            if 'snmp-index' in line:
+                continue
+            line = line.strip()
+            result = regexp.match(line)
+
+            if result is not None:
+                action = result.group(1).strip()
+                data = result.group(2).strip()
+
+
+                if action == 'config' or action == 'edit':
+
+                    data = data.replace('"', '')
+
+                    if data not in current_block.get_block_names():
+                        config_block = FortiConfig(data, action, current_block)
+                        current_block[data] = config_block
+                    else:
+                        config_block = current_block[data]
+
+                    current_block = config_block
+                elif action == 'end' or action == 'next':
+                    current_block = current_block.get_parent()
+                elif action == 'set':
+                    split_data = data.split(' ')
+                    parameter = split_data[0]
+                    data = split_data[1:]
+                    current_block.set_param(parameter, ' '.join(data))
diff --git a/pyFG/fortios.py b/pyFG/fortios.py
new file mode 100644
index 0000000..8fe40be
--- /dev/null
+++ b/pyFG/fortios.py
@@ -0,0 +1,392 @@
+# coding=utf-8
+
+from forticonfig import FortiConfig
+
+import exceptions
+import paramiko
+import StringIO
+import re
+import os
+from difflib import Differ
+
+import logging
+
+logger = logging.getLogger('pyFG')
+
+
+class FortiOS(object):
+
+    def __init__(self, hostname, vdom=None, username=None, password=None, keyfile=None, timeout=60):
+        """
+        Represents a device running FortiOS.
+
+        A :py:class:`FortiOS` object has three different :class:`~pyFG.forticonfig.FortiConfig` objects:
+
+        * **running_config** -- You can populate this from the device or from a file with the\
+            :func:`~pyFG.fortios.FortiOS.load_config` method. This will represent the live config\
+            of the device and shall not be modified by any means as that might break other methods as the \
+            :func:`~pyFG.fortios.FortiOS.commit`
+        * **candidate_config** -- You can populate this using the same mechanisms as you would populate the\
+            running_config. This represents the config you want to reach so, if you want to apply\
+            changes, here is where you would apply them.
+        * **original_config** -- This is automatically populated when you do a commit with the original config\
+            prior to the commit. This is useful for the :func:`~pyFG.fortios.FortiOS.rollback` operation or for\
+            checking stuff later on.
+
+        Args:
+            * **hostname** (str) -- FQDN or IP of the device you want to connect.
+            * **vdom** (str) -- VDOM you want to connect to. If it is None we will run the commands without moving\
+                to a VDOM.
+            * **username** (str) -- Username to connect to the device. If none is specified the current user will be\
+                used
+            * **password** (str) -- Username password
+            * **keyfile** (str) -- Path to the private key in case you want to use this authentication method.
+            * **timeout** (int) -- Time in seconds to wait for the device to respond.
+
+        """
+        self.hostname = hostname
+        self.vdom = vdom
+        self.original_config = None
+        self.running_config = FortiConfig('running', vdom=vdom)
+        self.candidate_config = FortiConfig('candidate', vdom=vdom)
+        self.ssh = None
+        self.username = username
+        self.password = password
+        self.keyfile = keyfile
+        self.timeout = timeout
+
+    def open(self):
+        """
+        Opens the ssh session with the device.
+        """
+
+        logger.debug('Connecting to device %s, vdom %s' % (self.hostname, self.vdom))
+
+        self.ssh = paramiko.SSHClient()
+        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+
+        cfg = {
+          'hostname': self.hostname, 
+          'timeout': self.timeout,
+          'username': self.username,
+          'password': self.password,
+          'key_filename': self.keyfile
+        }
+
+        if os.path.exists(os.path.expanduser("~/.ssh/config")):
+          ssh_config = paramiko.SSHConfig()
+          user_config_file = os.path.expanduser("~/.ssh/config")
+          with open(user_config_file) as f:
+            ssh_config.parse(f)
+          f.close()
+
+          host_conf = ssh_config.lookup(self.hostname)
+          if host_conf:
+            if 'proxycommand' in host_conf:
+              cfg['sock'] = paramiko.ProxyCommand(host_conf['proxycommand'])
+            if 'user' in host_conf:
+              cfg['username'] = host_conf['user']
+            if 'identityfile' in host_conf:
+              cfg['key_filename'] = host_conf['identityfile']
+            if 'hostname' in host_conf:
+              cfg['hostname'] = host_conf['hostname']
+
+        self.ssh.connect(**cfg)
+
+    def close(self):
+        """
+        Closes the ssh session with the device.
+        """
+
+        logger.debug('Closing connection to device %s' % self.hostname)
+        self.ssh.close()
+
+    def execute_command(self, command):
+        """
+        This method will execute the commands on the device without as if you were just connected to it (it will not
+        enter into any vdom). This method is not recommended unless you are 100% sure of what you are doing.
+
+
+        Args:
+            * **command** (str) -- Command to execute.
+
+        Returns:
+            A list of strings containing the output.
+
+        Raises:
+            exceptions.CommandExecutionException -- If it detects any problem with the command.
+        """
+        logger.debug('Executing commands:\n %s' % command)
+
+        err_msg = 'Something happened when executing some commands on device'
+
+        chan = self.ssh.get_transport().open_session()
+        chan.settimeout(5)
+
+        chan.exec_command(command)
+
+        error_chan = chan.makefile_stderr()
+        output_chan = chan.makefile()
+
+        error = ''
+        output = ''
+
+        for e in error_chan.read():
+            error += e
+
+        for o in output_chan.read():
+            output += o
+
+        '''
+        output = StringIO.StringIO()
+        error = StringIO.StringIO()
+
+        while not chan.exit_status_ready():
+            if chan.recv_stderr_ready():
+                data_err = chan.recv_stderr(1024)
+                while data_err:
+                    error.write(data_err)
+                    data_err = chan.recv_stderr(1024)
+
+            if chan.recv_ready():
+                data = chan.recv(256)
+                while data:
+                    output.write(data)
+                    data = chan.recv(256)
+
+        output = output.getvalue()
+        error = error.getvalue()
+        '''
+
+        if len(error) > 0:
+            msg = '%s %s:\n%s\n%s' % (err_msg, self.ssh.get_host_keys().keys()[0], command, error)
+            logger.error(msg)
+            raise exceptions.CommandExecutionException(msg)
+
+        regex = re.compile('Command fail')
+        if len(regex.findall(output)) > 0:
+            msg = '%s %s:\n%s\n%s'% (err_msg, self.ssh.get_host_keys().keys()[0], command, output)
+            logger.error(msg)
+            raise exceptions.CommandExecutionException(msg)
+
+        output = output.splitlines()
+
+        # We look for the prompt and remove it
+        i = 0
+        for line in output:
+            current_line = line.split('#')
+
+            if len(current_line) > 1:
+                output[i] = current_line[1]
+            else:
+                output[i] = current_line[0]
+            i += 1
+
+        return output[:-1]
+
+    def load_config(self, path='', in_candidate=False, empty_candidate=False, config_text=None):
+        """
+        This method will load a block of config represented as a :py:class:`FortiConfig` object in the running
+        config, in the candidate config or in both.
+
+        Args:
+            * **path** (str) -- This is the block of config you want to load. For example *system interface*\
+                or *router bgp*
+            * **in_candidate** (bool):
+                * If ``True`` the config will be loaded as *candidate*
+                * If ``False`` the config will be loaded as *running*
+            * **empty_candidate** (bool):
+                * If ``True`` the *candidate* config will be left unmodified.
+                * If ``False`` the *candidate* config will be loaded with a block of config containing\
+                the same information as the config loaded in the *running* config.
+            * **config_text** (str) -- Instead of loading the config from the device itself (using the ``path``\
+                variable, you can specify here the config as text.
+        """
+        logger.info('Loading config. path:%s, in_candidate:%s, empty_candidate:%s, config_text:%s' % (
+            path, in_candidate, empty_candidate, config_text is not None))
+
+        if config_text is None:
+            if self.vdom is not None:
+                if self.vdom == 'global':
+                    command = 'conf global\nshow %s\nend' % path
+                else:
+                    command = 'conf vdom\nedit %s\nshow %s\nend' % (self.vdom, path)
+            else:
+                command = 'show %s' % path
+
+            config_text = self.execute_command(command)
+
+        if not in_candidate:
+            self.running_config.parse_config_output(config_text)
+            self.running_config.add_path(path)
+
+        if not empty_candidate or in_candidate:
+            self.candidate_config.parse_config_output(config_text)
+            self.candidate_config.add_path(path)
+
+    def compare_config(self, other=None, text=False):
+        """
+        Compares running config with another config. This other config can be either the *running*
+        config or a :class:`~pyFG.forticonfig.FortiConfig`. The result of the comparison will be how to reach\
+        the state represented in the target config (either the *candidate* or *other*) from the *running*\
+        config.
+
+        Args:
+            * **other** (:class:`~pyFG.forticonfig.FortiConfig`) -- This parameter, if specified, will be used for the\
+                comparison. If it is not specified the candidate config will be used.
+            * **text** (bool):
+                * If ``True`` this method will return a text diff showing how to get from the running config to\
+                    the target config.
+                * If ``False`` this method will return all the exact commands that needs to be run on the running\
+                    config to reach the target config.
+
+        Returns:
+            See the explanation of the *text* arg in the section Args.
+
+        """
+        if other is None:
+            other = self.candidate_config
+
+        if not text:
+            return self.running_config.compare_config(other)
+        else:
+            diff = Differ()
+            result = diff.compare(
+                self.running_config.to_text().splitlines(),
+                other.to_text().splitlines()
+            )
+            return '\n'.join(result)
+
+    def commit(self, config_text=None, force=False):
+        """
+        This method will push some config changes to the device. If the commit is successful the running
+        config will be updated from the device and the previous config will be stored in the
+        original config. The candidate config will not be updated. If the commit was successful it should
+        match the running config. If it was not successful it will most certainly be different.
+
+        Args:
+            * **config_text** (string) -- If specified these are the config changes that will be applied. If you\
+                don't specify this parameter it will execute all necessary commands to reach the candidate_config from\
+                the running config.
+            * **force(bool)**:
+                * If ``True`` the new config will be pushed in *best effort*, errors will be ignored.
+                * If ``False`` a rollback will be triggered if an error is detected
+
+        Raises:
+            * :class:`~pyFG.exceptions.FailedCommit` -- Something failed but we could rollback our changes
+            * :class:`~pyFG.exceptions.ForcedCommit` -- Something failed but we avoided any rollback
+        """
+        self._commit(config_text, force)
+
+    def _commit(self, config_text=None, force=False, reload_original_config=True):
+        """
+        This method is the same as the :py:method:`commit`: method, however, it has an extra command that will trigger
+        the reload of the running config. The reason behind this is that in some circumstances you don´ want
+        to reload the running config, for example, when doing a rollback.
+
+        See :py:method:`commit`: for more details.
+        """
+        def _execute(config_text):
+            if config_text is None:
+                config_text = self.compare_config()
+
+            if self.vdom is None:
+                pre = ''
+            else:
+                pre = 'conf global\n    '
+
+            cmd = '%sexecute batch start\n' % pre
+            cmd += config_text
+            cmd += '\nexecute batch end\n'
+
+            self.execute_command(cmd)
+            last_log = self.execute_command('%sexecute batch lastlog' % pre)
+
+            return self._parse_batch_lastlog(last_log)
+
+        logger.info('Committing config ')
+
+        wrong_commands = _execute(config_text)
+
+        self._reload_config(reload_original_config)
+
+        retry_codes = [-3, -23]
+        retries = 5
+        while retries > 0:
+            retries -= 1
+            for wc in wrong_commands:
+                if int(wc[0]) in retry_codes:
+                    if config_text is None:
+                        config_text = self.compare_config()
+                    wrong_commands = _execute(config_text)
+                    self._reload_config(reload_original_config=False)
+                    break
+
+        if len(wrong_commands) > 0:
+            exit_code = -2
+            logging.debug('List of commands that failed: %s' % wrong_commands)
+
+            if not force:
+                exit_code = -1
+                self.rollback()
+
+            if exit_code < 0 :
+                raise exceptions.FailedCommit(wrong_commands)
+
+    def rollback(self):
+        """
+        It will rollback all changes and go to the *original_config*
+        """
+        logger.info('Rolling back changes')
+
+        config_text = self.compare_config(other=self.original_config)
+
+        if len(config_text) > 0:
+            return self._commit(config_text, force=True, reload_original_config=False)
+
+    @staticmethod
+    def _parse_batch_lastlog(last_log):
+        """
+        This static method will help reading the result of the commit, command by command.
+
+        Args:
+            last_log(list): A list containing, line by line, the result of committing the changes.
+
+        Returns:
+            A list of tuples that went wrong. The tuple will contain (*status_code*, *command*)
+        """
+        regexp = re.compile('(-?[0-9]\d*):\W+(.*)')
+
+        wrong_commands = list()
+        for line in last_log:
+            result = regexp.match(line)
+            if result is not None:
+                status_code = result.group(1)
+                command = result.group(2)
+
+                if int(status_code) < 0:
+                    wrong_commands.append((status_code, command))
+
+        return wrong_commands
+
+    def _reload_config(self, reload_original_config):
+        """
+        This command will update the running config from the live device.
+
+        Args:
+            * reload_original_config:
+                * If ``True`` the original config will be loaded with the running config before reloading the\
+                original config.
+                * If ``False`` the original config will remain untouched.
+        """
+        # We don't want to reload the config under some circumstances
+        if reload_original_config:
+            self.original_config = self.running_config
+            self.original_config.set_name('original')
+
+        paths = self.running_config.get_paths()
+
+        self.running_config = FortiConfig('running', vdom=self.vdom)
+
+        for path in paths:
+            self.load_config(path, empty_candidate=True)
diff --git a/pyfg.egg-info/PKG-INFO b/pyfg.egg-info/PKG-INFO
new file mode 100644
index 0000000..c18fd87
--- /dev/null
+++ b/pyfg.egg-info/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: pyfg
+Version: 0.47
+Summary: Python API for fortigate
+Home-page: https://github.com/spotify/pyfg
+Author: XNET
+Author-email: dbarroso at spotify.com
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/pyfg.egg-info/SOURCES.txt b/pyfg.egg-info/SOURCES.txt
new file mode 100644
index 0000000..3832d98
--- /dev/null
+++ b/pyfg.egg-info/SOURCES.txt
@@ -0,0 +1,14 @@
+MANIFEST.in
+requirements.txt
+setup.py
+pyFG/__init__.py
+pyFG/ansible_helpers.py
+pyFG/exceptions.py
+pyFG/forticonfig.py
+pyFG/fortios.py
+pyfg.egg-info/PKG-INFO
+pyfg.egg-info/SOURCES.txt
+pyfg.egg-info/dependency_links.txt
+pyfg.egg-info/pbr.json
+pyfg.egg-info/requires.txt
+pyfg.egg-info/top_level.txt
\ No newline at end of file
diff --git a/pyfg.egg-info/dependency_links.txt b/pyfg.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pyfg.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/pyfg.egg-info/pbr.json b/pyfg.egg-info/pbr.json
new file mode 100644
index 0000000..afeb5e5
--- /dev/null
+++ b/pyfg.egg-info/pbr.json
@@ -0,0 +1 @@
+{"is_release": true, "git_version": "c652adc"}
\ No newline at end of file
diff --git a/pyfg.egg-info/requires.txt b/pyfg.egg-info/requires.txt
new file mode 100644
index 0000000..8608c1b
--- /dev/null
+++ b/pyfg.egg-info/requires.txt
@@ -0,0 +1 @@
+paramiko
diff --git a/pyfg.egg-info/top_level.txt b/pyfg.egg-info/top_level.txt
new file mode 100644
index 0000000..f6341c7
--- /dev/null
+++ b/pyfg.egg-info/top_level.txt
@@ -0,0 +1 @@
+pyFG
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..aa35c71
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+paramiko
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..861a9f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..1b55700
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,23 @@
+import uuid
+
+from setuptools import setup, find_packages
+from pip.req import parse_requirements
+
+# parse_requirements() returns generator of pip.req.InstallRequirement objects
+install_reqs = parse_requirements('requirements.txt', session=uuid.uuid1())
+
... 15 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/pyfg.git



More information about the Python-modules-commits mailing list