Compare commits

...

14 Commits

Author SHA1 Message Date
Borjan Tchakaloff 3a49d17dfd syncthing_folder: Always share a folder with the current device
buildbot/travis_bionic Build done. Details
buildbot/multibuild_parent Build done. Details
There is no need to require that the device being configured is
explictly mentioned in its configuration. We can get it through
the status endpoint and automatically add it to the set of devices
the folder is shared with.
2021-02-19 17:40:28 +01:00
Borjan Tchakaloff c3cd1e450d Extract fetching data from the REST API 2021-02-18 17:35:43 +01:00
Borjan Tchakaloff c7aa08f97a syncthing_folder: Do not duplicate the `msg` key 2021-02-18 17:18:30 +01:00
Borjan Tchakaloff 38ff2db9ca syncthing_folder: Accept names of known devices 2021-02-18 17:03:55 +01:00
Borjan Tchakaloff aa0bab7c0b Update notice 2021-02-17 18:16:47 +01:00
Borjan Tchakaloff 6777a9ab6a syncthing_folder: Default to empty device list
When defining a new shared folder, the configuration is missing and
thus there are no current devices configured.
2021-02-17 18:05:08 +01:00
Borjan Tchakaloff 1b2952c1a6 syncthing_folder: Keep existing device ordering
Keep the existing list of devices if it contains the expected
devices.  This change fixes the current/expected configurations
comparison on devices ordering.
2021-01-01 17:17:39 +01:00
Borjan Tchakaloff 11282b098e syncthing_device: Accept path to the configuration file
Specifying `config_file` overrides the default path relative to
the current user.

This is handy when the executing user is a different user.
2021-01-01 16:01:13 +01:00
Borjan Tchakaloff e336ebde13 syncthing_folder: Accept path to the configuration file
Specifying `config_file` overrides the default path relative to
the current user.

This is handy when the executing user is a different user.
2021-01-01 15:57:21 +01:00
Borjan Tchakaloff 8fee0cb114 syncthing_folder: Accept file-system watcher flag
Accept the `fs_watcher` flag matching the `folder.fsWatcherEnabled`
attribute.
2021-01-01 15:57:21 +01:00
Borjan Tchakaloff 866f329f47 syncthing_folder: Accept folder type
Accept the `type` value matching the `folder.type` attribute.
2021-01-01 15:57:21 +01:00
Borjan Tchakaloff 7532f87638 syncthing_folder: Accept permission ignoring flag
Accept the `ignore_perms` flag matching the `folder.ignorePerms`
attribute.
2021-01-01 15:57:21 +01:00
Borjan Tchakaloff 1136e9b06b syncthing_folder: Factor folder configuration
The folder configuration is parametrized in one place, no need for
duplicates.
2021-01-01 15:57:21 +01:00
Borjan Tchakaloff 83e9add60c syncthing_folder: Always update the devices the folder is shared with
Ensure all devices the folder should be shared with is part of the
folder configuration.

Previously, only at folder creation would the devices be set.
2021-01-01 15:28:15 +01:00
3 changed files with 140 additions and 48 deletions

View File

