diff --git a/collection/galaxy.yml b/collection/galaxy.yml index c9b808e..1be7b0c 100644 --- a/collection/galaxy.yml +++ b/collection/galaxy.yml @@ -1,7 +1,7 @@ --- namespace: community name: syncthing -version: 1.1.1 +version: 1.1.2 readme: README.md authors: - Rafael Bodill diff --git a/collection/plugins/module_utils/syncthing_api.py b/collection/plugins/module_utils/syncthing_api.py new file mode 100644 index 0000000..d4544d5 --- /dev/null +++ b/collection/plugins/module_utils/syncthing_api.py @@ -0,0 +1,110 @@ +import json +import os +import platform +from xml.etree.ElementTree import parse + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url, url_argument_spec + +if platform.system() == 'Windows': + DEFAULT_ST_CONFIG_LOCATION = '%localappdata%/Syncthing/config.xml' +elif platform.system() == 'Darwin': + DEFAULT_ST_CONFIG_LOCATION = '$HOME/Library/Application Support/Syncthing/config.xml' +else: + DEFAULT_ST_CONFIG_LOCATION = '$HOME/.local/state/syncthing/config.xml' + + +class SyncthingBase(AnsibleModule): + def _make_headers(self, target=None): + url = '{}{}{}{}'.format( + self.params['host'], + self.api_url, + '/' if target else '', + target if target else '') + headers = {'X-Api-Key': self.params['api_key'] } + return url, headers + + def _get_key_from_filesystem(self): + try: + if self.params['config_file']: + stconfigfile = self.params['config_file'] + else: + stconfigfile = os.path.expandvars(DEFAULT_ST_CONFIG_LOCATION) + stconfig = parse(stconfigfile) + root = stconfig.getroot() + gui = root.find('gui') + self.params['api_key'] = gui.find('apikey').text + except Exception: + self.fail_json(msg="Auto-configuration failed. Please specify") + + def _api_call(self, method='GET', data=None, target=None, missing_ok=False): + unix_socket = None + if 'unix_socket' in self.params: + unix_socket = self.params['unix_socket'] + url, headers = self._make_headers(target=target) + if data: + headers['Content-Type'] = 'application/json' + data = json.dumps(data) + self.result = { + "changed": method != 'GET', + "response": None, + } + resp, info = fetch_url(self, + url=url, unix_socket=unix_socket, + data=data, headers=headers, + method=method, + timeout=self.params['timeout']) + if info: + self.result['response'] = info + else: + self.fail_json(msg='Error occured while calling host') + if info['status'] not in [200, 404]: + self.fail_json(msg='Error occured while calling host') + if info['status'] == 404: + if missing_ok: + return {} + else: + self.fail_json(msg='Error occured while calling host') + try: + content = resp.read() + except AttributeError: + self.result['content'] = info.pop('body', '') + self.fail_json(msg='Error occured while reading response') + try: + return json.loads(content) + except json.decoder.JSONDecodeError: + return {'content': content} + + def get_call(self, target=None): + return self._api_call(missing_ok=True, target=target) + def delete_call(self, target): + return self._api_call(method='DELETE', target=target) + def post_call(self, data=None, target=None): + return self._api_call(method='POST', data=data, target=target) + def put_call(self, data=None, target=None): + return self._api_call(method='PUT', data=data, target=target) + def patch_call(self, data=None, target=None): + return self._api_call(method='PATCH', data=data, target=target) + + def exit_json(self): + super().exit_json(**self.module.result) + def fail_json(self, msg=""): + super().fail_json(msg, **self.module.result) + + def __init__(self, api_url='/', module_args=None, supports_check_mode=True): + module_args_temp = url_argument_spec() + module_args_temp.update(dict( + host=dict(type='str', default='http://127.0.0.1:8384'), + unix_socket=dict(type='str', required=False), + api_key=dict(type='str', required=False, no_log=True), + config_file=dict(type='str', required=False), + timeout=dict(type='int', default=30), + )) + if module_args is None: + module_args = {} + module_args_temp.update(module_args) + super().__init__(module_args=module_args_temp, supports_check_mode=True) + + # Auto-configuration: Try to fetch API key from filesystem + if not self.params['api_key']: + self._get_key_from_filesystem() diff --git a/collection/plugins/modules/device.py b/collection/plugins/modules/device.py index 20d2b6f..7db37df 100644 --- a/collection/plugins/modules/device.py +++ b/collection/plugins/modules/device.py @@ -78,110 +78,8 @@ response: type: dict ''' -import os -import json -import platform -from xml.etree.ElementTree import parse - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url, url_argument_spec - -SYNCTHING_API_URI = "/rest/config/devices" -if platform.system() == 'Windows': - DEFAULT_ST_CONFIG_LOCATION = '%localappdata%/Syncthing/config.xml' -elif platform.system() == 'Darwin': - DEFAULT_ST_CONFIG_LOCATION = '$HOME/Library/Application Support/Syncthing/config.xml' -else: - DEFAULT_ST_CONFIG_LOCATION = '$HOME/.config/syncthing/config.xml' - - -def make_headers(host, api_key, unix_socket=None, device=None): - url = '{}{}{}{}'.format( - host, - SYNCTHING_API_URI, - '/' if device else '', - device if device else '') - headers = {'X-Api-Key': api_key } - return url, headers - -def get_key_from_filesystem(module): - try: - if module.params['config_file']: - stconfigfile = module.params['config_file'] - else: - stconfigfile = os.path.expandvars(DEFAULT_ST_CONFIG_LOCATION) - stconfig = parse(stconfigfile) - root = stconfig.getroot() - gui = root.find('gui') - api_key = gui.find('apikey').text - return api_key - except Exception: - module.fail_json(msg="Auto-configuration failed. Please specify" - "the API key manually.") - -# Fetch Syncthing configuration -def remote_config(module, method='GET', config=None, result=None, device=None, - missing_ok=False): - unix_socket = None - if 'unix_socket' in module.params: - unix_socket = module.params['unix_socket'] - url, headers = make_headers( - host=module.params['host'], - unix_socket=unix_socket, - api_key= module.params['api_key'], - device=device) - data = config - if config: - headers['Content-Type'] = 'application/json' - data = json.dumps(config) - if not result: - result = {} - - resp, info = fetch_url( - module, url=url, unix_socket=unix_socket, - data=data, headers=headers, - method=method, timeout=module.params['timeout']) - if info: - result['response'] = info - else: - module.fail_json(msg='Error occured while calling host', **result) - - if info['status'] not in [200, 404]: - module.fail_json(msg='Error occured while calling host', **result) - - if info['status'] == 404: - if missing_ok: - return {} - else: - module.fail_json(msg='Error occured while calling host', **result) - - try: - content = resp.read() - except AttributeError: - result['content'] = info.pop('body', '') - module.fail_json(msg='Error occured while reading response', **result) - - try: - return json.loads(content) - except json.decoder.JSONDecodeError: - return {'content': content} - - -def get_device(module, device=None): - return remote_config(module, missing_ok=True, device=device) -def delete_device(module, device, result=None): - return remote_config(module, method='DELETE', device=device, result=result) - -# Post the new configuration to Syncthing API -def post_device(module, config, result=None, device=None): - return remote_config(module, method='POST', config=config, result=result, - device=device) -def put_device(module, config, result=None, device=None): - return remote_config(module, method='PUT', config=config, result=result, - device=device) -def patch_device(module, config, result=None, device=None): - return remote_config(module, method='PATCH', config=config, result=result, - device=device) +from ansible.collections.syncthing.plugins.module_utils.syncthing_api \ + import SyncthingModule # Returns an object of a new device def create_device(params): @@ -208,63 +106,50 @@ def create_device(params): def run_module(): # module arguments - module_args = url_argument_spec() + module_args = {} module_args.update(dict( id=dict(type='str', required=True), name=dict(type='str', required=False), - host=dict(type='str', default='http://127.0.0.1:8384'), - unix_socket=dict(type='str', required=False), - api_key=dict(type='str', required=False, no_log=True), - config_file=dict(type='str', required=False), - timeout=dict(type='int', default=30), state=dict(type='str', default='present', choices=['absent', 'present', 'paused']), )) - # seed the result dict in the object - result = { - "changed": False, - "response": None, - } - # the AnsibleModule object will be our abstraction working with Ansible - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True + module = SyncthingModule( + api_url='/rest/config/devices', + argument_spec=module_args ) if module.params['state'] != 'absent' and not module.params['name']: - module.fail_json(msg='You must provide a name when creating', **result) + module.fail_json(msg='You must provide a name when creating') - if module.check_mode: - return result - - # Auto-configuration: Try to fetch API key from filesystem - if not module.params['api_key']: - module.params['api_key'] = get_key_from_filesystem(module) - - device = get_device(module, module.params['id']) + want_pause = module.params['state'] == 'paused' device_exists = False + device = module.get_call(target=module.params['id']) if 'deviceID' in device and device['deviceID'] == module.params['id']: device_exists = True - want_pause = module.params['state'] == 'paused' if module.params['state'] == 'absent': if device_exists: - delete_device(module, device=device['deviceID'], result=result) - result['changed'] = True + if module.check_mode: + module.result['changed'] = True + else: + module.delete_call(target=device['deviceID']) elif device_exists: # exists but maybe needs changing if device['paused'] != want_pause: device['paused'] = want_pause - patch_device(module, config=device, result=result, - device=device['deviceID']) - result['changed'] = True + if module.check_mode: + module.result['changed'] = True + else: + module.patch_call(data=device, target=device['deviceID']) else: # Doesn't exist but needs to be added - device = create_device(module.params) - post_device(module, config=device, result=result) - result['changed'] = True + if module.check_mode: + module.result['changed'] = True + else: + device = create_device(module.params) + module.post_call(data=device) - module.exit_json(**result) + module.exit_json() def main(): run_module()