Compare commits

..

14 Commits

Author SHA1 Message Date
Borjan Tchakaloff
3a49d17dfd syncthing_folder: Always share a folder with the current device
All checks were successful
buildbot/travis_bionic Build done.
buildbot/multibuild_parent Build done.
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 # 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. Collection of modules for [Syncthing](https://syncthing.net) management.
## Install ## Install
@ -12,9 +15,9 @@ Copy the `./library` directory to your Ansible project and ensure your
library = ./library 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. 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: 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 * [manage.yml] - Ensure Syncthing devices and folders across devices
[install_syncthing.yml]: http://
[manage.yml]: http://
## Modules ## Modules
### Module: `syncthing_device` ### Module: `syncthing_device`
@ -108,4 +108,5 @@ Examples:
## License ## License
Copyright: (c) 2018, Rafael Bodill `<justrafi at g>` 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) 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 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2018, Rafael Bodill <justrafi at google mail> # 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) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -39,6 +40,12 @@ options:
- API key to use for authentication with host. - API key to use for authentication with host.
If not provided, will try to auto-configure from filesystem. If not provided, will try to auto-configure from filesystem.
required: false 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: timeout:
description: description:
- The socket level timeout in seconds - The socket level timeout in seconds
@ -91,7 +98,10 @@ def make_headers(host, api_key):
def get_key_from_filesystem(module): def get_key_from_filesystem(module):
try: 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) stconfig = parse(stconfigfile)
root = stconfig.getroot() root = stconfig.getroot()
gui = root.find('gui') gui = root.find('gui')
@ -166,6 +176,7 @@ def run_module():
name=dict(type='str', required=False), name=dict(type='str', required=False),
host=dict(type='str', default='http://127.0.0.1:8384'), host=dict(type='str', default='http://127.0.0.1:8384'),
api_key=dict(type='str', required=False, no_log=True), api_key=dict(type='str', required=False, no_log=True),
config_file=dict(type='str', required=False),
timeout=dict(type='int', default=30), timeout=dict(type='int', default=30),
state=dict(type='str', default='present', state=dict(type='str', default='present',
choices=['absent', 'present', 'pause']), choices=['absent', 'present', 'pause']),

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2018, Rafael Bodill <justrafi at gmail> # 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) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = { ANSIBLE_METADATA = {
@ -38,6 +39,20 @@ options:
description: description:
- List of devices to share folder with - List of devices to share folder with
required: false 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: host:
description: description:
- Host to connect to, including port - Host to connect to, including port
@ -47,6 +62,12 @@ options:
- API key to use for authentication with host. - API key to use for authentication with host.
If not provided, will try to auto-configure from filesystem. If not provided, will try to auto-configure from filesystem.
required: false 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: timeout:
description: description:
- The socket level timeout in seconds - 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.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url, url_argument_spec 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': if platform.system() == 'Windows':
DEFAULT_ST_CONFIG_LOCATION = '%localappdata%/Syncthing/config.xml' DEFAULT_ST_CONFIG_LOCATION = '%localappdata%/Syncthing/config.xml'
elif platform.system() == 'Darwin': elif platform.system() == 'Darwin':
@ -94,14 +115,17 @@ else:
DEFAULT_ST_CONFIG_LOCATION = '$HOME/.config/syncthing/config.xml' DEFAULT_ST_CONFIG_LOCATION = '$HOME/.config/syncthing/config.xml'
def make_headers(host, api_key): def make_headers(host, api_key, resource):
url = '{}{}'.format(host, SYNCTHING_API_URI) url = '{}{}/{}'.format(host, SYNCTHING_API_BASE_URI, resource)
headers = {'X-Api-Key': api_key } headers = {'X-Api-Key': api_key }
return url, headers return url, headers
def get_key_from_filesystem(module): def get_key_from_filesystem(module):
try: 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) stconfig = parse(stconfigfile)
root = stconfig.getroot() root = stconfig.getroot()
gui = root.find('gui') gui = root.find('gui')
@ -111,12 +135,18 @@ def get_key_from_filesystem(module):
module.fail_json(msg="Auto-configuration failed. Please specify" module.fail_json(msg="Auto-configuration failed. Please specify"
"the API key manually.") "the API key manually.")
# Fetch Syncthing configuration def get_data_from_rest_api(module, resource):
def get_config(module): url, headers = make_headers(
url, headers = make_headers(module.params['host'], module.params['api_key']) module.params['host'], module.params['api_key'], resource
)
resp, info = fetch_url( resp, info = fetch_url(
module, url, data=None, headers=headers, module,
method='GET', timeout=module.params['timeout']) url,
data=None,
headers=headers,
method='GET',
timeout=module.params['timeout']
)
if not info or info['status'] != 200: if not info or info['status'] != 200:
result['response'] = info result['response'] = info
@ -131,9 +161,34 @@ def get_config(module):
return json.loads(content) 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 # Post the new configuration to Syncthing API
def post_config(module, config, result): 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' headers['Content-Type'] = 'application/json'
result['msg'] = config result['msg'] = config
@ -143,23 +198,51 @@ def post_config(module, config, result):
if not info or info['status'] != 200: if not info or info['status'] != 200:
result['response'] = str(info) 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 # Returns an object of a new folder
def create_folder(params): def create_folder(params, self_id, current_device_ids, devices_mapping):
folder = { # 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, 'autoNormalize': True,
'copiers': 0, 'copiers': 0,
'devices': [], 'devices': devices,
'disableSparseFiles': False, 'disableSparseFiles': False,
'disableTempIndexes': False, 'disableTempIndexes': False,
'filesystemType': 'basic', 'filesystemType': 'basic',
'fsWatcherDelayS': 10, 'fsWatcherDelayS': 10,
'fsWatcherEnabled': True, 'fsWatcherEnabled': params['fs_watcher'],
'hashers': 0, 'hashers': 0,
'id': params['id'], 'id': params['id'],
'ignoreDelete': False, 'ignoreDelete': False,
'ignorePerms': False, 'ignorePerms': params['ignore_perms'],
'label': params['label'] if params['label'] else params['id'], 'label': params['label'] if params['label'] else params['id'],
'markerName': '.stfolder', 'markerName': '.stfolder',
'maxConflicts': -1, 'maxConflicts': -1,
@ -174,7 +257,7 @@ def create_folder(params):
'pullerPauseS': 0, 'pullerPauseS': 0,
'rescanIntervalS': 3600, 'rescanIntervalS': 3600,
'scanProgressIntervalS': 0, 'scanProgressIntervalS': 0,
'type': 'sendreceive', 'type': params['type'],
'useLargeBlocks': False, 'useLargeBlocks': False,
'versioning': { 'versioning': {
'params': {}, 'params': {},
@ -183,15 +266,6 @@ def create_folder(params):
'weakHashThresholdPct': 25 '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(): def run_module():
# module arguments # module arguments
module_args = url_argument_spec() module_args = url_argument_spec()
@ -200,8 +274,13 @@ def run_module():
label=dict(type='str', required=False), label=dict(type='str', required=False),
path=dict(type='path', required=False), path=dict(type='path', required=False),
devices=dict(type='list', required=False, default=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'), host=dict(type='str', default='http://127.0.0.1:8384'),
api_key=dict(type='str', required=False, no_log=True), api_key=dict(type='str', required=False, no_log=True),
config_file=dict(type='path', required=False),
timeout=dict(type='int', default=30), timeout=dict(type='int', default=30),
state=dict(type='str', default='present', state=dict(type='str', default='present',
choices=['absent', 'present', 'pause']), choices=['absent', 'present', 'pause']),
@ -230,6 +309,8 @@ def run_module():
module.params['api_key'] = get_key_from_filesystem(module) module.params['api_key'] = get_key_from_filesystem(module)
config = get_config(module) config = get_config(module)
self_id = get_status(module)['myID']
devices_mapping = get_devices_mapping(config)
if module.params['state'] == 'absent': if module.params['state'] == 'absent':
# Remove folder from list, if found # Remove folder from list, if found
for idx, folder in enumerate(config['folders']): for idx, folder in enumerate(config['folders']):
@ -238,22 +319,21 @@ def run_module():
result['changed'] = True result['changed'] = True
break break
else: else:
# Bail-out if folder is already added folder_config = get_folder_config(module.params['id'], config)
for folder in config['folders']: folder_config_devices = (
if folder['id'] == module.params['id']: [d['deviceID'] for d in folder_config['devices']] if folder_config else []
want_pause = module.params['state'] == 'pause' )
if (want_pause and folder['paused']) or \ folder_config_wanted = create_folder(
(not want_pause and not folder['paused']): module.params, self_id, folder_config_devices, devices_mapping
module.exit_json(**result) )
else:
folder['paused'] = want_pause
result['changed'] = True
break
# Append the new folder into configuration if folder_config is None:
if not result['changed']: config['folders'].append(folder_config_wanted)
folder = create_folder(module.params) result['changed'] = True
config['folders'].append(folder) elif folder_config != folder_config_wanted:
# Update the folder configuration in-place
folder_config.clear()
folder_config.update(folder_config_wanted)
result['changed'] = True result['changed'] = True
if result['changed']: if result['changed']: