[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