about summary refs log tree commit diff
path: root/nixpkgs/nixos/tests/syncthing-many-devices.nix
blob: 2251bf077453374ea2b25da3bd74e198a98564b2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import ./make-test-python.nix ({ lib, pkgs, ... }:

# This nixosTest is supposed to check the following:
#
# - Whether syncthing's API handles multiple requests for many devices, see
#   https://github.com/NixOS/nixpkgs/issues/260262
#
# - Whether syncthing-init.service generated bash script removes devices and
#   folders that are not present in the user's configuration, which is partly
#   injected into the script. See also:
#   https://github.com/NixOS/nixpkgs/issues/259256
#

let
  # Just a long path not to copy paste
  configPath = "/var/lib/syncthing/.config/syncthing/config.xml";

  # We will iterate this and more attribute sets defined here, later in the
  # testScript. Start with this, and distinguish these settings from other
  # settings, as we check these differently with xmllint, due to the ID.
  settingsWithId = {
    devices = {
      # All of the device IDs used here were generated by the following command:
      #
      #    (${pkgs.syncthing}/bin/syncthing generate --home /tmp/foo\
      #       | grep ID: | sed 's/.*ID: *//') && rm -rf /tmp/foo
      #
      # See also discussion at:
      # https://forum.syncthing.net/t/how-to-generate-dummy-device-ids/20927/8
      test_device1.id  = "IVTZ5XF-EF3GKFT-GS4AZLG-IT6H2ZP-6WK75SF-AFXQXJJ-BNRZ4N6-XPDKVAU";
      test_device2.id  = "5C35H56-Z2GFF4F-F3IVD4B-GJYVWIE-SMDBJZN-GI66KWP-52JIQGN-4AVLYAM";
      test_device3.id  = "XKLSKHE-BZOHV7B-WQZACEF-GTH36NP-6JSBB6L-RXS3M7C-EEVWO2L-C5B4OAJ";
      test_device4.id  = "APN5Q7J-35GZETO-5KCLF35-ZA7KBWK-HGWPBNG-FERF24R-UTLGMEX-4VJ6PQX";
      test_device5.id  = "D4YXQEE-5MK6LIK-BRU5QWM-ZRXJCK2-N3RQBJE-23JKTQQ-LYGDPHF-RFPZIQX";
      test_device6.id  = "TKMCH64-T44VSLI-6FN2YLF-URBZOBR-ATO4DYX-GEDRIII-CSMRQAI-UAQMDQG";
      test_device7.id  = "472EEBG-Q4PZCD4-4CX6PGF-XS3FSQ2-UFXBZVB-PGNXWLX-7FKBLER-NJ3EMAR";
      test_device8.id  = "HW6KUMK-WTBG24L-2HZQXLO-TGJSG2M-2JG3FHX-5OGYRUJ-T6L5NN7-L364QAZ";
      test_device9.id  = "YAE24AP-7LSVY4T-J74ZSEM-A2IK6RB-FGA35TP-AG4CSLU-ED4UYYY-2J2TDQU";
      test_device10.id = "277XFSB-OFMQOBI-3XGNGUE-Y7FWRV3-QQDADIY-QIIPQ26-EOGTYKW-JP2EXAI";
      test_device11.id = "2WWXVTN-Q3QWAAY-XFORMRM-2FDI5XZ-OGN33BD-XOLL42R-DHLT2ML-QYXDQAU";
    };
    # Generates a few folders with IDs and paths as written...
    folders = lib.pipe 6 [
      (builtins.genList (x: {
        name = "/var/lib/syncthing/test_folder${builtins.toString x}";
        value = {
          id = "DontDeleteMe${builtins.toString x}";
        };
      }))
      builtins.listToAttrs
    ];
  };
  # Non default options that we check later if were applied
  settingsWithoutId = {
    options = {
      autoUpgradeIntervalH = 0;
      urAccepted = -1;
    };
    gui = {
      theme = "dark";
    };
  };
  # Used later when checking whether settings were set in config.xml:
  checkSettingWithId = { t # t for type
  , id
  , not ? false
  }: ''
    print("Searching for a ${t} with id ${id}")
    configVal_${t} = machine.succeed(
        "${pkgs.libxml2}/bin/xmllint "
        "--xpath 'string(//${t}[@id=\"${id}\"]/@id)' ${configPath}"
    )
    print("${t}.id = {}".format(configVal_${t}))
    assert "${id}" ${if not then "not" else ""} in configVal_${t}
  '';
  # Same as checkSettingWithId, but for 'options' and 'gui'
  checkSettingWithoutId = { t # t for type
  , n # n for name
  , v # v for value
  , not ? false
  }: ''
    print("checking whether setting ${t}.${n} is set to ${v}")
    configVal_${t}_${n} = machine.succeed(
        "${pkgs.libxml2}/bin/xmllint "
        "--xpath 'string(/configuration/${t}/${n})' ${configPath}"
    )
    print("${t}.${n} = {}".format(configVal_${t}_${n}))
    assert "${v}" ${if not then "not" else ""} in configVal_${t}_${n}
  '';
  # Removes duplication a bit to define this function for the IDs to delete -
  # we check whether they were added after our script ran, and before the
  # systemd unit's bash script ran, and afterwards - whether the systemd unit
  # worked.
  checkSettingsToDelete = {
    not
  }: lib.pipe IDsToDelete [
    (lib.mapAttrsToList (t: id:
      checkSettingWithId {
        inherit t id;
        inherit not;
      }
    ))
    lib.concatStrings
  ];
  # These IDs are added to syncthing using the API, similarly to how the
  # generated systemd unit's bash script does it. Only we add it and expect the
  # systemd unit bash script to remove them when executed.
  IDsToDelete = {
    # Also created using the syncthing generate command above
    device = "LZ2CTHT-3W2M7BC-CMKDFZL-DLUQJFS-WJR73PA-NZGODWG-DZBHCHI-OXTQXAK";
    # Intentionally this is a substring of the IDs of the 'test_folder's, as
    # explained in: https://github.com/NixOS/nixpkgs/issues/259256
    folder = "DeleteMe";
  };
  addDeviceToDeleteScript = pkgs.writers.writeBash "syncthing-add-device-to-delete.sh" ''
    set -euo pipefail

    export RUNTIME_DIRECTORY=/tmp

    curl() {
        # get the api key by parsing the config.xml
        while
            ! ${pkgs.libxml2}/bin/xmllint \
                --xpath 'string(configuration/gui/apikey)' \
                ${configPath} \
                >"$RUNTIME_DIRECTORY/api_key"
        do sleep 1; done

        (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"

        ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
            --retry 1000 --retry-delay 1 --retry-all-errors \
            "$@"
    }
    curl -d ${lib.escapeShellArg (builtins.toJSON { deviceID = IDsToDelete.device;})} \
        -X POST 127.0.0.1:8384/rest/config/devices
    curl -d ${lib.escapeShellArg (builtins.toJSON { id = IDsToDelete.folder;})} \
        -X POST 127.0.0.1:8384/rest/config/folders
  '';
in {
  name = "syncthing-init";
  meta.maintainers = with lib.maintainers; [ doronbehar ];

  nodes.machine = {
    services.syncthing = {
      enable = true;
      overrideDevices = true;
      overrideFolders = true;
      settings = settingsWithoutId // settingsWithId;
    };
  };
  testScript = ''
    machine.wait_for_unit("syncthing-init.service")
  '' + (lib.pipe settingsWithId [
    # Check that folders and devices were added properly and that all IDs exist
    (lib.mapAttrsRecursive (path: id:
      checkSettingWithId {
        # plural -> solitary
        t = (lib.removeSuffix "s" (builtins.elemAt path 0));
        inherit id;
      }
    ))
    # Get all the values we applied the above function upon
    (lib.collect builtins.isString)
    lib.concatStrings
  ]) + (lib.pipe settingsWithoutId [
    # Check that all other syncthing.settings were added properly with correct
    # values
    (lib.mapAttrsRecursive (path: value:
      checkSettingWithoutId {
        t = (builtins.elemAt path 0);
        n = (builtins.elemAt path 1);
        v = (builtins.toString value);
      }
    ))
    # Get all the values we applied the above function upon
    (lib.collect builtins.isString)
    lib.concatStrings
  ]) + ''
    # Run the script on the machine
    machine.succeed("${addDeviceToDeleteScript}")
  '' + (checkSettingsToDelete {
    not = false;
  }) + ''
    # Useful for debugging later
    machine.copy_from_vm("${configPath}", "before")

    machine.systemctl("restart syncthing-init.service")
    machine.wait_for_unit("syncthing-init.service")
  '' + (checkSettingsToDelete {
    not = true;
  }) + ''
    # Useful for debugging later
    machine.copy_from_vm("${configPath}", "after")

    # Copy the systemd unit's bash script, to inspect it for debugging.
    mergeScript = machine.succeed(
        "systemctl cat syncthing-init.service | "
        "${pkgs.initool}/bin/initool g - Service ExecStart --value-only"
    ).strip() # strip from new lines
    machine.copy_from_vm(mergeScript, "")
  '';
})