@ -1,5 +1,8 @@
# Ansible Modules for Syncthing
Forked from [github.com/rafi/ansible-modules-syncthing]
(https://github.com/rafi/ansible-modules-syncthing).
Collection of modules for [Syncthing](https://syncthing.net) management.
## Install
@ -12,9 +15,9 @@ Copy the `./library` directory to your Ansible project and ensure your
library = ./library
```
Please note this module was test on:
Please note this module was tested on:
* Ubuntu 16.04 with Syncthing v0.14.52
* Debian Buster with Syncthing v1.0.0
Please report successful usage on other platforms/versions.
@ -22,12 +25,9 @@ Please report successful usage on other platforms/versions.
See [example playbooks](./playbooks) for robust feature usage:
* [install_syncthing.yml] - Install Syncthing on Ubuntu (with systemd)
* [install_syncthing.yml] - Install Syncthing on Debian/Ubuntu (with systemd)
* [manage.yml] - Ensure Syncthing devices and folders across devices
[install_syncthing.yml]: http://
[manage.yml]: http://
## Modules
### Module: `syncthing_device`
@ -108,4 +108,5 @@ Examples:
## License
Copyright: (c) 2018, Rafael Bodill `<justrafi at g>`
Copyright: (c) 2020--2021, Borjan Tchakaloff `<first name at last name dot fr>`
GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Rafael Bodill <justrafi at google mail>
# Copyright: (c) 2020, Borjan Tchakaloff <first name at last name dot fr>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {
@ -39,6 +40,12 @@ options:
- API key to use for authentication with host.
If not provided, will try to auto-configure from filesystem.
required: false
config_file:
description:
- Path to the Syncthing configuration file for automatic
discovery (`api_key`). Note that the running user needs read
access to the file.
required: false
timeout:
description:
- The socket level timeout in seconds
@ -91,7 +98,10 @@ def make_headers(host, api_key):
def get_key_from_filesystem(module):
try:
stconfigfile = os.path.expandvars(DEFAULT_ST_CONFIG_LOCATION)
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')
@ -166,6 +176,7 @@ def run_module():
name=dict(type='str', required=False),
host=dict(type='str', default='http://127.0.0.1:8384'),
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', 'pause']),

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Rafael Bodill <justrafi at gmail>
# Copyright: (c) 2020, Borjan Tchakaloff <first name at last name dot fr>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {
@ -38,6 +39,20 @@ options:
description:
- List of devices to share folder with
required: false
fs_watcher:
description:
- Whether to activate the file-system watcher.
default: true
ignore_perms:
description:
- Whether to ignore permissions when looking for changes.
default: false
type:
description:
- The folder type: sending local chances, and/or receiving
remote changes.
default: sendreceive
choices: ['sendreceive', 'sendonly', 'receiveonly']
host:
description:
- Host to connect to, including port
@ -47,6 +62,12 @@ options:
- API key to use for authentication with host.
If not provided, will try to auto-configure from filesystem.
required: false
config_file:
description:
- Path to the Syncthing configuration file for automatic
discovery (`api_key`). Note that the running user needs read
access to the file.
required: false
timeout:
description:
- The socket level timeout in seconds
@ -85,7 +106,7 @@ 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/system/config"
SYNCTHING_API_BASE_URI = "/rest"
if platform.system() == 'Windows':
DEFAULT_ST_CONFIG_LOCATION = '%localappdata%/Syncthing/config.xml'
elif platform.system() == 'Darwin':
@ -94,14 +115,17 @@ else:
DEFAULT_ST_CONFIG_LOCATION = '$HOME/.config/syncthing/config.xml'
def make_headers(host, api_key):
url = '{}{}'.format(host, SYNCTHING_API_URI)
def make_headers(host, api_key, resource):
url = '{}{}/{}'.format(host, SYNCTHING_API_BASE_URI, resource)
headers = {'X-Api-Key': api_key }
return url, headers
def get_key_from_filesystem(module):
try:
stconfigfile = os.path.expandvars(DEFAULT_ST_CONFIG_LOCATION)
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')
@ -111,12 +135,18 @@ def get_key_from_filesystem(module):
module.fail_json(msg="Auto-configuration failed. Please specify"
"the API key manually.")
# Fetch Syncthing configuration
def get_config(module):
url, headers = make_headers(module.params['host'], module.params['api_key'])
def get_data_from_rest_api(module, resource):
url, headers = make_headers(
module.params['host'], module.params['api_key'], resource
)
resp, info = fetch_url(
module, url, data=None, headers=headers,
method='GET', timeout=module.params['timeout'])
module,
url,
data=None,
headers=headers,
method='GET',
timeout=module.params['timeout']
)
if not info or info['status'] != 200:
result['response'] = info
@ -131,9 +161,34 @@ def get_config(module):
return json.loads(content)
# Fetch Syncthing configuration
def get_config(module):
return get_data_from_rest_api(module, 'system/config')
# Fetch Syncthing status
def get_status(module):
return get_data_from_rest_api(module, 'system/status')
# Get the device name -> device ID mapping.
def get_devices_mapping(config):
return {
device['name']: device['deviceID'] for device in config['devices']
}
# Get the folder configuration from the global configuration, if it exists
def get_folder_config(folder_id, config):
for folder in config['folders']:
if folder['id'] == folder_id:
return folder
return None
# Post the new configuration to Syncthing API
def post_config(module, config, result):
url, headers = make_headers(module.params['host'], module.params['api_key'])
url, headers = make_headers(
module.params['host'],
module.params['api_key'],
'system/config',
)
headers['Content-Type'] = 'application/json'
result['msg'] = config
@ -143,23 +198,51 @@ def post_config(module, config, result):
if not info or info['status'] != 200:
result['response'] = str(info)
module.fail_json(msg='Error occured while posting new config', **result)
module.fail_json(**result)
# Returns an object of a new folder
def create_folder(params):
folder = {
def create_folder(params, self_id, current_device_ids, devices_mapping):
# We need the current device ID as per the Syncthing API.
# If missing, Syncthing will add it alright, but we don't want to give
# the false idea that this configuration is different just because of that.
wanted_device_ids = {self_id}
for device_name_or_id in params['devices']:
if device_name_or_id in devices_mapping:
wanted_device_ids.add(devices_mapping[device_name_or_id])
else:
# Purposefully do not validate we already know this device ID or
# name as per previous behavior. This will need to be fixed.
wanted_device_ids.add(device_name_or_id)
# Keep the original ordering if collections are equivalent.
# Again, for idempotency reasons.
device_ids = (
current_device_ids
if set(current_device_ids) == wanted_device_ids
else sorted(wanted_device_ids)
)
# Sort the device IDs to keep idem-potency
devices = [
{
'deviceID': device_id,
'introducedBy': '',
} for device_id in device_ids
]
return {
'autoNormalize': True,
'copiers': 0,
'devices': [],
'devices': devices,
'disableSparseFiles': False,
'disableTempIndexes': False,
'filesystemType': 'basic',
'fsWatcherDelayS': 10,
'fsWatcherEnabled': True,
'fsWatcherEnabled': params['fs_watcher'],
'hashers': 0,
'id': params['id'],
'ignoreDelete': False,
'ignorePerms': False,
'ignorePerms': params['ignore_perms'],
'label': params['label'] if params['label'] else params['id'],
'markerName': '.stfolder',
'maxConflicts': -1,
@ -174,7 +257,7 @@ def create_folder(params):
'pullerPauseS': 0,
'rescanIntervalS': 3600,
'scanProgressIntervalS': 0,
'type': 'sendreceive',
'type': params['type'],
'useLargeBlocks': False,
'versioning': {
'params': {},
@ -183,15 +266,6 @@ def create_folder(params):
'weakHashThresholdPct': 25
}
# Collect wanted devices to share folder with
for device_id in params['devices']:
folder['devices'].append({
'deviceID': device_id,
'introducedBy': '',
})
return folder
def run_module():
# module arguments
module_args = url_argument_spec()
@ -200,8 +274,13 @@ def run_module():
label=dict(type='str', required=False),
path=dict(type='path', required=False),
devices=dict(type='list', required=False, default=False),
fs_watcher=dict(type='bool', default=True),
ignore_perms=dict(type='bool', required=False, default=False),
type=dict(type='str', default='sendreceive',
choices=['sendreceive', 'sendonly', 'receiveonly']),
host=dict(type='str', default='http://127.0.0.1:8384'),
api_key=dict(type='str', required=False, no_log=True),
config_file=dict(type='path', required=False),
timeout=dict(type='int', default=30),
state=dict(type='str', default='present',
choices=['absent', 'present', 'pause']),
@ -230,6 +309,8 @@ def run_module():
module.params['api_key'] = get_key_from_filesystem(module)
config = get_config(module)
self_id = get_status(module)['myID']
devices_mapping = get_devices_mapping(config)
if module.params['state'] == 'absent':
# Remove folder from list, if found
for idx, folder in enumerate(config['folders']):
@ -238,22 +319,21 @@ def run_module():
result['changed'] = True
break
else:
# Bail-out if folder is already added
for folder in config['folders']:
if folder['id'] == module.params['id']:
want_pause = module.params['state'] == 'pause'
if (want_pause and folder['paused']) or \
(not want_pause and not folder['paused']):
module.exit_json(**result)
else:
folder['paused'] = want_pause
result['changed'] = True
break
folder_config = get_folder_config(module.params['id'], config)
folder_config_devices = (
[d['deviceID'] for d in folder_config['devices']] if folder_config else []
)
folder_config_wanted = create_folder(
module.params, self_id, folder_config_devices, devices_mapping
)
# Append the new folder into configuration
if not result['changed']:
folder = create_folder(module.params)
config['folders'].append(folder)
if folder_config is None:
config['folders'].append(folder_config_wanted)
result['changed'] = True
elif folder_config != folder_config_wanted:
# Update the folder configuration in-place
folder_config.clear()
folder_config.update(folder_config_wanted)
result['changed'] = True
if result['changed']: