From 234a877ea5d9bbc33277b740d156f8309e9f6c3d Mon Sep 17 00:00:00 2001 From: Harri Tuominen Date: Tue, 30 Jan 2018 21:00:29 +0200 Subject: [PATCH] Netact cm command2 (#34873) --- .../modules/network/netact/__init__.py | 0 .../network/netact/netact_cm_command.py | 367 ++++++++++++++++++ test/units/modules/network/netact/__init__.py | 0 .../network/netact/test_netact_cm_command.py | 183 +++++++++ 4 files changed, 550 insertions(+) create mode 100644 lib/ansible/modules/network/netact/__init__.py create mode 100644 lib/ansible/modules/network/netact/netact_cm_command.py create mode 100644 test/units/modules/network/netact/__init__.py create mode 100644 test/units/modules/network/netact/test_netact_cm_command.py diff --git a/lib/ansible/modules/network/netact/__init__.py b/lib/ansible/modules/network/netact/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/modules/network/netact/netact_cm_command.py b/lib/ansible/modules/network/netact/netact_cm_command.py new file mode 100644 index 00000000000..7e5cca03294 --- /dev/null +++ b/lib/ansible/modules/network/netact/netact_cm_command.py @@ -0,0 +1,367 @@ +#!/usr/bin/python +# Copyright: Nokia +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# pylint: disable=invalid-name +# pylint: disable=wrong-import-position +# pylint: disable=too-many-locals +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements + +""" +NetAct CM ansible command module +""" +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: netact_cm_command + +short_description: Manage network configuration data in Nokia Core and Radio networks + +version_added: "2.5" + +description: + netact_cm_command can be used to run various configuration management operations. + This module requires that the target hosts have Nokia NetAct network management system installed. + Module will access the Configurator command line interface in NetAct to upload network configuration to NetAct, + run configuration export, plan import and configuration provision operations + To set the scope of the operation, define Distinguished Name (DN) or Working Set (WS) or + Maintenance Region (MR) as input +options: + operation: + description: + Supported operations allow user to upload actual configuration from the network, to import and + provision prepared plans, or export reference or actual configuration for planning purposes. + Provision_Mass_Modification enables provisioning the same parameters to multiple network elements. + This operation supports modifications only to one object class at a time. With this option + NetAct Configurator creates and provisions a plan to the network with the given scope and options. + required: true + choices: + - upload + - provision + - import + - export + - Provision_Mass_Modification + aliases: + - op + opsName: + description: + - user specified operation name + required: false + DN: + description: + Sets the exact scope of the operation in form of a list of managed object + Distinguished Names (DN) in the network. + A single DN or a list of DNs can be given (comma separated list without spaces). + Alternatively, if DN or a list of DNs is not given, working set (WS) or Maintenance Region (MR) + must be provided as parameter to set the scope of operation. + required: false + + WS: + description: + Sets the scope of the operation to use one or more pre-defined working sets (WS) in NetAct. + A working set contains network elements selected by user according to defined criteria. + A single WS name, or multiple WSs can be provided (comma-separated list without spaces). + Alternatively, if a WS name or a list of WSs is not given, Distinguished Name (DN) or + Maintenance Region(MR) must be provided as parameter to set the scope of operation. + required: false + MR: + description: + Sets the scope of the operation to network elements assigned to a Maintenance Region (MR) + Value can be set as MR IDs including the Maintenance Region Collection (MRC) + information (for example MRC-FIN1/MR-Hel). + Multiple MRs can be given (comma-separated list without spaces) + The value of this parameter is searched through MR IDs under given MRC. If there is no match, + then it is searched from all MR names. + Alternatively, if MR ID or a list or MR IDs is not given, Distinguished Name (DN) or Working Set (WS) + must be provided as parameter to set the scope of operation. + required: false + planName: + description: + - Specifies a plan name. + required: false + typeOption: + description: + Specifies the type of the export operation. + required: false + choices: + - plan + - actual + - reference + - template + - siteTemplate + aliases: + - type + fileFormat: + description: + Indicates file format. + required: false + choices: + - RAML2 + - CSV + - XLSX + fileName: + description: + - Specifies a file name. Valid for Import and Export operations. + required: false + inputFile: + description: + Specifies full path to plan file location for the import operation. + This parameter (inputFile) or the fileName parameter must be filled. If both are present then + the inputFile is used. + required: false + createBackupPlan: + description: + - Specifies if backup plan generation is enabled. + required: false + type: bool + backupPlanName: + description: + - Specifies a backup plan name + required: false + verbose: + description: + NetAct Configurator will print more info + required: false + extra_opts: + description: + Extra options to be set for operations. Check Configuration Management > Configuration Management + Operating Procedures > Command Line Operations in Nokia NetAct user documentation for further + information for extra options. + required: false +notes: + - Check mode is not currently supported +author: + - Harri Tuominen (@hatuomin) +''' + +EXAMPLES = ''' +# Pass in a message +- name: Upload + netact_cm_command: + operation: "Upload" + opsname: 'Uploading_test' + dn: "PLMN-PLMN/MRBTS-746" + extra_opts: '-btsContentInUse true' + +- name: Provision + netact_cm_command: + operation: "Provision" + opsname: 'Provision_test' + dn: "PLMN-PLMN/MRBTS-746" + planName: 'mySiteTemplate' + type: 'actual' + createBackupPlan: true + backupPlanName: 'myBackupPlanName' + +- name: Export and fetching data from target + netact_cm_command: + operation: "Export" + opsname: 'Export_test' + planName: 'mySiteTemplate' + type: 'actual' + fileName: 'exportTest.xml' +- fetch: + src: /var/opt/nokia/oss/global/racops/export/exportTest.xml + dest: fetched + +- name: Import + netact_cm_command: + operation: "Import" + opsname: 'Import_test' + fileFormat: 'CSV' + type: 'plan' + fileName: 'myCSVFile' + planName: 'myPlanName' + extra_ops: 'enablePolicyPlans true' + +# fail the module +- name: Test failure of the module + netact_cm_command: + name: fail me +''' + +RETURN = ''' +original_message: + description: The original name param that was passed in + returned: Command line + type: string + sample: '/opt/oss/bin/racclimx.sh -op Upload -opsName Uploading_testi -DN PLMN-PLMN/MRBTS-746' +message: + description: The output message that the netact_cm_command module generates + returned: Command output message + type: string +changed: + description: data changed + returned: true if data is changed + type: boolean +''' + +from ansible.module_utils.basic import AnsibleModule + +racclimx = '/opt/oss/bin/racclimx.sh' + + +def main(): + """ + Main module where option are handled and command is executed + :return: + """ + # define the available arguments/parameters that a user can pass to + # the module + module_args = dict( + operation=dict(type='str', required=True, + aliases=['op'], + choices=['Upload', 'Provision', 'Import', + 'Export', 'Provision_Mass_Modification']), + opsName=dict(type='str', required=False), + DN=dict(type='str', required=False), + WS=dict(type='str', required=False), + MR=dict(type='str', required=False), + + planName=dict(type='str', required=False), + typeOption=dict(type='str', required=False, aliases=['type'], + choices=['plan', 'actual', 'reference', 'template', 'siteTemplate']), + fileFormat=dict(type='str', required=False, choices=['CSV', 'RAML2', 'XLSX']), + fileName=dict(type='str', required=False), + createBackupPlan=dict(type='bool', required=False), + backupPlanName=dict(type='str', required=False), + inputFile=dict(type='str', required=False), + + verbose=dict(type='str', required=False), + extra_opts=dict(type='str', required=False) + ) + + # seed the result dict in the object + # we primarily care about changed and state + # change is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + original_message='', + cmd='', + message='' + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # if the user is working with this module in only check mode we do not + # want to make any changes to the environment, just return the current + # state with no modifications + if module.check_mode: + result['skipped'] = True + result['msg'] = 'check mode not (yet) supported for this module' + module.exit_json(**result) + + # manipulate or modify the state as needed (this is going to be the + # part where your module will do what it needs to do) + + operation = module.params.get('operation') + if not operation: + module.fail_json(msg='Operation not defined', **result) + + opsname = module.params.get('opsName') + dn = module.params.get('DN') + ws = module.params.get('WS') + mr = module.params.get('MR') + + planname = module.params.get('planName') + typeoption = module.params.get('typeOption') + fileformat = module.params.get('fileFormat') + filename = module.params.get('fileName') + + createBackupPlan = module.params.get('createBackupPlan') + backupPlanName = module.params.get('backupPlanName') + inputfile = module.params.get('inputFile') + + extra_opts = module.params.get('extra_opts') + verbose = module.params.get('verbose') + + # parameter checks + + command = [racclimx, '-op', operation] + + if opsname: + command.append('-opsName') + command.append(opsname) + + if dn: + command.append('-DN') + command.append(dn) + + if ws: + command.append('-WS') + command.append(ws) + + if mr: + command.append('-MR') + command.append(mr) + + if planname: + command.append('-planName') + command.append(planname) + + if typeoption: + command.append('-type') + command.append(typeoption) + + if fileformat: + command.append('-fileFormat') + command.append(fileformat) + + if filename: + command.append('-fileName') + command.append(filename) + + if createBackupPlan: + command.append('-createBackupPlan') + command.append('true') + + if backupPlanName: + command.append('-backupPlanName') + command.append(backupPlanName) + + if inputfile: + command.append('-inputFile') + command.append(inputfile) + + if extra_opts: + command = command + extra_opts.split(" ") + + if verbose: + if verbose == 'True': + command.append("-v") + + rc, out, err = module.run_command(command, check_rc=True) + if rc != 0: + result['changed'] = False + module.fail_json(msg=err) + else: + result['changed'] = True + result['original_message'] = out + result['cmd'] = command + result['message'] = out + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/netact/__init__.py b/test/units/modules/network/netact/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/units/modules/network/netact/test_netact_cm_command.py b/test/units/modules/network/netact/test_netact_cm_command.py new file mode 100644 index 00000000000..f5bc8724e88 --- /dev/null +++ b/test/units/modules/network/netact/test_netact_cm_command.py @@ -0,0 +1,183 @@ +""" +netact_cm_command unit tests +""" + +# -*- coding: utf-8 -*- + + +# (c) 2017, Nokia +# 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 . + +# pylint: disable=invalid-name,protected-access,function-redefined,unused-argument +# pylint: disable=unused-import,redundant-unittest-assert + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +import json + +from ansible.compat.tests import unittest +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes +from ansible.modules.network.netact import netact_cm_command +from ansible.compat.tests.mock import patch + +from units.modules.utils import set_module_args as _set_module_args, \ + AnsibleExitJson, AnsibleFailJson, ModuleTestCase + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + pass + + +def exit_json(*args, **kwargs): + """function to patch over exit_json; package return data into an exception""" + if 'changed' not in kwargs: + kwargs['changed'] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + """function to patch over fail_json; package return data into an exception""" + kwargs['failed'] = True + raise AnsibleFailJson(kwargs) + + +def get_bin_path(self, arg, required=False): + """Mock AnsibleModule.get_bin_path""" + if arg.endswith('netact_cm_command'): + return '/usr/bin/my_command' + else: + if required: + fail_json(msg='%r not found !' % arg) + + +class TestClass(unittest.TestCase): + """ + Test cases + """ + + def setUp(self): + self.mock_module_helper = patch.multiple(basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path) + self.mock_module_helper.start() + self.addCleanup(self.mock_module_helper.stop) + + def test_module_fail_when_required_args_missing(self): + """ + Testing that command is failing if args are missing + :return: + """ + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + netact_cm_command.main() + self.assertTrue(False) + + def test_ensure_command_called(self): + """ + Testing that command is executed with correct args + :return: + """ + set_module_args({ + 'operation': "Upload", + 'opsName': 'Uploading_testi', + 'DN': "PLMN-PLMN/MRBTS-746", + }) + + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + stdout = 'configuration updated' + stderr = '' + return_code = 0 + mock_run_command.return_value = return_code, stdout, stderr # successful execution + + with self.assertRaises(AnsibleExitJson) as result: + netact_cm_command.main() + print(result.exception.args) + self.assertTrue(result.exception.args[0]['changed']) # ensure result is changed + + mock_run_command.assert_called_once_with( + ['/opt/oss/bin/racclimx.sh', '-op', 'Upload', '-opsName', 'Uploading_testi', + '-DN', 'PLMN-PLMN/MRBTS-746'], + check_rc=True) + + def test_ensure_backupPlanName_outputs_correctly(self): + """ + Testing that command is executed with correct args + :return: + """ + set_module_args({ + 'operation': "Provision", + 'opsName': 'Provision_test', + 'WS': "PLMN-PLMN/MRBTS-746", + 'createBackupPlan': "Yes", + 'backupPlanName': "backupPlanName" + }) + + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + stdout = 'configuration updated' + stderr = '' + return_code = 0 + mock_run_command.return_value = return_code, stdout, stderr # successful execution + + with self.assertRaises(AnsibleExitJson) as result: + netact_cm_command.main() + print(result.exception.args) + self.assertTrue(result.exception.args[0]['changed']) # ensure result is changed + + mock_run_command.assert_called_once_with( + ['/opt/oss/bin/racclimx.sh', '-op', 'Provision', '-opsName', 'Provision_test', + '-WS', 'PLMN-PLMN/MRBTS-746', '-createBackupPlan', 'true', '-backupPlanName', 'backupPlanName'], + check_rc=True) + + def test_withwrongargs(self): + """ + Testing that wrong attribute causing error + :return: + """ + set_module_args({ + 'operation': "Upload", + 'opsName': 'Uploading_testi', + 'MR': "PLMN-PLMN/MRBTS-746", + 'abc': 'abc' + }) + + with self.assertRaises(AnsibleFailJson): + with patch.object(basic.AnsibleModule, 'run_command') as mock_run_command: + stdout = 'configuration updated' + stderr = '' + return_code = 0 + mock_run_command.return_value = return_code, stdout, stderr # successful execution + + with self.assertRaises(AnsibleExitJson) as result: + netact_cm_command.main() + self.assertTrue(result.exception.args[0]['changed']) # ensure result is changed + + self.assertFalse(True) # ensure result is changed