notes: 2.3.0 -> 2.3.1 (#352950)
[NixPkgs.git] / nixos / tests / syncthing-many-devices.nix
blob2251bf077453374ea2b25da3bd74e198a98564b2
1 import ./make-test-python.nix ({ lib, pkgs, ... }:
3 # This nixosTest is supposed to check the following:
5 # - Whether syncthing's API handles multiple requests for many devices, see
6 #   https://github.com/NixOS/nixpkgs/issues/260262
8 # - Whether syncthing-init.service generated bash script removes devices and
9 #   folders that are not present in the user's configuration, which is partly
10 #   injected into the script. See also:
11 #   https://github.com/NixOS/nixpkgs/issues/259256
14 let
15   # Just a long path not to copy paste
16   configPath = "/var/lib/syncthing/.config/syncthing/config.xml";
18   # We will iterate this and more attribute sets defined here, later in the
19   # testScript. Start with this, and distinguish these settings from other
20   # settings, as we check these differently with xmllint, due to the ID.
21   settingsWithId = {
22     devices = {
23       # All of the device IDs used here were generated by the following command:
24       #
25       #    (${pkgs.syncthing}/bin/syncthing generate --home /tmp/foo\
26       #       | grep ID: | sed 's/.*ID: *//') && rm -rf /tmp/foo
27       #
28       # See also discussion at:
29       # https://forum.syncthing.net/t/how-to-generate-dummy-device-ids/20927/8
30       test_device1.id  = "IVTZ5XF-EF3GKFT-GS4AZLG-IT6H2ZP-6WK75SF-AFXQXJJ-BNRZ4N6-XPDKVAU";
31       test_device2.id  = "5C35H56-Z2GFF4F-F3IVD4B-GJYVWIE-SMDBJZN-GI66KWP-52JIQGN-4AVLYAM";
32       test_device3.id  = "XKLSKHE-BZOHV7B-WQZACEF-GTH36NP-6JSBB6L-RXS3M7C-EEVWO2L-C5B4OAJ";
33       test_device4.id  = "APN5Q7J-35GZETO-5KCLF35-ZA7KBWK-HGWPBNG-FERF24R-UTLGMEX-4VJ6PQX";
34       test_device5.id  = "D4YXQEE-5MK6LIK-BRU5QWM-ZRXJCK2-N3RQBJE-23JKTQQ-LYGDPHF-RFPZIQX";
35       test_device6.id  = "TKMCH64-T44VSLI-6FN2YLF-URBZOBR-ATO4DYX-GEDRIII-CSMRQAI-UAQMDQG";
36       test_device7.id  = "472EEBG-Q4PZCD4-4CX6PGF-XS3FSQ2-UFXBZVB-PGNXWLX-7FKBLER-NJ3EMAR";
37       test_device8.id  = "HW6KUMK-WTBG24L-2HZQXLO-TGJSG2M-2JG3FHX-5OGYRUJ-T6L5NN7-L364QAZ";
38       test_device9.id  = "YAE24AP-7LSVY4T-J74ZSEM-A2IK6RB-FGA35TP-AG4CSLU-ED4UYYY-2J2TDQU";
39       test_device10.id = "277XFSB-OFMQOBI-3XGNGUE-Y7FWRV3-QQDADIY-QIIPQ26-EOGTYKW-JP2EXAI";
40       test_device11.id = "2WWXVTN-Q3QWAAY-XFORMRM-2FDI5XZ-OGN33BD-XOLL42R-DHLT2ML-QYXDQAU";
41     };
42     # Generates a few folders with IDs and paths as written...
43     folders = lib.pipe 6 [
44       (builtins.genList (x: {
45         name = "/var/lib/syncthing/test_folder${builtins.toString x}";
46         value = {
47           id = "DontDeleteMe${builtins.toString x}";
48         };
49       }))
50       builtins.listToAttrs
51     ];
52   };
53   # Non default options that we check later if were applied
54   settingsWithoutId = {
55     options = {
56       autoUpgradeIntervalH = 0;
57       urAccepted = -1;
58     };
59     gui = {
60       theme = "dark";
61     };
62   };
63   # Used later when checking whether settings were set in config.xml:
64   checkSettingWithId = { t # t for type
65   , id
66   , not ? false
67   }: ''
68     print("Searching for a ${t} with id ${id}")
69     configVal_${t} = machine.succeed(
70         "${pkgs.libxml2}/bin/xmllint "
71         "--xpath 'string(//${t}[@id=\"${id}\"]/@id)' ${configPath}"
72     )
73     print("${t}.id = {}".format(configVal_${t}))
74     assert "${id}" ${if not then "not" else ""} in configVal_${t}
75   '';
76   # Same as checkSettingWithId, but for 'options' and 'gui'
77   checkSettingWithoutId = { t # t for type
78   , n # n for name
79   , v # v for value
80   , not ? false
81   }: ''
82     print("checking whether setting ${t}.${n} is set to ${v}")
83     configVal_${t}_${n} = machine.succeed(
84         "${pkgs.libxml2}/bin/xmllint "
85         "--xpath 'string(/configuration/${t}/${n})' ${configPath}"
86     )
87     print("${t}.${n} = {}".format(configVal_${t}_${n}))
88     assert "${v}" ${if not then "not" else ""} in configVal_${t}_${n}
89   '';
90   # Removes duplication a bit to define this function for the IDs to delete -
91   # we check whether they were added after our script ran, and before the
92   # systemd unit's bash script ran, and afterwards - whether the systemd unit
93   # worked.
94   checkSettingsToDelete = {
95     not
96   }: lib.pipe IDsToDelete [
97     (lib.mapAttrsToList (t: id:
98       checkSettingWithId {
99         inherit t id;
100         inherit not;
101       }
102     ))
103     lib.concatStrings
104   ];
105   # These IDs are added to syncthing using the API, similarly to how the
106   # generated systemd unit's bash script does it. Only we add it and expect the
107   # systemd unit bash script to remove them when executed.
108   IDsToDelete = {
109     # Also created using the syncthing generate command above
110     device = "LZ2CTHT-3W2M7BC-CMKDFZL-DLUQJFS-WJR73PA-NZGODWG-DZBHCHI-OXTQXAK";
111     # Intentionally this is a substring of the IDs of the 'test_folder's, as
112     # explained in: https://github.com/NixOS/nixpkgs/issues/259256
113     folder = "DeleteMe";
114   };
115   addDeviceToDeleteScript = pkgs.writers.writeBash "syncthing-add-device-to-delete.sh" ''
116     set -euo pipefail
118     export RUNTIME_DIRECTORY=/tmp
120     curl() {
121         # get the api key by parsing the config.xml
122         while
123             ! ${pkgs.libxml2}/bin/xmllint \
124                 --xpath 'string(configuration/gui/apikey)' \
125                 ${configPath} \
126                 >"$RUNTIME_DIRECTORY/api_key"
127         do sleep 1; done
129         (printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
131         ${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
132             --retry 1000 --retry-delay 1 --retry-all-errors \
133             "$@"
134     }
135     curl -d ${lib.escapeShellArg (builtins.toJSON { deviceID = IDsToDelete.device;})} \
136         -X POST 127.0.0.1:8384/rest/config/devices
137     curl -d ${lib.escapeShellArg (builtins.toJSON { id = IDsToDelete.folder;})} \
138         -X POST 127.0.0.1:8384/rest/config/folders
139   '';
140 in {
141   name = "syncthing-init";
142   meta.maintainers = with lib.maintainers; [ doronbehar ];
144   nodes.machine = {
145     services.syncthing = {
146       enable = true;
147       overrideDevices = true;
148       overrideFolders = true;
149       settings = settingsWithoutId // settingsWithId;
150     };
151   };
152   testScript = ''
153     machine.wait_for_unit("syncthing-init.service")
154   '' + (lib.pipe settingsWithId [
155     # Check that folders and devices were added properly and that all IDs exist
156     (lib.mapAttrsRecursive (path: id:
157       checkSettingWithId {
158         # plural -> solitary
159         t = (lib.removeSuffix "s" (builtins.elemAt path 0));
160         inherit id;
161       }
162     ))
163     # Get all the values we applied the above function upon
164     (lib.collect builtins.isString)
165     lib.concatStrings
166   ]) + (lib.pipe settingsWithoutId [
167     # Check that all other syncthing.settings were added properly with correct
168     # values
169     (lib.mapAttrsRecursive (path: value:
170       checkSettingWithoutId {
171         t = (builtins.elemAt path 0);
172         n = (builtins.elemAt path 1);
173         v = (builtins.toString value);
174       }
175     ))
176     # Get all the values we applied the above function upon
177     (lib.collect builtins.isString)
178     lib.concatStrings
179   ]) + ''
180     # Run the script on the machine
181     machine.succeed("${addDeviceToDeleteScript}")
182   '' + (checkSettingsToDelete {
183     not = false;
184   }) + ''
185     # Useful for debugging later
186     machine.copy_from_vm("${configPath}", "before")
188     machine.systemctl("restart syncthing-init.service")
189     machine.wait_for_unit("syncthing-init.service")
190   '' + (checkSettingsToDelete {
191     not = true;
192   }) + ''
193     # Useful for debugging later
194     machine.copy_from_vm("${configPath}", "after")
196     # Copy the systemd unit's bash script, to inspect it for debugging.
197     mergeScript = machine.succeed(
198         "systemctl cat syncthing-init.service | "
199         "${pkgs.initool}/bin/initool g - Service ExecStart --value-only"
200     ).strip() # strip from new lines
201     machine.copy_from_vm(mergeScript, "")
202   '';