As a big fan of automation and Ansible I was pretty unhappy when I found out the default “user” Ansible module was not able to handle user management in a way that GAiA likes – since the users need to be added to the config, I started looking at ways to either bring the functionality to the “user” module, or create a separate module that could handle this task.
Not too long ago CheckPoint has released GAiA API. GAiA API is going to be shipped and enabled by default in R80.40 and newer releases. However, API can be manually installed as an extension to boxes running older versions of CheckPoint. Combined with the fact that CheckPoint also contributed a set of modules to Ansible, I felt like it would be neat to expand the functionalities already provided by a vendor until they give us their “official” way of achieving the same goal.
Okay, enough talking, I will cut to the chase – having the base of the GAiA modules I decided to add two new ones: one to manage users and one to gather facts about users. The modules won’t be long so I thought it would be worth to share them with you 🙂
First lets start with downloading the relevant collection. To do that, we are going to use following command:
ansible-galaxy collection install check_point.gaia
Now let’s create a new directory for our playbook:
mkdir checkpoint_playbooks cd checkpoint_playbooks
And paste following into a new file called “hosts”, changing relevant bits:
[checkpoint] 192.168.100.10 [checkpoint:vars] ansible_httpapi_use_ssl=True ansible_httpapi_validate_certs=False ansible_user=ansible ansible_password=mypasswordtowebui ansible_network_os=checkpoint ansible_ssh_private_key_file=/home/tecden/.ssh/ansible_ssh_key ansible_network_os=check_point.gaia.checkpoint
You will need to change the IP address in the [checkpoint] section to include the hosts you want to run the playbook against. In the vars section we can see that I am using both password and private key file. The reason for this is simple – in my playbook, after I create an account I will then put a public key in to allow the new users to authenticate without any passwords.
And now the playbook called api_test.yml (you can call it whatever you like, just swap out the api_test.yml value from examples coming up next):
--- - name: API test hosts: checkpoint connection: httpapi tasks: - name: add tecden user check_point.gaia.cp_gaia_user: name: tecden uid: 1010 roles: adminRole homedir: /home/tecden shell: cli - name: add key connection: ssh authorized_key: user: tecden state: present key: "{{ lookup('file', '/home/tecden/.ssh/tecden.pub') }}"
Alright. Notice that we are referring to a weird module called “check_point.gaia.cp_gaia_user”. This is a module we are going to create right now. Run following command to open a VIM editor and lets create the new module:
vim ~/.ansible/collections/ansible_collections/check_point/gaia/plugins/modules/cp_gaia.user.py
And paste following contents:
#!/usr/bin/python # -*- coding: utf-8 -*- # # Ansible module to manage CheckPoint Firewall (c) 2019 # # 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 <http://www.gnu.org/licenses/>. # from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} from ansible.module_utils.basic import AnsibleModule from ansible_collections.check_point.gaia.plugins.module_utils.checkpoint import idempotent_api_call DOCUMENTATION = """ author: Dawid (tecden.co.uk, @killstrelok) description: - Set up a user module: cp_gaia_user options: name: description: Login of the user. required: true type: str uid: description: UID of the user. Needs to be a number required: true type: int password: description: Password of the user. required: false type: str homedir: description: Home directory of the new user. If left default, creates home directory using following pattern: /home/{{ name }} required: false type: str shell: description: Shell of the new user. Uses Cli(sh) by default. required: false type: str, choices=['scp-only', 'tcsh', 'csh', 'sh', 'no-login', 'bash' ,'cli'] roles: description: Role of the new user. required: false type: str, choices=['adminRole','monitorRole'] short_description: Manage user. version_added: '2.9' """ EXAMPLES = """ - name: add user check_point.gaia.cp_gaia_user: name: new-user uid: 1010 """ RETURN = """ user: description: The updated user. returned: always type: dict """ def main(): # arguments for the module: fields = dict( name=dict(type='str', required=True), uid=dict(type='int'), password=dict(type='str'), homedir=dict(type='str'), shell=dict(type='str', choices=['scp-only', 'tcsh', 'csh', 'sh', 'no-login', 'bash' ,'cli']), roles=dict(type='str', choices=['adminRole','monitorRole']), ) module = AnsibleModule(argument_spec=fields, supports_check_mode=True) api_call_object = 'user' ignore = [] keys = ["name"] res = idempotent_api_call(module, api_call_object, ignore, keys) module.exit_json(**res) if __name__ == "__main__": main()
And now the facts module (located in ~/.ansible/collections/ansible_collections/check_point/gaia/plugins/modules/cp_gaia.users_facts.py
– optional, not required for the module above to work):
#!/usr/bin/python # -*- coding: utf-8 -*- # # Ansible module to manage CheckPoint Firewall (c) 2019 # # 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 <http://www.gnu.org/licenses/>. # from __future__ import (absolute_import, division, print_function) __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = """ module: cp_gaia_users_facts author: Dawid (tecden.co.uk, @killstrelok) description: - Show Users short_description: Show User/s version_added: '2.9' options: name: description: User to show. If not specified, all users information is returned. required: false type: str """ EXAMPLES = """ - name: Show physical interfaces cp_gaia_users_facts: - name: Show physical interface by specifying it name cp_gaia_users_facts: name: tecden """ RETURN = """ ansible_facts: description: The user/s facts. returned: always. type: list """ from ansible.module_utils.basic import AnsibleModule from ansible_collections.check_point.gaia.plugins.module_utils.checkpoint import facts_api_call def main(): # arguments for the module: fields = dict( name=dict(required=False, type="str") ) module = AnsibleModule(argument_spec=fields, supports_check_mode=True) api_call_object = "user" keys = ["name"] res = facts_api_call(module, api_call_object, keys) module.exit_json(**res) if __name__ == "__main__": main()
Now, the idempotent_api_call function does not really work when a user does not exist (because API returns error 400 with “object_not_found” message and the function fails as the logic to handle any code that’s not code 200 is missing). Therefore, I decided to modify the checkpoint.py file a little bit, and changed following section:
def idempotent_api_call(module, api_call_object, ignore, keys): modules_params_original = module.params module_params_show = dict((k, v) for k, v in module.params.items() if k in keys and v is not None) module.params = module_params_show before = api_call(module=module, api_call_object="show-{0}".format(api_call_object)) [before.pop(key) for key in ignore] # Run the command: module.params = modules_params_original res = api_call(module=module, api_call_object="set-{0}".format(api_call_object)) module.params = module_params_show after = res.copy() [after.pop(key) for key in ignore] changed = False if before == after else True return { api_call_object.replace('-', '_'): res, "changed": changed }
to this:
def idempotent_api_call(module, api_call_object, ignore, keys): modules_params_original = module.params module_params_show = dict((k, v) for k, v in module.params.items() if k in keys and v is not None) module.params = module_params_show before = api_call(module=module, api_call_object="show-{0}".format(api_call_object)) [before.pop(key) for key in ignore] # Run the command: module.params = modules_params_original if before == "object_not_found": res = api_call(module=module, api_call_object="add-{0}".format(api_call_object)) else: res = api_call(module=module, api_call_object="set-{0}".format(api_call_object)) module.params = module_params_show after = res.copy() [after.pop(key) for key in ignore] changed = False if before == after else True return { api_call_object.replace('-', '_'): res, "changed": changed }
And this section:
def api_call(module, api_call_object): payload = get_payload_from_parameters(module.params) connection = Connection(module._socket_path) version = get_version(module) code, response = send_request(connection, version, api_call_object, payload) if code != 200: module.fail_json(msg=parse_fail_message(code, response)) return response
to this:
def api_call(module, api_call_object): payload = get_payload_from_parameters(module.params) connection = Connection(module._socket_path) version = get_version(module) code, response = send_request(connection, version, api_call_object, payload) if code != 200: if code == 400 and response['code'] == 'object_not_found' and any(nonfatal in api_call_object for nonfatal in ["user"]): response = "object_not_found" else: module.fail_json(msg=parse_fail_message(code, response)) return response
I tried to follow a logic where some API calls can fail with “object_not_found” message (e.g. when creating a user) and others cannot (as for example we might attempt to add an IP address to an interface that doesn’t exist etc.). Therefore, any calls that are “fine to fail” should be listed alongside “user” string in following piece of code, like:
any(nonfatal in api_call_object for nonfatal in ["user", "group", "younameit")
Alright, let’s test it. I am going to run following command from the checkpoint_playbooks directory that we created right at the beginning and that contains the playbook and hosts file:
strelok@tecden:~$ ansible-playbook api_test.yml -i hosts PLAY [API test] **************************************************************** TASK [Gathering Facts] ********************************************************* [DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host 192.168.100.10 should use /usr/bin/python3, but is using /usr/bin/python for backward compatibility with prior Ansible releases. A future Ansible release will default to using the discovered platform python for this host. See https://docs.ansible.com/ansible /2.9/reference_appendices/interpreter_discovery.html for more information. This feature will be removed in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. ok: [192.168.100.10] TASK [add tecden user] ********************************************************* [WARNING]: Module did not set no_log for password changed: [192.168.100.10] TASK [add key] ***************************************************************** changed: [192.168.100.10] PLAY RECAP ********************************************************************* 192.168.100.10 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
One more try to see if the modules will now do nothing as our user already exists and has appropriate key added:
strelok@tecden:~$ ansible-playbook api_test_tecden.yml -i hosts PLAY [API test] **************************************************************** TASK [Gathering Facts] ********************************************************* [DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host 192.168.100.10 should use /usr/bin/python3, but is using /usr/bin/python for backward compatibility with prior Ansible releases. A future Ansible release will default to using the discovered platform python for this host. See https://docs.ansible.com/ansible /2.9/reference_appendices/interpreter_discovery.html for more information. This feature will be removed in version 2.12. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg. ok: [192.168.100.10] TASK [add tecden user] ********************************************************* [WARNING]: Module did not set no_log for password ok: [192.168.100.10] TASK [add key] ***************************************************************** ok: [192.168.100.10] PLAY RECAP ********************************************************************* 192.168.100.10 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Woohoo! Nothing happened which means we achieved our goal 🙂
I will be looking to add the ability to define “state” to delete users if needed but I believe this would require more work with the checkpoint.py file which I don’t really want to do right now.
Also sincere apology to @chkp-yuvalfe (CheckPoint employee that maintains the CheckPointAnsibleGAIACollection repo and whose modules I used as templates for above) if my code looks terrible, makes no sense or if I re-invented the wheel.