pyfg-0.50/0000755000076500000240000000000013073164632013333 5ustar lindblomstaff00000000000000pyfg-0.50/MANIFEST.in0000644000076500000240000000003113066475157015074 0ustar lindblomstaff00000000000000include requirements.txt pyfg-0.50/PKG-INFO0000644000076500000240000000035113073164632014427 0ustar lindblomstaff00000000000000Metadata-Version: 1.0 Name: pyfg Version: 0.50 Summary: Python API for fortigate Home-page: https://github.com/spotify/pyfg Author: XNET Author-email: lindblom+pyfg@spotify.com License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN pyfg-0.50/pyFG/0000755000076500000240000000000013073164632014200 5ustar lindblomstaff00000000000000pyfg-0.50/pyFG/__init__.py0000644000076500000240000000016013066475157016317 0ustar lindblomstaff00000000000000from pyFG.fortios import FortiOS from pyFG.forticonfig import FortiConfig __all__ = ['FortiOS', 'FortiConfig'] pyfg-0.50/pyFG/ansible_helpers.py0000644000076500000240000000157613066475157017733 0ustar lindblomstaff00000000000000import 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) pyfg-0.50/pyFG/exceptions.py0000644000076500000240000000021213066475157016737 0ustar lindblomstaff00000000000000 class CommandExecutionException(Exception): pass class FailedCommit(Exception): pass class ForcedCommit(Exception): pass pyfg-0.50/pyFG/forticonfig.py0000644000076500000240000003172713066475157017106 0ustar lindblomstaff00000000000000from __future__ import unicode_literals import re from collections import OrderedDict from pyFG import py23_compat 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.items(): 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.items(): 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 isinstance(output, py23_compat.string_types): 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)) pyfg-0.50/pyFG/fortios.py0000644000076500000240000003600713073163705016245 0ustar lindblomstaff00000000000000# coding=utf-8 from __future__ import unicode_literals from pyFG.forticonfig import FortiConfig from pyFG import py23_compat from pyFG import exceptions import paramiko import re import os import io 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 # Set key exchange explcitly to address known fortinet issue paramiko.Transport._preferred_kex = ('diffie-hellman-group14-sha1', 'diffie-hellman-group-exchange-sha1', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group1-sha1', ) 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 io.open(user_config_file, 'rt', encoding='utf-8') as f: ssh_config.parse(f) 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() @staticmethod def _read_wrapper(data): """Ensure unicode always returned on read.""" # Paramiko (strangely) in PY3 returns an int here. if isinstance(data, int): data = chr(data) # Ensure unicode return py23_compat.text_type(data) 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 = error + self._read_wrapper(e) for o in output_chan.read(): output = output + self._read_wrapper(o) 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) pyfg-0.50/pyFG/py23_compat.py0000644000076500000240000000061513073163705016714 0ustar lindblomstaff00000000000000"""Simplify Python3 compatibility. Modeled after six behavior for small set of things""" from __future__ import print_function from __future__ import unicode_literals import sys PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 if sys.version_info[0] == 3: string_types = (str,) text_type = str else: string_types = (basestring,) # noqa text_type = unicode # noqa pyfg-0.50/pyfg.egg-info/0000755000076500000240000000000013073164632015772 5ustar lindblomstaff00000000000000pyfg-0.50/pyfg.egg-info/dependency_links.txt0000644000076500000240000000000113073164632022040 0ustar lindblomstaff00000000000000 pyfg-0.50/pyfg.egg-info/PKG-INFO0000644000076500000240000000035113073164632017066 0ustar lindblomstaff00000000000000Metadata-Version: 1.0 Name: pyfg Version: 0.50 Summary: Python API for fortigate Home-page: https://github.com/spotify/pyfg Author: XNET Author-email: lindblom+pyfg@spotify.com License: UNKNOWN Description: UNKNOWN Platform: UNKNOWN pyfg-0.50/pyfg.egg-info/requires.txt0000644000076500000240000000001113073164632020362 0ustar lindblomstaff00000000000000paramiko pyfg-0.50/pyfg.egg-info/SOURCES.txt0000644000076500000240000000045613073164632017663 0ustar lindblomstaff00000000000000MANIFEST.in requirements.txt setup.cfg setup.py pyFG/__init__.py pyFG/ansible_helpers.py pyFG/exceptions.py pyFG/forticonfig.py pyFG/fortios.py pyFG/py23_compat.py pyfg.egg-info/PKG-INFO pyfg.egg-info/SOURCES.txt pyfg.egg-info/dependency_links.txt pyfg.egg-info/requires.txt pyfg.egg-info/top_level.txtpyfg-0.50/pyfg.egg-info/top_level.txt0000644000076500000240000000000513073164632020517 0ustar lindblomstaff00000000000000pyFG pyfg-0.50/requirements.txt0000644000076500000240000000001013066475157016617 0ustar lindblomstaff00000000000000paramikopyfg-0.50/setup.cfg0000644000076500000240000000031413073164632015152 0ustar lindblomstaff00000000000000[pylama] linters = mccabe,pep8,pyflakes ignore = D203,C901,E501,E265,E122 skip = build/*,.tox/*,examples/* [pylama:pep8] max_line_length = 100 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyfg-0.50/setup.py0000644000076500000240000000122413073164373015046 0ustar lindblomstaff00000000000000import 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()) # reqs is a list of requirement # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] reqs = [str(ir.req) for ir in install_reqs] setup( name="pyfg", version="0.50", packages=find_packages(), author="XNET", author_email="lindblom+pyfg@spotify.com", description="Python API for fortigate", url="https://github.com/spotify/pyfg", include_package_data=True, install_requires=reqs )