diff --git a/lib/ansible/modules/network/f5/bigip_config.py b/lib/ansible/modules/network/f5/bigip_config.py
new file mode 100644
index 0000000000..240bdc93d8
--- /dev/null
+++ b/lib/ansible/modules/network/f5/bigip_config.py
@@ -0,0 +1,374 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright 2017 F5 Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+ANSIBLE_METADATA = {
+ 'status': ['preview'],
+ 'supported_by': 'community',
+ 'metadata_version': '1.0'
+}
+
+DOCUMENTATION = '''
+---
+module: bigip_config
+short_description: Manage BIG-IP configuration sections.
+description:
+ - Manages a BIG-IP configuration by allowing TMSH commands that
+ modify running configuration, or merge SCF formatted files into
+ the running configuration. Additionally, this module is of
+ significant importance because it allows you to save your running
+ configuration to disk. Since the F5 module only manipulate running
+ configuration, it is important that you utilize this module to save
+ that running config.
+version_added: "2.4"
+options:
+ save:
+ description:
+ - The C(save) argument instructs the module to save the
+ running-config to startup-config. This operation is performed
+ after any changes are made to the current running config. If
+ no changes are made, the configuration is still saved to the
+ startup config. This option will always cause the module to
+ return changed.
+ choices:
+ - yes
+ - no
+ default: no
+ reset:
+ description:
+ - Loads the default configuration on the device. If this option
+ is specified, the default configuration will be loaded before
+ any commands or other provided configuration is run.
+ choices:
+ - yes
+ - no
+ default: no
+ merge_content:
+ description:
+ - Loads the specified configuration that you want to merge into
+ the running configuration. This is equivalent to using the
+ C(tmsh) command C(load sys config from-terminal merge). If
+ you need to read configuration from a file or template, use
+ Ansible's C(file) or C(template) lookup plugins respectively.
+ verify:
+ description:
+ - Validates the specified configuration to see whether they are
+ valid to replace the running configuration. The running
+ configuration will not be changed.
+ choices:
+ - yes
+ - no
+ default: yes
+notes:
+ - Requires the f5-sdk Python package on the host. This is as easy as pip
+ install f5-sdk.
+requirements:
+ - f5-sdk >= 2.2.3
+extends_documentation_fragment: f5
+author:
+ - Tim Rupp (@caphrim007)
+'''
+
+EXAMPLES = '''
+- name: Save the running configuration of the BIG-IP
+ bigip_config:
+ save: yes
+ server: "lb.mydomain.com"
+ password: "secret"
+ user: "admin"
+ validate_certs: "no"
+ delegate_to: localhost
+
+- name: Reset the BIG-IP configuration, for example, to RMA the device
+ bigip_config:
+ reset: yes
+ save: yes
+ server: "lb.mydomain.com"
+ password: "secret"
+ user: "admin"
+ validate_certs: "no"
+ delegate_to: localhost
+
+- name: Load an SCF configuration
+ bigip_config:
+ merge_content: "{{ lookup('file', '/path/to/config.scf') }}"
+ server: "lb.mydomain.com"
+ password: "secret"
+ user: "admin"
+ validate_certs: "no"
+ delegate_to: localhost
+'''
+
+RETURN = '''
+stdout:
+ description: The set of responses from the options
+ returned: always
+ type: list
+ sample: ['...', '...']
+
+stdout_lines:
+ description: The value of stdout split into a list
+ returned: always
+ type: list
+ sample: [['...', '...'], ['...'], ['...']]
+'''
+
+import os
+import tempfile
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+from ansible.module_utils.f5_utils import (
+ AnsibleF5Client,
+ AnsibleF5Parameters,
+ HAS_F5SDK,
+ F5ModuleError,
+ iControlUnexpectedHTTPError,
+ iteritems,
+ defaultdict
+)
+from ansible.module_utils.basic import BOOLEANS
+
+
+class Parameters(AnsibleF5Parameters):
+ returnables = ['stdout', 'stdout_lines']
+
+ def __init__(self, params=None):
+ self._values = defaultdict(lambda: None)
+ if params:
+ self.update(params)
+
+ def to_return(self):
+ result = {}
+ for returnable in self.returnables:
+ result[returnable] = getattr(self, returnable)
+ result = self._filter_params(result)
+ return result
+
+ def update(self, params=None):
+ if params:
+ for k, v in iteritems(params):
+ if self.api_map is not None and k in self.api_map:
+ map_key = self.api_map[k]
+ else:
+ map_key = k
+
+ # Handle weird API parameters like `dns.proxy.__iter__` by
+ # using a map provided by the module developer
+ class_attr = getattr(type(self), map_key, None)
+ if isinstance(class_attr, property):
+ # There is a mapped value for the api_map key
+ if class_attr.fset is None:
+ # If the mapped value does not have an associated setter
+ self._values[map_key] = v
+ else:
+ # The mapped value has a setter
+ setattr(self, map_key, v)
+ else:
+ # If the mapped value is not a @property
+ self._values[map_key] = v
+
+
+class ModuleManager(object):
+ def __init__(self, client):
+ self.client = client
+ self.want = Parameters(self.client.module.params)
+ self.changes = Parameters()
+
+ def _set_changed_options(self):
+ changed = {}
+ for key in Parameters.returnables:
+ if getattr(self.want, key) is not None:
+ changed[key] = getattr(self.want, key)
+ if changed:
+ self.changes = Parameters(changed)
+
+ def _to_lines(self, stdout):
+ lines = list()
+ for item in stdout:
+ if isinstance(item, str):
+ item = str(item).split('\n')
+ lines.append(item)
+ return lines
+
+ def exec_module(self):
+ result = dict()
+
+ try:
+ self.execute()
+ except iControlUnexpectedHTTPError as e:
+ raise F5ModuleError(str(e))
+
+ result.update(**self.changes.to_return())
+ result.update(dict(changed=True))
+ return result
+
+ def execute(self):
+ responses = []
+ if self.want.reset:
+ response = self.reset()
+ responses.append(response)
+
+ if self.want.merge_content:
+ if self.want.verify:
+ response = self.merge(verify=True)
+ responses.append(response)
+ else:
+ response = self.merge(verify=False)
+ responses.append(response)
+
+ if self.want.save:
+ response = self.save()
+ responses.append(response)
+
+ self.changes = Parameters({
+ 'stdout': responses,
+ 'stdout_lines': self._to_lines(responses)
+ })
+
+ def reset(self):
+ if self.client.check_mode:
+ return True
+ return self.reset_device()
+
+ def reset_device(self):
+ command = 'tmsh load sys config default'
+ output = self.client.api.tm.util.bash.exec_cmd(
+ 'run',
+ utilCmdArgs='-c "{0}"'.format(command)
+ )
+ if hasattr(output, 'commandResult'):
+ return str(output.commandResult)
+ return None
+
+ def merge(self, verify=True):
+ temp_name = next(tempfile._get_candidate_names())
+ remote_path = "/var/config/rest/downloads/{0}".format(temp_name)
+ temp_path = '/tmp/' + temp_name
+
+ if self.client.check_mode:
+ return True
+
+ self.upload_to_device(temp_name)
+ self.move_on_device(remote_path)
+ response = self.merge_on_device(
+ remote_path=temp_path, verify=verify
+ )
+ self.remove_temporary_file(remote_path=temp_path)
+ return response
+
+ def merge_on_device(self, remote_path, verify=True):
+ result = None
+
+ command = 'tmsh load sys config file {0} merge'.format(
+ remote_path
+ )
+ if verify:
+ command += ' verify'
+
+ output = self.client.api.tm.util.bash.exec_cmd(
+ 'run',
+ utilCmdArgs='-c "{0}"'.format(command)
+ )
+ if hasattr(output, 'commandResult'):
+ result = str(output.commandResult)
+ return result
+
+ def remove_temporary_file(self, remote_path):
+ self.client.api.tm.util.unix_rm.exec_cmd(
+ 'run',
+ utilCmdArgs=remote_path
+ )
+
+ def move_on_device(self, remote_path):
+ self.client.api.tm.util.unix_mv.exec_cmd(
+ 'run',
+ utilCmdArgs='{0} /tmp/{1}'.format(
+ remote_path, os.path.basename(remote_path)
+ )
+ )
+
+ def upload_to_device(self, temp_name):
+ template = StringIO(self.want.merge_content)
+ upload = self.client.api.shared.file_transfer.uploads
+ upload.upload_stringio(template, temp_name)
+
+ def save(self):
+ if self.client.check_mode:
+ return True
+ return self.save_on_device()
+
+ def save_on_device(self):
+ result = None
+ command = 'tmsh save sys config'
+ output = self.client.api.tm.util.bash.exec_cmd(
+ 'run',
+ utilCmdArgs='-c "{0}"'.format(command)
+ )
+ if hasattr(output, 'commandResult'):
+ result = str(output.commandResult)
+ return result
+
+
+class ArgumentSpec(object):
+ def __init__(self):
+ self.supports_check_mode = True
+ self.argument_spec = dict(
+ reset=dict(
+ type='bool',
+ default=False
+ ),
+ merge_content=dict(),
+ verify=dict(
+ type='bool',
+ default=True
+ ),
+ save=dict(
+ type='bool',
+ default=True
+ )
+ )
+ self.f5_product_name = 'bigip'
+
+
+def main():
+ if not HAS_F5SDK:
+ raise F5ModuleError("The python f5-sdk module is required")
+
+ spec = ArgumentSpec()
+
+ client = AnsibleF5Client(
+ argument_spec=spec.argument_spec,
+ supports_check_mode=spec.supports_check_mode,
+ f5_product_name=spec.f5_product_name
+ )
+
+ try:
+ mm = ModuleManager(client)
+ results = mm.exec_module()
+ client.module.exit_json(**results)
+ except F5ModuleError as e:
+ client.module.fail_json(msg=str(e))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/units/modules/network/f5/test_bigip_config.py b/test/units/modules/network/f5/test_bigip_config.py
new file mode 100644
index 0000000000..dc1aefe2f7
--- /dev/null
+++ b/test/units/modules/network/f5/test_bigip_config.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2017 F5 Networks Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+if sys.version_info < (2, 7):
+ from nose.plugins.skip import SkipTest
+ raise SkipTest("F5 Ansible modules require Python >= 2.7")
+
+import os
+import json
+
+from ansible.compat.tests import unittest
+from ansible.compat.tests.mock import patch, Mock
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.f5_utils import AnsibleF5Client
+
+try:
+ from library.bigip_config import Parameters
+ from library.bigip_config import ModuleManager
+ from library.bigip_config import ArgumentSpec
+except ImportError:
+ from ansible.modules.network.f5.bigip_config import Parameters
+ from ansible.modules.network.f5.bigip_config import ModuleManager
+ from ansible.modules.network.f5.bigip_config import ArgumentSpec
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+
+
+def set_module_args(args):
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+def load_fixture(name):
+ path = os.path.join(fixture_path, name)
+ with open(path) as f:
+ data = f.read()
+ try:
+ data = json.loads(data)
+ except Exception:
+ pass
+ return data
+
+
+class TestParameters(unittest.TestCase):
+ def test_module_parameters(self):
+ args = dict(
+ save='yes',
+ reset='yes',
+ merge_content='asdasd',
+ verify='no',
+ server='localhost',
+ user='admin',
+ password='password'
+ )
+ p = Parameters(args)
+ assert p.save == 'yes'
+ assert p.reset == 'yes'
+ assert p.merge_content == 'asdasd'
+
+
+@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root',
+ return_value=True)
+class TestManager(unittest.TestCase):
+
+ def setUp(self):
+ self.spec = ArgumentSpec()
+
+ def test_run_single_command(self, *args):
+ set_module_args(dict(
+ save='yes',
+ reset='yes',
+ merge_content='asdasd',
+ verify='no',
+ server='localhost',
+ user='admin',
+ password='password'
+ ))
+
+ client = AnsibleF5Client(
+ argument_spec=self.spec.argument_spec,
+ supports_check_mode=self.spec.supports_check_mode,
+ f5_product_name=self.spec.f5_product_name
+ )
+ mm = ModuleManager(client)
+
+ # Override methods to force specific logic in the module to happen
+ mm.exit_json = Mock(return_value=True)
+ mm.reset_device = Mock(return_value=True)
+ mm.upload_to_device = Mock(return_value=True)
+ mm.move_on_device = Mock(return_value=True)
+ mm.merge_on_device = Mock(return_value=True)
+ mm.remove_temporary_file = Mock(return_value=True)
+ mm.save_on_device = Mock(return_value=True)
+
+ results = mm.exec_module()
+
+ assert results['changed'] is True